-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathClient.swift
156 lines (140 loc) · 5.16 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
import Foundation
public struct Client {
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
}
static let decoder: JSONDecoder = {
var dec = JSONDecoder()
dec.dateDecodingStrategy = .iso8601withOptionalFractionalSeconds
return dec
}()
static let encoder: JSONEncoder = {
var enc = JSONEncoder()
enc.dateEncodingStrategy = .iso8601withFractionalSeconds
return enc
}()
private func doRequest(
path: String,
method: HTTPMethod,
body: Data? = nil
) async throws(ClientError) -> HTTPResponse {
let url = url.appendingPathComponent(path)
var req = URLRequest(url: url)
if let token { req.addValue(token, forHTTPHeaderField: Headers.sessionToken) }
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(
_ path: String,
method: HTTPMethod,
body: some Encodable & Sendable
) async throws(ClientError) -> HTTPResponse {
let encodedBody: Data?
do {
encodedBody = try Client.encoder.encode(body)
} catch {
throw .encodeFailure(error)
}
return try await doRequest(path: path, method: method, body: encodedBody)
}
func request(
_ path: String,
method: HTTPMethod
) async throws(ClientError) -> HTTPResponse {
try await doRequest(path: path, method: method)
}
func responseAsError(_ resp: HTTPResponse) -> ClientError {
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>(_: T.Type, from data: Data) throws(ClientError) -> T where T: Decodable {
do {
return try Client.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>")
}
}
}
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 ClientError: 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 }
}