@@ -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 , * )
88public 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