-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathClient.swift
139 lines (124 loc) · 4.08 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
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(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 Client.decoder.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(resp.data.prefix(1024))
}
}
}
public struct APIError: Decodable, Sendable {
let response: Response
let statusCode: Int
let method: String
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(Data)
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 or non HTTP response: \(data)"
case let .encodeFailure(error):
"Failed to encode body: \(error)"
}
}
public var localizedDescription: String { description }
}