Skip to content

Commit 2628cc0

Browse files
committed
Add plugin system to enhance requests and responses when needed
1 parent 04433a0 commit 2628cc0

File tree

1 file changed

+41
-11
lines changed

1 file changed

+41
-11
lines changed

Sources/HandySwift/Types/RESTClient.swift

+41-11
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import FoundationNetworking
66
/// A client to consume a REST API. Uses JSON to encode/decode body data.
77
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
88
public final class RESTClient: Sendable {
9-
public enum RequestError: Error, LocalizedError, Sendable {
9+
public enum RESTClientError: Error, LocalizedError, Sendable {
1010
public typealias Context = String
1111

12+
case responsePluginFailed(Error, Context?)
1213
case failedToEncodeBody(Error, Context?)
1314
case failedToLoadData(Error, Context?)
1415
case failedToDecodeSuccessBody(Error, Context?)
@@ -19,6 +20,8 @@ public final class RESTClient: Sendable {
1920

2021
public var errorDescription: String? {
2122
switch self {
23+
case .responsePluginFailed(let error, _):
24+
"\(self.errorContext) Response plugin failed: \(error.localizedDescription)"
2225
case .failedToEncodeBody(let error, _):
2326
"\(self.errorContext) Failed to encode body: \(error.localizedDescription)"
2427
case .failedToLoadData(let error, _):
@@ -38,8 +41,8 @@ public final class RESTClient: Sendable {
3841

3942
private var errorContext: String {
4043
switch self {
41-
case .failedToEncodeBody(_, let context), .failedToLoadData(_, let context), .failedToDecodeSuccessBody(_, let context),
42-
.failedToDecodeClientErrorBody(_, let context), .clientError(_, let context):
44+
case .responsePluginFailed(_, let context), .failedToEncodeBody(_, let context), .failedToLoadData(_, let context),
45+
.failedToDecodeSuccessBody(_, let context), .failedToDecodeClientErrorBody(_, let context), .clientError(_, let context):
4346
if let context {
4447
return "[\(context): Client Error]"
4548
} else {
@@ -98,11 +101,21 @@ public final class RESTClient: Sendable {
98101
}
99102
}
100103

104+
public protocol RequestPlugin: Sendable {
105+
func apply(to request: inout URLRequest)
106+
}
107+
108+
public protocol ResponsePlugin: Sendable {
109+
func apply(to response: inout HTTPURLResponse, data: inout Data) throws
110+
}
111+
101112
let baseURL: URL
102113
let baseHeaders: [String: String]
103114
let baseQueryItems: [URLQueryItem]
104115
let jsonEncoder: JSONEncoder
105116
let jsonDecoder: JSONDecoder
117+
let requestPlugins: [any RequestPlugin]
118+
let responsePlugins: [any ResponsePlugin]
106119
let baseErrorContext: String?
107120
let errorBodyToMessage: @Sendable (Data) throws -> String
108121

@@ -113,6 +126,8 @@ public final class RESTClient: Sendable {
113126
baseQueryItems: [URLQueryItem] = [],
114127
jsonEncoder: JSONEncoder = .init(),
115128
jsonDecoder: JSONDecoder = .init(),
129+
requestPlugins: [any RequestPlugin] = [],
130+
responsePlugins: [any ResponsePlugin] = [],
116131
baseErrorContext: String? = nil,
117132
errorBodyToMessage: @Sendable @escaping (Data) throws -> String
118133
) {
@@ -121,6 +136,8 @@ public final class RESTClient: Sendable {
121136
self.baseQueryItems = baseQueryItems
122137
self.jsonEncoder = jsonEncoder
123138
self.jsonDecoder = jsonDecoder
139+
self.requestPlugins = requestPlugins
140+
self.responsePlugins = responsePlugins
124141
self.baseErrorContext = baseErrorContext
125142
self.errorBodyToMessage = errorBodyToMessage
126143
}
@@ -132,7 +149,7 @@ public final class RESTClient: Sendable {
132149
extraHeaders: [String: String] = [:],
133150
extraQueryItems: [URLQueryItem] = [],
134151
errorContext: String? = nil
135-
) async throws(RequestError) {
152+
) async throws(RESTClientError) {
136153
_ = try await self.fetchData(method: method, path: path, body: body, extraHeaders: extraHeaders, extraQueryItems: extraQueryItems)
137154
}
138155

@@ -143,7 +160,7 @@ public final class RESTClient: Sendable {
143160
extraHeaders: [String: String] = [:],
144161
extraQueryItems: [URLQueryItem] = [],
145162
errorContext: String? = nil
146-
) async throws(RequestError) -> ResponseBodyType {
163+
) async throws(RESTClientError) -> ResponseBodyType {
147164
let responseData = try await self.fetchData(method: method, path: path, body: body, extraHeaders: extraHeaders, extraQueryItems: extraQueryItems)
148165

149166
do {
@@ -160,7 +177,7 @@ public final class RESTClient: Sendable {
160177
extraHeaders: [String: String] = [:],
161178
extraQueryItems: [URLQueryItem] = [],
162179
errorContext: String? = nil
163-
) async throws(RequestError) -> Data {
180+
) async throws(RESTClientError) -> Data {
164181
let url = self.baseURL
165182
.appending(path: path)
166183
.appending(queryItems: self.baseQueryItems + extraQueryItems)
@@ -176,14 +193,18 @@ public final class RESTClient: Sendable {
176193
do {
177194
request.httpBody = try body.httpData(jsonEncoder: self.jsonEncoder)
178195
} catch {
179-
throw RequestError.failedToEncodeBody(error, self.errorContext(requestContext: errorContext))
196+
throw RESTClientError.failedToEncodeBody(error, self.errorContext(requestContext: errorContext))
180197
}
181198

182199
request.setValue(body.contentType, forHTTPHeaderField: "Content-Type")
183200
}
184201

185202
request.setValue("application/json", forHTTPHeaderField: "Accept")
186203

204+
for plugin in self.requestPlugins {
205+
plugin.apply(to: &request)
206+
}
207+
187208
let (data, response) = try await self.performRequest(request, errorContext: errorContext)
188209
return try await self.handle(data: data, response: response, for: request, errorContext: errorContext)
189210
}
@@ -194,26 +215,35 @@ public final class RESTClient: Sendable {
194215
return context
195216
}
196217

197-
private func performRequest(_ request: URLRequest, errorContext: String?) async throws(RequestError) -> (Data, URLResponse) {
218+
private func performRequest(_ request: URLRequest, errorContext: String?) async throws(RESTClientError) -> (Data, URLResponse) {
198219
self.logRequestIfDebug(request)
199220

200221
let data: Data
201222
let response: URLResponse
202223
do {
203224
(data, response) = try await URLSession.shared.data(for: request)
204225
} catch {
205-
throw RequestError.failedToLoadData(error, self.errorContext(requestContext: errorContext))
226+
throw RESTClientError.failedToLoadData(error, self.errorContext(requestContext: errorContext))
206227
}
207228

208229
self.logResponseIfDebug(response, data: data)
209230
return (data, response)
210231
}
211232

212-
private func handle(data: Data, response: URLResponse, for request: URLRequest, errorContext: String?, attempt: Int = 1) async throws(RequestError) -> Data {
213-
guard let httpResponse = response as? HTTPURLResponse else {
233+
private func handle(data: Data, response: URLResponse, for request: URLRequest, errorContext: String?, attempt: Int = 1) async throws(RESTClientError) -> Data {
234+
guard var httpResponse = response as? HTTPURLResponse else {
214235
throw .unexpectedResponseType(response, self.errorContext(requestContext: errorContext))
215236
}
216237

238+
var data = data
239+
for responsePlugin in self.responsePlugins {
240+
do {
241+
try responsePlugin.apply(to: &httpResponse, data: &data)
242+
} catch {
243+
throw RESTClientError.responsePluginFailed(error, self.errorContext(requestContext: errorContext))
244+
}
245+
}
246+
217247
switch httpResponse.statusCode {
218248
case 200..<300:
219249
return data

0 commit comments

Comments
 (0)