APIClient.swift
4.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//
// APIClient.swift
// hippo-watch Watch App
//
import Foundation
enum NetworkEnvironment: Int {
case dev = 0
case xlab = 1
case release = 2
static var current: NetworkEnvironment = .release
var baseURL: URL {
switch self {
case .dev:
return URL(string: "https://dev-api.doublefeel.cn")!
case .xlab:
return URL(string: "https://xlab-api.doublefeel.cn")!
case .release:
return URL(string: "https://api.doublefeel.cn")!
}
}
}
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
}
enum ParameterEncoding {
case url
case json
}
protocol APIEndpoint {
var path: String { get }
var method: HTTPMethod { get }
var parameters: [String: Any]? { get }
var encoding: ParameterEncoding { get }
}
enum APIClientError: LocalizedError {
case invalidURL
case invalidResponse
case badStatus(Int, String?)
case emptyResponse
var errorDescription: String? {
switch self {
case .invalidURL:
return "接口地址无效"
case .invalidResponse:
return "接口响应无效"
case .badStatus(let statusCode, let message):
return message ?? "接口请求失败:\(statusCode)"
case .emptyResponse:
return "接口返回为空"
}
}
}
final class APIClient {
static let shared = APIClient()
private let session: URLSession
private let decoder: JSONDecoder
private init(session: URLSession = .shared) {
self.session = session
self.decoder = JSONDecoder()
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
}
func request<T: Decodable>(_ endpoint: APIEndpoint) async throws -> T {
let request = try makeRequest(for: endpoint)
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIClientError.invalidResponse
}
guard (200..<300).contains(httpResponse.statusCode) else {
throw APIClientError.badStatus(httpResponse.statusCode, parseErrorMessage(from: data))
}
if T.self == EmptyResponse.self {
return EmptyResponse() as! T
}
guard !data.isEmpty else {
throw APIClientError.emptyResponse
}
return try decoder.decode(T.self, from: data)
}
private func makeRequest(for endpoint: APIEndpoint) throws -> URLRequest {
guard var components = URLComponents(
url: NetworkEnvironment.current.baseURL.appendingPathComponent(endpoint.path),
resolvingAgainstBaseURL: false
) else {
throw APIClientError.invalidURL
}
if endpoint.encoding == .url, let parameters = endpoint.parameters {
components.queryItems = parameters.map { key, value in
URLQueryItem(name: key, value: String(describing: value))
}
}
guard let url = components.url else {
throw APIClientError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = endpoint.method.rawValue
request.timeoutInterval = 30
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(WatchUserAgent.share.finalUA.isEmpty ? "doublefeel" : WatchUserAgent.share.finalUA, forHTTPHeaderField: "User-Agent")
if let token = WatchUserinfoManager.share.token, !token.isEmpty {
request.setValue(token, forHTTPHeaderField: "access_token")
}
if endpoint.encoding == .json, let parameters = endpoint.parameters {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
}
return request
}
private func parseErrorMessage(from data: Data) -> String? {
guard !data.isEmpty,
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return nil
}
return object["error"] as? String
?? object["message"] as? String
?? object["detail"] as? String
}
}
extension Error {
func getToastErrorDescription() -> String {
if let localizedError = self as? LocalizedError,
let description = localizedError.errorDescription {
return description
}
return localizedDescription
}
}