-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathClient.swift
140 lines (125 loc) · 3.99 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
import Alamofire
import Foundation
protocol Client: Sendable {
init(url: URL, token: String?)
func user(_ ident: String) async throws(ClientError) -> User
}
struct CoderClient: Client {
public let url: URL
public var token: String?
static let decoder: JSONDecoder = {
var dec = JSONDecoder()
dec.dateDecodingStrategy = .iso8601withOptionalFractionalSeconds
return dec
}()
let encoder: JSONEncoder = {
var enc = JSONEncoder()
enc.dateEncodingStrategy = .iso8601withFractionalSeconds
return enc
}()
func request<T: Encodable & Sendable>(
_ path: String,
method: HTTPMethod,
body: T? = nil
) async throws(ClientError) -> HTTPResponse {
let url = self.url.appendingPathComponent(path)
let headers: HTTPHeaders? = token.map { [Headers.sessionToken: $0] }
let out = await AF.request(
url,
method: method,
parameters: body,
headers: headers
).serializingData().response
switch out.result {
case let .success(data):
return HTTPResponse(resp: out.response!, data: data, req: out.request)
case let .failure(error):
throw ClientError.reqError(error)
}
}
func request(
_ path: String,
method: HTTPMethod
) async throws(ClientError) -> HTTPResponse {
let url = self.url.appendingPathComponent(path)
let headers: HTTPHeaders? = token.map { [Headers.sessionToken: $0] }
let out = await AF.request(
url,
method: method,
headers: headers
).serializingData().response
switch out.result {
case let .success(data):
return HTTPResponse(resp: out.response!, data: data, req: out.request)
case let .failure(error):
throw ClientError.reqError(error)
}
}
func responseAsError(_ resp: HTTPResponse) -> ClientError {
do {
let body = try CoderClient.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 ClientError.apiError(out)
} catch {
return ClientError.unexpectedResponse(resp.data[...1024])
}
}
enum Headers {
static let sessionToken = "Coder-Session-Token"
}
}
struct HTTPResponse {
let resp: HTTPURLResponse
let data: Data
let req: URLRequest?
}
struct APIError: Decodable {
let response: Response
let statusCode: Int
let method: String?
let url: URL?
var description: String {
var components: [String] = []
if let method = method, let url = url {
components.append("\(method) \(url.absoluteString)")
}
components.append("Unexpected 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")
}
}
struct Response: Decodable {
let message: String
let detail: String?
let validations: [FieldValidation]?
}
struct FieldValidation: Decodable {
let field: String
let detail: String
}
enum ClientError: Error {
case apiError(APIError)
case reqError(AFError)
case unexpectedResponse(Data)
var description: String {
switch self {
case let .apiError(error):
return error.description
case let .reqError(error):
return error.localizedDescription
case let .unexpectedResponse(data):
return "Unexpected response: \(data)"
}
}
}