/
APIClient.swift
192 lines (169 loc) · 7.9 KB
/
APIClient.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
// The MIT License (MIT)
//
// Copyright (c) 2021-2022 Alexander Grebenyuk (github.com/kean).
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
public protocol APIClientDelegate {
func client(_ client: APIClient, willSendRequest request: inout URLRequest) async throws
func shouldClientRetry(_ client: APIClient, for request: URLRequest, withError error: Error) async throws -> Bool
func client(_ client: APIClient, didReceiveInvalidResponse response: HTTPURLResponse, data: Data) -> Error
}
public actor APIClient {
private let conf: Configuration
private let session: URLSession
private let serializer: Serializer
private let delegate: APIClientDelegate
private let loader = DataLoader()
public struct Configuration {
public var host: String
public var port: Int?
/// If `true`, uses `http` instead of `https`.
public var isInsecure = false
public var sessionConfiguration: URLSessionConfiguration = .default
/// By default, uses decoder with `.iso8601` date decoding strategy.
public var decoder: JSONDecoder?
/// By default, uses encoder with `.iso8601` date encoding strategy.
public var encoder: JSONEncoder?
/// The (optional) client delegate.
public var delegate: APIClientDelegate?
/// The (optional) URLSession delegate that allows you to monitor the underlying URLSession.
public var sessionDelegate: URLSessionDelegate?
public init(host: String, sessionConfiguration: URLSessionConfiguration = .default, delegate: APIClientDelegate? = nil) {
self.host = host
self.sessionConfiguration = sessionConfiguration
self.delegate = delegate
}
}
/// Initializes the client with the given parameters.
///
/// - parameter host: A host to be used for requests with relative paths.
/// - parameter configure: Updates the client configuration.
public convenience init(host: String, _ configure: (inout APIClient.Configuration) -> Void = { _ in }) {
var configuration = Configuration(host: host)
configure(&configuration)
self.init(configuration: configuration)
}
/// Initializes the client with the given configuration.
public init(configuration: Configuration) {
self.conf = configuration
let queue = OperationQueue(maxConcurrentOperationCount: 1)
let delegate = URLSessionProxyDelegate.make(loader: loader, delegate: configuration.sessionDelegate)
self.session = URLSession(configuration: configuration.sessionConfiguration, delegate: delegate, delegateQueue: queue)
self.delegate = configuration.delegate ?? DefaultAPIClientDelegate()
self.serializer = Serializer(decoder: configuration.decoder, encoder: configuration.encoder)
}
/// Sends the given request and returns a response with a decoded response value.
public func send<T: Decodable>(_ request: Request<T?>) async throws -> Response<T?> {
try await send(request) { data in
if data.isEmpty {
return nil
} else {
return try await self.decode(data)
}
}
}
/// Sends the given request and returns a response with a decoded response value.
public func send<T: Decodable>(_ request: Request<T>) async throws -> Response<T> {
try await send(request, decode)
}
private func decode<T: Decodable>(_ data: Data) async throws -> T {
if T.self == Data.self {
return data as! T
} else if T.self == String.self {
guard let string = String(data: data, encoding: .utf8) else { throw URLError(.badServerResponse) }
return string as! T
} else {
return try await self.serializer.decode(data)
}
}
/// Sends the given request.
@discardableResult
public func send(_ request: Request<Void>) async throws -> Response<Void> {
try await send(request) { _ in () }
}
private func send<T>(_ request: Request<T>, _ decode: @escaping (Data) async throws -> T) async throws -> Response<T> {
let response = try await data(for: request)
let value = try await decode(response.value)
return response.map { _ in value } // Keep metadata
}
/// Returns response data for the given request.
public func data<T>(for request: Request<T>) async throws -> Response<Data> {
let request = try await makeRequest(for: request)
return try await send(request)
}
private func send(_ request: URLRequest) async throws -> Response<Data> {
do {
return try await actuallySend(request)
} catch {
guard try await delegate.shouldClientRetry(self, for: request, withError: error) else { throw error }
return try await actuallySend(request)
}
}
private func actuallySend(_ request: URLRequest) async throws -> Response<Data> {
var request = request
try await delegate.client(self, willSendRequest: &request)
let (data, response, metrics) = try await loader.data(for: request, session: session)
try validate(response: response, data: data)
return Response(value: data, data: data, request: request, response: response, metrics: metrics)
}
private func makeRequest<T>(for request: Request<T>) async throws -> URLRequest {
let url = try makeURL(path: request.path, query: request.query)
return try await makeRequest(url: url, method: request.method, body: request.body, headers: request.headers)
}
private func makeURL(path: String, query: [(String, String?)]?) throws -> URL {
guard let url = URL(string: path),
var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
throw URLError(.badURL)
}
if path.starts(with: "/") {
components.scheme = conf.isInsecure ? "http" : "https"
components.host = conf.host
if let port = conf.port {
components.port = port
}
}
if let query = query {
components.queryItems = query.map(URLQueryItem.init)
}
guard let url = components.url else {
throw URLError(.badURL)
}
return url
}
private func makeRequest(url: URL, method: String, body: AnyEncodable?, headers: [String: String]?) async throws -> URLRequest {
var request = URLRequest(url: url)
request.allHTTPHeaderFields = headers
request.httpMethod = method
if let body = body {
request.httpBody = try await serializer.encode(body)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
}
request.setValue("application/json", forHTTPHeaderField: "Accept")
return request
}
private func validate(response: URLResponse, data: Data) throws {
guard let httpResponse = response as? HTTPURLResponse else { return }
if !(200..<300).contains(httpResponse.statusCode) {
throw delegate.client(self, didReceiveInvalidResponse: httpResponse, data: data)
}
}
}
public enum APIError: Error, LocalizedError {
case unacceptableStatusCode(Int)
public var errorDescription: String? {
switch self {
case .unacceptableStatusCode(let statusCode):
return "Response status code was unacceptable: \(statusCode)."
}
}
}
public extension APIClientDelegate {
func client(_ client: APIClient, willSendRequest request: inout URLRequest) async throws {}
func shouldClientRetry(_ client: APIClient, for request: URLRequest, withError error: Error) async throws -> Bool { false }
func client(_ client: APIClient, didReceiveInvalidResponse response: HTTPURLResponse, data: Data) -> Error {
APIError.unacceptableStatusCode(response.statusCode)
}
}
private struct DefaultAPIClientDelegate: APIClientDelegate {}