-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathClient.swift
206 lines (188 loc) · 5.88 KB
/
Client.swift
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import Foundation
public struct Client: Sendable {
public let url: URL
public var token: String?
public var headers: [HTTPHeader]
public init(url: URL, token: String? = nil, headers: [HTTPHeader] = []) {
self.url = url
self.token = token
self.headers = headers
}
func request(
_ path: String,
method: HTTPMethod,
body: some Encodable & Sendable
) async throws(SDKError) -> HTTPResponse {
var headers = headers
if let token {
headers += [.init(name: Headers.sessionToken, value: token)]
}
return try await CoderSDK.request(
baseURL: url,
path: path,
method: method,
headers: headers,
body: body
)
}
func request(
_ path: String,
method: HTTPMethod
) async throws(SDKError) -> HTTPResponse {
var headers = headers
if let token {
headers += [.init(name: Headers.sessionToken, value: token)]
}
return try await CoderSDK.request(
baseURL: url,
path: path,
method: method,
headers: headers
)
}
}
public struct APIError: Decodable, Sendable {
public let response: Response
public let statusCode: Int
public let method: String
public let url: URL
var description: String {
var components = ["\(method) \(url.absoluteString)\nUnexpected status code \(statusCode):\n\(response.message)"]
if let detail = response.detail {
components.append("\tError: \(detail)")
}
if let validations = response.validations, !validations.isEmpty {
let validationMessages = validations.map { "\t\($0.field): \($0.detail)" }
components.append(contentsOf: validationMessages)
}
return components.joined(separator: "\n")
}
}
public struct Response: Decodable, Sendable {
let message: String
let detail: String?
let validations: [FieldValidation]?
}
public struct FieldValidation: Decodable, Sendable {
let field: String
let detail: String
}
public enum SDKError: Error {
case api(APIError)
case network(any Error)
case unexpectedResponse(String)
case encodeFailure(any Error)
public var description: String {
switch self {
case let .api(error):
error.description
case let .network(error):
error.localizedDescription
case let .unexpectedResponse(data):
"Unexpected response: \(data)"
case let .encodeFailure(error):
"Failed to encode body: \(error.localizedDescription)"
}
}
public var localizedDescription: String { description }
}
let decoder: JSONDecoder = {
var dec = JSONDecoder()
dec.dateDecodingStrategy = .iso8601withOptionalFractionalSeconds
return dec
}()
let encoder: JSONEncoder = {
var enc = JSONEncoder()
enc.dateEncodingStrategy = .iso8601withFractionalSeconds
return enc
}()
func doRequest(
baseURL: URL,
path: String,
method: HTTPMethod,
headers: [HTTPHeader] = [],
body: Data? = nil
) async throws(SDKError) -> HTTPResponse {
let url = baseURL.appendingPathComponent(path)
var req = URLRequest(url: url)
req.httpMethod = method.rawValue
for header in headers {
req.addValue(header.value, forHTTPHeaderField: header.name)
}
req.httpBody = body
let data: Data
let resp: URLResponse
do {
(data, resp) = try await URLSession.shared.data(for: req)
} catch {
throw .network(error)
}
guard let httpResponse = resp as? HTTPURLResponse else {
throw .unexpectedResponse(String(data: data, encoding: .utf8) ?? "<non-utf8 data>")
}
return HTTPResponse(resp: httpResponse, data: data, req: req)
}
func request(
baseURL: URL,
path: String,
method: HTTPMethod,
headers: [HTTPHeader] = [],
body: some Encodable & Sendable
) async throws(SDKError) -> HTTPResponse {
let encodedBody: Data
do {
encodedBody = try encoder.encode(body)
} catch {
throw .encodeFailure(error)
}
return try await doRequest(
baseURL: baseURL,
path: path,
method: method,
headers: headers,
body: encodedBody
)
}
func request(
baseURL: URL,
path: String,
method: HTTPMethod,
headers: [HTTPHeader] = []
) async throws(SDKError) -> HTTPResponse {
try await doRequest(
baseURL: baseURL,
path: path,
method: method,
headers: headers
)
}
func responseAsError(_ resp: HTTPResponse) -> SDKError {
do {
let body = try decode(Response.self, from: resp.data)
let out = APIError(
response: body,
statusCode: resp.resp.statusCode,
method: resp.req.httpMethod!,
url: resp.req.url!
)
return .api(out)
} catch {
return .unexpectedResponse(String(data: resp.data, encoding: .utf8) ?? "<non-utf8 data>")
}
}
// Wrapper around JSONDecoder.decode that displays useful error messages from `DecodingError`.
func decode<T: Decodable>(_: T.Type, from data: Data) throws(SDKError) -> T {
do {
return try decoder.decode(T.self, from: data)
} catch let DecodingError.keyNotFound(_, context) {
throw .unexpectedResponse("Key not found: \(context.debugDescription)")
} catch let DecodingError.valueNotFound(_, context) {
throw .unexpectedResponse("Value not found: \(context.debugDescription)")
} catch let DecodingError.typeMismatch(_, context) {
throw .unexpectedResponse("Type mismatch: \(context.debugDescription)")
} catch let DecodingError.dataCorrupted(context) {
throw .unexpectedResponse("Data corrupted: \(context.debugDescription)")
} catch {
throw .unexpectedResponse(String(data: data.prefix(1024), encoding: .utf8) ?? "<non-utf8 data>")
}
}