@@ -6,9 +6,10 @@ import FoundationNetworking
6
6
/// A client to consume a REST API. Uses JSON to encode/decode body data.
7
7
@available ( iOS 16 , macOS 13 , tvOS 16 , watchOS 9 , * )
8
8
public final class RESTClient : Sendable {
9
- public enum RequestError : Error , LocalizedError , Sendable {
9
+ public enum RESTClientError : Error , LocalizedError , Sendable {
10
10
public typealias Context = String
11
11
12
+ case responsePluginFailed( Error , Context ? )
12
13
case failedToEncodeBody( Error , Context ? )
13
14
case failedToLoadData( Error , Context ? )
14
15
case failedToDecodeSuccessBody( Error , Context ? )
@@ -19,6 +20,8 @@ public final class RESTClient: Sendable {
19
20
20
21
public var errorDescription : String ? {
21
22
switch self {
23
+ case . responsePluginFailed( let error, _) :
24
+ " \( self . errorContext) Response plugin failed: \( error. localizedDescription) "
22
25
case . failedToEncodeBody( let error, _) :
23
26
" \( self . errorContext) Failed to encode body: \( error. localizedDescription) "
24
27
case . failedToLoadData( let error, _) :
@@ -38,8 +41,8 @@ public final class RESTClient: Sendable {
38
41
39
42
private var errorContext : String {
40
43
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) :
43
46
if let context {
44
47
return " [ \( context) : Client Error] "
45
48
} else {
@@ -98,11 +101,21 @@ public final class RESTClient: Sendable {
98
101
}
99
102
}
100
103
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
+
101
112
let baseURL : URL
102
113
let baseHeaders : [ String : String ]
103
114
let baseQueryItems : [ URLQueryItem ]
104
115
let jsonEncoder : JSONEncoder
105
116
let jsonDecoder : JSONDecoder
117
+ let requestPlugins : [ any RequestPlugin ]
118
+ let responsePlugins : [ any ResponsePlugin ]
106
119
let baseErrorContext : String ?
107
120
let errorBodyToMessage : @Sendable ( Data) throws -> String
108
121
@@ -113,6 +126,8 @@ public final class RESTClient: Sendable {
113
126
baseQueryItems: [ URLQueryItem ] = [ ] ,
114
127
jsonEncoder: JSONEncoder = . init( ) ,
115
128
jsonDecoder: JSONDecoder = . init( ) ,
129
+ requestPlugins: [ any RequestPlugin ] = [ ] ,
130
+ responsePlugins: [ any ResponsePlugin ] = [ ] ,
116
131
baseErrorContext: String ? = nil ,
117
132
errorBodyToMessage: @Sendable @escaping ( Data) throws -> String
118
133
) {
@@ -121,6 +136,8 @@ public final class RESTClient: Sendable {
121
136
self . baseQueryItems = baseQueryItems
122
137
self . jsonEncoder = jsonEncoder
123
138
self . jsonDecoder = jsonDecoder
139
+ self . requestPlugins = requestPlugins
140
+ self . responsePlugins = responsePlugins
124
141
self . baseErrorContext = baseErrorContext
125
142
self . errorBodyToMessage = errorBodyToMessage
126
143
}
@@ -132,7 +149,7 @@ public final class RESTClient: Sendable {
132
149
extraHeaders: [ String : String ] = [ : ] ,
133
150
extraQueryItems: [ URLQueryItem ] = [ ] ,
134
151
errorContext: String ? = nil
135
- ) async throws ( RequestError ) {
152
+ ) async throws ( RESTClientError ) {
136
153
_ = try await self . fetchData ( method: method, path: path, body: body, extraHeaders: extraHeaders, extraQueryItems: extraQueryItems)
137
154
}
138
155
@@ -143,7 +160,7 @@ public final class RESTClient: Sendable {
143
160
extraHeaders: [ String : String ] = [ : ] ,
144
161
extraQueryItems: [ URLQueryItem ] = [ ] ,
145
162
errorContext: String ? = nil
146
- ) async throws ( RequestError ) -> ResponseBodyType {
163
+ ) async throws ( RESTClientError ) -> ResponseBodyType {
147
164
let responseData = try await self . fetchData ( method: method, path: path, body: body, extraHeaders: extraHeaders, extraQueryItems: extraQueryItems)
148
165
149
166
do {
@@ -160,7 +177,7 @@ public final class RESTClient: Sendable {
160
177
extraHeaders: [ String : String ] = [ : ] ,
161
178
extraQueryItems: [ URLQueryItem ] = [ ] ,
162
179
errorContext: String ? = nil
163
- ) async throws ( RequestError ) -> Data {
180
+ ) async throws ( RESTClientError ) -> Data {
164
181
let url = self . baseURL
165
182
. appending ( path: path)
166
183
. appending ( queryItems: self . baseQueryItems + extraQueryItems)
@@ -176,14 +193,18 @@ public final class RESTClient: Sendable {
176
193
do {
177
194
request. httpBody = try body. httpData ( jsonEncoder: self . jsonEncoder)
178
195
} catch {
179
- throw RequestError . failedToEncodeBody ( error, self . errorContext ( requestContext: errorContext) )
196
+ throw RESTClientError . failedToEncodeBody ( error, self . errorContext ( requestContext: errorContext) )
180
197
}
181
198
182
199
request. setValue ( body. contentType, forHTTPHeaderField: " Content-Type " )
183
200
}
184
201
185
202
request. setValue ( " application/json " , forHTTPHeaderField: " Accept " )
186
203
204
+ for plugin in self . requestPlugins {
205
+ plugin. apply ( to: & request)
206
+ }
207
+
187
208
let ( data, response) = try await self . performRequest ( request, errorContext: errorContext)
188
209
return try await self . handle ( data: data, response: response, for: request, errorContext: errorContext)
189
210
}
@@ -194,26 +215,35 @@ public final class RESTClient: Sendable {
194
215
return context
195
216
}
196
217
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 ) {
198
219
self . logRequestIfDebug ( request)
199
220
200
221
let data : Data
201
222
let response : URLResponse
202
223
do {
203
224
( data, response) = try await URLSession . shared. data ( for: request)
204
225
} catch {
205
- throw RequestError . failedToLoadData ( error, self . errorContext ( requestContext: errorContext) )
226
+ throw RESTClientError . failedToLoadData ( error, self . errorContext ( requestContext: errorContext) )
206
227
}
207
228
208
229
self . logResponseIfDebug ( response, data: data)
209
230
return ( data, response)
210
231
}
211
232
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 {
214
235
throw . unexpectedResponseType( response, self . errorContext ( requestContext: errorContext) )
215
236
}
216
237
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
+
217
247
switch httpResponse. statusCode {
218
248
case 200 ..< 300 :
219
249
return data
0 commit comments