[swift6] urlsession interceptor (#19797)

* [swift6] alamofire interceptor

* [swift6] alamofire interceptor

* [swift6] urlsession interceptor

* [swift6] urlsession interceptor

* [swift6] urlsession interceptor
This commit is contained in:
Bruno Coelho 2024-10-07 12:16:18 +01:00 committed by GitHub
parent 4c81563708
commit 9a0fc5900f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 891 additions and 347 deletions

View File

@ -30,8 +30,12 @@ import Alamofire{{/useAlamofire}}
/// Configures the range of HTTP status codes that will result in a successful response
///
/// If a HTTP status code is outside of this range the response will be interpreted as failed.
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var successfulStatusCodeRange: Range<Int>{{#useAlamofire}}
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var successfulStatusCodeRange: Range<Int>{{#useURLSession}}
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var interceptor: OpenAPIInterceptor{{/useURLSession}}{{#useAlamofire}}
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var interceptor: RequestInterceptor?
/// ResponseSerializer that will be used by the generator for `Data` responses
///
/// If unchanged, Alamofires default `DataResponseSerializer` will be used.
@ -52,7 +56,8 @@ import Alamofire{{/useAlamofire}}
requestBuilderFactory: RequestBuilderFactory = {{#useAlamofire}}AlamofireRequestBuilderFactory(){{/useAlamofire}}{{#useURLSession}}URLSessionRequestBuilderFactory(){{/useURLSession}},
apiResponseQueue: DispatchQueue = .main,
codableHelper: CodableHelper = CodableHelper(),
successfulStatusCodeRange: Range<Int> = 200..<300{{#useAlamofire}},
successfulStatusCodeRange: Range<Int> = 200..<300{{#useURLSession}},
interceptor: OpenAPIInterceptor = DefaultOpenAPIInterceptor(){{/useURLSession}}{{#useAlamofire}},
interceptor: RequestInterceptor? = nil,
dataResponseSerializer: AnyResponseSerializer<Data> = AnyResponseSerializer(DataResponseSerializer()),
stringResponseSerializer: AnyResponseSerializer<String> = AnyResponseSerializer(StringResponseSerializer()){{/useAlamofire}}{{/useVapor}}
@ -67,7 +72,8 @@ import Alamofire{{/useAlamofire}}
self.requestBuilderFactory = requestBuilderFactory
self.apiResponseQueue = apiResponseQueue
self.codableHelper = codableHelper
self.successfulStatusCodeRange = successfulStatusCodeRange{{#useAlamofire}}
self.successfulStatusCodeRange = successfulStatusCodeRange{{#useURLSession}}
self.interceptor = interceptor{{/useURLSession}}{{#useAlamofire}}
self.interceptor = interceptor
self.dataResponseSerializer = dataResponseSerializer
self.stringResponseSerializer = stringResponseSerializer{{/useAlamofire}}{{/useVapor}}

View File

@ -25,7 +25,7 @@ import UniformTypeIdentifiers
}
// Protocol allowing implementations to alter what is returned or to test their implementations.
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} protocol URLSessionProtocol {
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} protocol URLSessionProtocol: Sendable {
// Task which performs the network fetch. Expected to be from URLSession.dataTask(with:completionHandler:) such that a network request
// is sent off when `.resume()` is called.
func dataTaskFromProtocol(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTaskProtocol
@ -160,21 +160,46 @@ fileprivate class URLSessionRequestBuilderConfiguration: @unchecked Sendable {
do {
let request = try createURLRequest(urlSession: urlSession, method: xMethod, encoding: encoding, headers: headers)
let dataTask = urlSession.dataTaskFromProtocol(with: request) { data, response, error in
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
openAPIClient.interceptor.intercept(urlRequest: request, urlSession: urlSession, openAPIClient: openAPIClient) { result in
switch result {
case .success(let modifiedRequest):
let dataTask = urlSession.dataTaskFromProtocol(with: modifiedRequest) { data, response, error in
self.cleanupRequest()
if let response, let error {
self.openAPIClient.interceptor.retry(urlRequest: modifiedRequest, urlSession: urlSession, openAPIClient: self.openAPIClient, data: data, response: response, error: error) { retry in
switch retry {
case .retry:
self.execute(completion: completion)
case .dontRetry:
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
} else {
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
self.onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = self.taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = self.credential
self.requestTask.set(task: dataTask)
dataTask.resume()
case .failure(let error):
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
}
}
}
onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = credential
requestTask.set(task: dataTask)
dataTask.resume()
} catch {
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
@ -676,3 +701,26 @@ private extension Optional where Wrapped == Data {
}
extension JSONDataEncoding: ParameterEncoding {}
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} enum OpenAPIInterceptorRetry {
case retry
case dontRetry
}
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} protocol OpenAPIInterceptor {
func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, Error>) -> Void)
func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void)
}
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} class DefaultOpenAPIInterceptor: OpenAPIInterceptor {
public init() {}
public func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
completion(.success(urlRequest))
}
public func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void) {
completion(.dontRetry)
}
}

View File

@ -22,7 +22,9 @@ open class OpenAPIClient: @unchecked Sendable {
///
/// If a HTTP status code is outside of this range the response will be interpreted as failed.
public var successfulStatusCodeRange: Range<Int>
public var interceptor: RequestInterceptor?
/// ResponseSerializer that will be used by the generator for `Data` responses
///
/// If unchanged, Alamofires default `DataResponseSerializer` will be used.

View File

@ -22,7 +22,9 @@ open class OpenAPIClient: @unchecked Sendable {
///
/// If a HTTP status code is outside of this range the response will be interpreted as failed.
public var successfulStatusCodeRange: Range<Int>
public var interceptor: RequestInterceptor?
/// ResponseSerializer that will be used by the generator for `Data` responses
///
/// If unchanged, Alamofires default `DataResponseSerializer` will be used.

View File

@ -22,6 +22,8 @@ open class OpenAPIClient: @unchecked Sendable {
/// If a HTTP status code is outside of this range the response will be interpreted as failed.
public var successfulStatusCodeRange: Range<Int>
public var interceptor: OpenAPIInterceptor
public init(
basePath: String = "http://petstore.swagger.io:80/v2",
customHeaders: [String: String] = [:],
@ -29,7 +31,8 @@ open class OpenAPIClient: @unchecked Sendable {
requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(),
apiResponseQueue: DispatchQueue = .main,
codableHelper: CodableHelper = CodableHelper(),
successfulStatusCodeRange: Range<Int> = 200..<300
successfulStatusCodeRange: Range<Int> = 200..<300,
interceptor: OpenAPIInterceptor = DefaultOpenAPIInterceptor()
) {
self.basePath = basePath
self.customHeaders = customHeaders
@ -38,6 +41,7 @@ open class OpenAPIClient: @unchecked Sendable {
self.apiResponseQueue = apiResponseQueue
self.codableHelper = codableHelper
self.successfulStatusCodeRange = successfulStatusCodeRange
self.interceptor = interceptor
}
public static let shared = OpenAPIClient()

View File

@ -25,7 +25,7 @@ public protocol URLSessionDataTaskProtocol {
}
// Protocol allowing implementations to alter what is returned or to test their implementations.
public protocol URLSessionProtocol {
public protocol URLSessionProtocol: Sendable {
// Task which performs the network fetch. Expected to be from URLSession.dataTask(with:completionHandler:) such that a network request
// is sent off when `.resume()` is called.
func dataTaskFromProtocol(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTaskProtocol
@ -160,21 +160,46 @@ open class URLSessionRequestBuilder<T>: RequestBuilder<T>, @unchecked Sendable {
do {
let request = try createURLRequest(urlSession: urlSession, method: xMethod, encoding: encoding, headers: headers)
let dataTask = urlSession.dataTaskFromProtocol(with: request) { data, response, error in
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
openAPIClient.interceptor.intercept(urlRequest: request, urlSession: urlSession, openAPIClient: openAPIClient) { result in
switch result {
case .success(let modifiedRequest):
let dataTask = urlSession.dataTaskFromProtocol(with: modifiedRequest) { data, response, error in
self.cleanupRequest()
if let response, let error {
self.openAPIClient.interceptor.retry(urlRequest: modifiedRequest, urlSession: urlSession, openAPIClient: self.openAPIClient, data: data, response: response, error: error) { retry in
switch retry {
case .retry:
self.execute(completion: completion)
case .dontRetry:
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
} else {
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
self.onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = self.taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = self.credential
self.requestTask.set(task: dataTask)
dataTask.resume()
case .failure(let error):
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
}
}
}
onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = credential
requestTask.set(task: dataTask)
dataTask.resume()
} catch {
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
@ -676,3 +701,26 @@ private extension Optional where Wrapped == Data {
}
extension JSONDataEncoding: ParameterEncoding {}
public enum OpenAPIInterceptorRetry {
case retry
case dontRetry
}
public protocol OpenAPIInterceptor {
func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, Error>) -> Void)
func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void)
}
public class DefaultOpenAPIInterceptor: OpenAPIInterceptor {
public init() {}
public func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
completion(.success(urlRequest))
}
public func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void) {
completion(.dontRetry)
}
}

View File

@ -22,6 +22,8 @@ open class OpenAPIClient: @unchecked Sendable {
/// If a HTTP status code is outside of this range the response will be interpreted as failed.
public var successfulStatusCodeRange: Range<Int>
public var interceptor: OpenAPIInterceptor
public init(
basePath: String = "http://petstore.swagger.io:80/v2",
customHeaders: [String: String] = [:],
@ -29,7 +31,8 @@ open class OpenAPIClient: @unchecked Sendable {
requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(),
apiResponseQueue: DispatchQueue = .main,
codableHelper: CodableHelper = CodableHelper(),
successfulStatusCodeRange: Range<Int> = 200..<300
successfulStatusCodeRange: Range<Int> = 200..<300,
interceptor: OpenAPIInterceptor = DefaultOpenAPIInterceptor()
) {
self.basePath = basePath
self.customHeaders = customHeaders
@ -38,6 +41,7 @@ open class OpenAPIClient: @unchecked Sendable {
self.apiResponseQueue = apiResponseQueue
self.codableHelper = codableHelper
self.successfulStatusCodeRange = successfulStatusCodeRange
self.interceptor = interceptor
}
public static let shared = OpenAPIClient()

View File

@ -25,7 +25,7 @@ public protocol URLSessionDataTaskProtocol {
}
// Protocol allowing implementations to alter what is returned or to test their implementations.
public protocol URLSessionProtocol {
public protocol URLSessionProtocol: Sendable {
// Task which performs the network fetch. Expected to be from URLSession.dataTask(with:completionHandler:) such that a network request
// is sent off when `.resume()` is called.
func dataTaskFromProtocol(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTaskProtocol
@ -160,21 +160,46 @@ open class URLSessionRequestBuilder<T>: RequestBuilder<T>, @unchecked Sendable {
do {
let request = try createURLRequest(urlSession: urlSession, method: xMethod, encoding: encoding, headers: headers)
let dataTask = urlSession.dataTaskFromProtocol(with: request) { data, response, error in
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
openAPIClient.interceptor.intercept(urlRequest: request, urlSession: urlSession, openAPIClient: openAPIClient) { result in
switch result {
case .success(let modifiedRequest):
let dataTask = urlSession.dataTaskFromProtocol(with: modifiedRequest) { data, response, error in
self.cleanupRequest()
if let response, let error {
self.openAPIClient.interceptor.retry(urlRequest: modifiedRequest, urlSession: urlSession, openAPIClient: self.openAPIClient, data: data, response: response, error: error) { retry in
switch retry {
case .retry:
self.execute(completion: completion)
case .dontRetry:
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
} else {
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
self.onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = self.taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = self.credential
self.requestTask.set(task: dataTask)
dataTask.resume()
case .failure(let error):
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
}
}
}
onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = credential
requestTask.set(task: dataTask)
dataTask.resume()
} catch {
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
@ -676,3 +701,26 @@ private extension Optional where Wrapped == Data {
}
extension JSONDataEncoding: ParameterEncoding {}
public enum OpenAPIInterceptorRetry {
case retry
case dontRetry
}
public protocol OpenAPIInterceptor {
func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, Error>) -> Void)
func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void)
}
public class DefaultOpenAPIInterceptor: OpenAPIInterceptor {
public init() {}
public func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
completion(.success(urlRequest))
}
public func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void) {
completion(.dontRetry)
}
}

View File

@ -22,6 +22,8 @@ open class OpenAPIClient: @unchecked Sendable {
/// If a HTTP status code is outside of this range the response will be interpreted as failed.
public var successfulStatusCodeRange: Range<Int>
public var interceptor: OpenAPIInterceptor
public init(
basePath: String = "http://petstore.swagger.io:80/v2",
customHeaders: [String: String] = [:],
@ -29,7 +31,8 @@ open class OpenAPIClient: @unchecked Sendable {
requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(),
apiResponseQueue: DispatchQueue = .main,
codableHelper: CodableHelper = CodableHelper(),
successfulStatusCodeRange: Range<Int> = 200..<300
successfulStatusCodeRange: Range<Int> = 200..<300,
interceptor: OpenAPIInterceptor = DefaultOpenAPIInterceptor()
) {
self.basePath = basePath
self.customHeaders = customHeaders
@ -38,6 +41,7 @@ open class OpenAPIClient: @unchecked Sendable {
self.apiResponseQueue = apiResponseQueue
self.codableHelper = codableHelper
self.successfulStatusCodeRange = successfulStatusCodeRange
self.interceptor = interceptor
}
public static let shared = OpenAPIClient()

View File

@ -25,7 +25,7 @@ public protocol URLSessionDataTaskProtocol {
}
// Protocol allowing implementations to alter what is returned or to test their implementations.
public protocol URLSessionProtocol {
public protocol URLSessionProtocol: Sendable {
// Task which performs the network fetch. Expected to be from URLSession.dataTask(with:completionHandler:) such that a network request
// is sent off when `.resume()` is called.
func dataTaskFromProtocol(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTaskProtocol
@ -160,21 +160,46 @@ open class URLSessionRequestBuilder<T>: RequestBuilder<T>, @unchecked Sendable {
do {
let request = try createURLRequest(urlSession: urlSession, method: xMethod, encoding: encoding, headers: headers)
let dataTask = urlSession.dataTaskFromProtocol(with: request) { data, response, error in
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
openAPIClient.interceptor.intercept(urlRequest: request, urlSession: urlSession, openAPIClient: openAPIClient) { result in
switch result {
case .success(let modifiedRequest):
let dataTask = urlSession.dataTaskFromProtocol(with: modifiedRequest) { data, response, error in
self.cleanupRequest()
if let response, let error {
self.openAPIClient.interceptor.retry(urlRequest: modifiedRequest, urlSession: urlSession, openAPIClient: self.openAPIClient, data: data, response: response, error: error) { retry in
switch retry {
case .retry:
self.execute(completion: completion)
case .dontRetry:
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
} else {
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
self.onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = self.taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = self.credential
self.requestTask.set(task: dataTask)
dataTask.resume()
case .failure(let error):
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
}
}
}
onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = credential
requestTask.set(task: dataTask)
dataTask.resume()
} catch {
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
@ -676,3 +701,26 @@ private extension Optional where Wrapped == Data {
}
extension JSONDataEncoding: ParameterEncoding {}
public enum OpenAPIInterceptorRetry {
case retry
case dontRetry
}
public protocol OpenAPIInterceptor {
func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, Error>) -> Void)
func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void)
}
public class DefaultOpenAPIInterceptor: OpenAPIInterceptor {
public init() {}
public func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
completion(.success(urlRequest))
}
public func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void) {
completion(.dontRetry)
}
}

View File

@ -22,6 +22,8 @@ open class OpenAPIClient: @unchecked Sendable {
/// If a HTTP status code is outside of this range the response will be interpreted as failed.
public var successfulStatusCodeRange: Range<Int>
public var interceptor: OpenAPIInterceptor
public init(
basePath: String = "http://petstore.swagger.io:80/v2",
customHeaders: [String: String] = [:],
@ -29,7 +31,8 @@ open class OpenAPIClient: @unchecked Sendable {
requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(),
apiResponseQueue: DispatchQueue = .main,
codableHelper: CodableHelper = CodableHelper(),
successfulStatusCodeRange: Range<Int> = 200..<300
successfulStatusCodeRange: Range<Int> = 200..<300,
interceptor: OpenAPIInterceptor = DefaultOpenAPIInterceptor()
) {
self.basePath = basePath
self.customHeaders = customHeaders
@ -38,6 +41,7 @@ open class OpenAPIClient: @unchecked Sendable {
self.apiResponseQueue = apiResponseQueue
self.codableHelper = codableHelper
self.successfulStatusCodeRange = successfulStatusCodeRange
self.interceptor = interceptor
}
public static let shared = OpenAPIClient()

View File

@ -25,7 +25,7 @@ public protocol URLSessionDataTaskProtocol {
}
// Protocol allowing implementations to alter what is returned or to test their implementations.
public protocol URLSessionProtocol {
public protocol URLSessionProtocol: Sendable {
// Task which performs the network fetch. Expected to be from URLSession.dataTask(with:completionHandler:) such that a network request
// is sent off when `.resume()` is called.
func dataTaskFromProtocol(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTaskProtocol
@ -160,21 +160,46 @@ open class URLSessionRequestBuilder<T>: RequestBuilder<T>, @unchecked Sendable {
do {
let request = try createURLRequest(urlSession: urlSession, method: xMethod, encoding: encoding, headers: headers)
let dataTask = urlSession.dataTaskFromProtocol(with: request) { data, response, error in
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
openAPIClient.interceptor.intercept(urlRequest: request, urlSession: urlSession, openAPIClient: openAPIClient) { result in
switch result {
case .success(let modifiedRequest):
let dataTask = urlSession.dataTaskFromProtocol(with: modifiedRequest) { data, response, error in
self.cleanupRequest()
if let response, let error {
self.openAPIClient.interceptor.retry(urlRequest: modifiedRequest, urlSession: urlSession, openAPIClient: self.openAPIClient, data: data, response: response, error: error) { retry in
switch retry {
case .retry:
self.execute(completion: completion)
case .dontRetry:
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
} else {
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
self.onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = self.taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = self.credential
self.requestTask.set(task: dataTask)
dataTask.resume()
case .failure(let error):
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
}
}
}
onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = credential
requestTask.set(task: dataTask)
dataTask.resume()
} catch {
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
@ -676,3 +701,26 @@ private extension Optional where Wrapped == Data {
}
extension JSONDataEncoding: ParameterEncoding {}
public enum OpenAPIInterceptorRetry {
case retry
case dontRetry
}
public protocol OpenAPIInterceptor {
func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, Error>) -> Void)
func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void)
}
public class DefaultOpenAPIInterceptor: OpenAPIInterceptor {
public init() {}
public func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
completion(.success(urlRequest))
}
public func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void) {
completion(.dontRetry)
}
}

View File

@ -22,6 +22,8 @@ open class OpenAPIClient: @unchecked Sendable {
/// If a HTTP status code is outside of this range the response will be interpreted as failed.
public var successfulStatusCodeRange: Range<Int>
public var interceptor: OpenAPIInterceptor
public init(
basePath: String = "http://petstore.swagger.io:80/v2",
customHeaders: [String: String] = [:],
@ -29,7 +31,8 @@ open class OpenAPIClient: @unchecked Sendable {
requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(),
apiResponseQueue: DispatchQueue = .main,
codableHelper: CodableHelper = CodableHelper(),
successfulStatusCodeRange: Range<Int> = 200..<300
successfulStatusCodeRange: Range<Int> = 200..<300,
interceptor: OpenAPIInterceptor = DefaultOpenAPIInterceptor()
) {
self.basePath = basePath
self.customHeaders = customHeaders
@ -38,6 +41,7 @@ open class OpenAPIClient: @unchecked Sendable {
self.apiResponseQueue = apiResponseQueue
self.codableHelper = codableHelper
self.successfulStatusCodeRange = successfulStatusCodeRange
self.interceptor = interceptor
}
public static let shared = OpenAPIClient()

View File

@ -25,7 +25,7 @@ public protocol URLSessionDataTaskProtocol {
}
// Protocol allowing implementations to alter what is returned or to test their implementations.
public protocol URLSessionProtocol {
public protocol URLSessionProtocol: Sendable {
// Task which performs the network fetch. Expected to be from URLSession.dataTask(with:completionHandler:) such that a network request
// is sent off when `.resume()` is called.
func dataTaskFromProtocol(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTaskProtocol
@ -160,21 +160,46 @@ open class URLSessionRequestBuilder<T>: RequestBuilder<T>, @unchecked Sendable {
do {
let request = try createURLRequest(urlSession: urlSession, method: xMethod, encoding: encoding, headers: headers)
let dataTask = urlSession.dataTaskFromProtocol(with: request) { data, response, error in
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
openAPIClient.interceptor.intercept(urlRequest: request, urlSession: urlSession, openAPIClient: openAPIClient) { result in
switch result {
case .success(let modifiedRequest):
let dataTask = urlSession.dataTaskFromProtocol(with: modifiedRequest) { data, response, error in
self.cleanupRequest()
if let response, let error {
self.openAPIClient.interceptor.retry(urlRequest: modifiedRequest, urlSession: urlSession, openAPIClient: self.openAPIClient, data: data, response: response, error: error) { retry in
switch retry {
case .retry:
self.execute(completion: completion)
case .dontRetry:
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
} else {
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
self.onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = self.taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = self.credential
self.requestTask.set(task: dataTask)
dataTask.resume()
case .failure(let error):
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
}
}
}
onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = credential
requestTask.set(task: dataTask)
dataTask.resume()
} catch {
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
@ -676,3 +701,26 @@ private extension Optional where Wrapped == Data {
}
extension JSONDataEncoding: ParameterEncoding {}
public enum OpenAPIInterceptorRetry {
case retry
case dontRetry
}
public protocol OpenAPIInterceptor {
func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, Error>) -> Void)
func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void)
}
public class DefaultOpenAPIInterceptor: OpenAPIInterceptor {
public init() {}
public func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
completion(.success(urlRequest))
}
public func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void) {
completion(.dontRetry)
}
}

View File

@ -22,6 +22,8 @@ open class OpenAPIClient: @unchecked Sendable {
/// If a HTTP status code is outside of this range the response will be interpreted as failed.
public var successfulStatusCodeRange: Range<Int>
public var interceptor: OpenAPIInterceptor
public init(
basePath: String = "http://localhost",
customHeaders: [String: String] = [:],
@ -29,7 +31,8 @@ open class OpenAPIClient: @unchecked Sendable {
requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(),
apiResponseQueue: DispatchQueue = .main,
codableHelper: CodableHelper = CodableHelper(),
successfulStatusCodeRange: Range<Int> = 200..<300
successfulStatusCodeRange: Range<Int> = 200..<300,
interceptor: OpenAPIInterceptor = DefaultOpenAPIInterceptor()
) {
self.basePath = basePath
self.customHeaders = customHeaders
@ -38,6 +41,7 @@ open class OpenAPIClient: @unchecked Sendable {
self.apiResponseQueue = apiResponseQueue
self.codableHelper = codableHelper
self.successfulStatusCodeRange = successfulStatusCodeRange
self.interceptor = interceptor
}
public static let shared = OpenAPIClient()

View File

@ -25,7 +25,7 @@ public protocol URLSessionDataTaskProtocol {
}
// Protocol allowing implementations to alter what is returned or to test their implementations.
public protocol URLSessionProtocol {
public protocol URLSessionProtocol: Sendable {
// Task which performs the network fetch. Expected to be from URLSession.dataTask(with:completionHandler:) such that a network request
// is sent off when `.resume()` is called.
func dataTaskFromProtocol(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTaskProtocol
@ -160,21 +160,46 @@ open class URLSessionRequestBuilder<T>: RequestBuilder<T>, @unchecked Sendable {
do {
let request = try createURLRequest(urlSession: urlSession, method: xMethod, encoding: encoding, headers: headers)
let dataTask = urlSession.dataTaskFromProtocol(with: request) { data, response, error in
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
openAPIClient.interceptor.intercept(urlRequest: request, urlSession: urlSession, openAPIClient: openAPIClient) { result in
switch result {
case .success(let modifiedRequest):
let dataTask = urlSession.dataTaskFromProtocol(with: modifiedRequest) { data, response, error in
self.cleanupRequest()
if let response, let error {
self.openAPIClient.interceptor.retry(urlRequest: modifiedRequest, urlSession: urlSession, openAPIClient: self.openAPIClient, data: data, response: response, error: error) { retry in
switch retry {
case .retry:
self.execute(completion: completion)
case .dontRetry:
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
} else {
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
self.onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = self.taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = self.credential
self.requestTask.set(task: dataTask)
dataTask.resume()
case .failure(let error):
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
}
}
}
onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = credential
requestTask.set(task: dataTask)
dataTask.resume()
} catch {
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
@ -676,3 +701,26 @@ private extension Optional where Wrapped == Data {
}
extension JSONDataEncoding: ParameterEncoding {}
public enum OpenAPIInterceptorRetry {
case retry
case dontRetry
}
public protocol OpenAPIInterceptor {
func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, Error>) -> Void)
func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void)
}
public class DefaultOpenAPIInterceptor: OpenAPIInterceptor {
public init() {}
public func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
completion(.success(urlRequest))
}
public func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void) {
completion(.dontRetry)
}
}

View File

@ -22,6 +22,8 @@ open class OpenAPIClient: @unchecked Sendable {
/// If a HTTP status code is outside of this range the response will be interpreted as failed.
public var successfulStatusCodeRange: Range<Int>
public var interceptor: OpenAPIInterceptor
public init(
basePath: String = "http://petstore.swagger.io:80/v2",
customHeaders: [String: String] = [:],
@ -29,7 +31,8 @@ open class OpenAPIClient: @unchecked Sendable {
requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(),
apiResponseQueue: DispatchQueue = .main,
codableHelper: CodableHelper = CodableHelper(),
successfulStatusCodeRange: Range<Int> = 200..<300
successfulStatusCodeRange: Range<Int> = 200..<300,
interceptor: OpenAPIInterceptor = DefaultOpenAPIInterceptor()
) {
self.basePath = basePath
self.customHeaders = customHeaders
@ -38,6 +41,7 @@ open class OpenAPIClient: @unchecked Sendable {
self.apiResponseQueue = apiResponseQueue
self.codableHelper = codableHelper
self.successfulStatusCodeRange = successfulStatusCodeRange
self.interceptor = interceptor
}
public static let shared = OpenAPIClient()

View File

@ -25,7 +25,7 @@ public protocol URLSessionDataTaskProtocol {
}
// Protocol allowing implementations to alter what is returned or to test their implementations.
public protocol URLSessionProtocol {
public protocol URLSessionProtocol: Sendable {
// Task which performs the network fetch. Expected to be from URLSession.dataTask(with:completionHandler:) such that a network request
// is sent off when `.resume()` is called.
func dataTaskFromProtocol(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTaskProtocol
@ -160,21 +160,46 @@ open class URLSessionRequestBuilder<T>: RequestBuilder<T>, @unchecked Sendable {
do {
let request = try createURLRequest(urlSession: urlSession, method: xMethod, encoding: encoding, headers: headers)
let dataTask = urlSession.dataTaskFromProtocol(with: request) { data, response, error in
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
openAPIClient.interceptor.intercept(urlRequest: request, urlSession: urlSession, openAPIClient: openAPIClient) { result in
switch result {
case .success(let modifiedRequest):
let dataTask = urlSession.dataTaskFromProtocol(with: modifiedRequest) { data, response, error in
self.cleanupRequest()
if let response, let error {
self.openAPIClient.interceptor.retry(urlRequest: modifiedRequest, urlSession: urlSession, openAPIClient: self.openAPIClient, data: data, response: response, error: error) { retry in
switch retry {
case .retry:
self.execute(completion: completion)
case .dontRetry:
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
} else {
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
self.onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = self.taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = self.credential
self.requestTask.set(task: dataTask)
dataTask.resume()
case .failure(let error):
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
}
}
}
onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = credential
requestTask.set(task: dataTask)
dataTask.resume()
} catch {
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
@ -676,3 +701,26 @@ private extension Optional where Wrapped == Data {
}
extension JSONDataEncoding: ParameterEncoding {}
public enum OpenAPIInterceptorRetry {
case retry
case dontRetry
}
public protocol OpenAPIInterceptor {
func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, Error>) -> Void)
func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void)
}
public class DefaultOpenAPIInterceptor: OpenAPIInterceptor {
public init() {}
public func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
completion(.success(urlRequest))
}
public func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void) {
completion(.dontRetry)
}
}

View File

@ -22,6 +22,8 @@ internal class OpenAPIClient: @unchecked Sendable {
/// If a HTTP status code is outside of this range the response will be interpreted as failed.
internal var successfulStatusCodeRange: Range<Int>
internal var interceptor: OpenAPIInterceptor
internal init(
basePath: String = "http://petstore.swagger.io:80/v2",
customHeaders: [String: String] = [:],
@ -29,7 +31,8 @@ internal class OpenAPIClient: @unchecked Sendable {
requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(),
apiResponseQueue: DispatchQueue = .main,
codableHelper: CodableHelper = CodableHelper(),
successfulStatusCodeRange: Range<Int> = 200..<300
successfulStatusCodeRange: Range<Int> = 200..<300,
interceptor: OpenAPIInterceptor = DefaultOpenAPIInterceptor()
) {
self.basePath = basePath
self.customHeaders = customHeaders
@ -38,6 +41,7 @@ internal class OpenAPIClient: @unchecked Sendable {
self.apiResponseQueue = apiResponseQueue
self.codableHelper = codableHelper
self.successfulStatusCodeRange = successfulStatusCodeRange
self.interceptor = interceptor
}
internal static let shared = OpenAPIClient()

View File

@ -25,7 +25,7 @@ internal protocol URLSessionDataTaskProtocol {
}
// Protocol allowing implementations to alter what is returned or to test their implementations.
internal protocol URLSessionProtocol {
internal protocol URLSessionProtocol: Sendable {
// Task which performs the network fetch. Expected to be from URLSession.dataTask(with:completionHandler:) such that a network request
// is sent off when `.resume()` is called.
func dataTaskFromProtocol(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTaskProtocol
@ -160,21 +160,46 @@ internal class URLSessionRequestBuilder<T>: RequestBuilder<T>, @unchecked Sendab
do {
let request = try createURLRequest(urlSession: urlSession, method: xMethod, encoding: encoding, headers: headers)
let dataTask = urlSession.dataTaskFromProtocol(with: request) { data, response, error in
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
openAPIClient.interceptor.intercept(urlRequest: request, urlSession: urlSession, openAPIClient: openAPIClient) { result in
switch result {
case .success(let modifiedRequest):
let dataTask = urlSession.dataTaskFromProtocol(with: modifiedRequest) { data, response, error in
self.cleanupRequest()
if let response, let error {
self.openAPIClient.interceptor.retry(urlRequest: modifiedRequest, urlSession: urlSession, openAPIClient: self.openAPIClient, data: data, response: response, error: error) { retry in
switch retry {
case .retry:
self.execute(completion: completion)
case .dontRetry:
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
} else {
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
self.onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = self.taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = self.credential
self.requestTask.set(task: dataTask)
dataTask.resume()
case .failure(let error):
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
}
}
}
onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = credential
requestTask.set(task: dataTask)
dataTask.resume()
} catch {
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
@ -676,3 +701,26 @@ private extension Optional where Wrapped == Data {
}
extension JSONDataEncoding: ParameterEncoding {}
internal enum OpenAPIInterceptorRetry {
case retry
case dontRetry
}
internal protocol OpenAPIInterceptor {
func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, Error>) -> Void)
func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void)
}
internal class DefaultOpenAPIInterceptor: OpenAPIInterceptor {
public init() {}
public func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
completion(.success(urlRequest))
}
public func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void) {
completion(.dontRetry)
}
}

View File

@ -22,6 +22,8 @@ open class OpenAPIClient: @unchecked Sendable {
/// If a HTTP status code is outside of this range the response will be interpreted as failed.
public var successfulStatusCodeRange: Range<Int>
public var interceptor: OpenAPIInterceptor
public init(
basePath: String = "http://petstore.swagger.io:80/v2",
customHeaders: [String: String] = [:],
@ -29,7 +31,8 @@ open class OpenAPIClient: @unchecked Sendable {
requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(),
apiResponseQueue: DispatchQueue = .main,
codableHelper: CodableHelper = CodableHelper(),
successfulStatusCodeRange: Range<Int> = 200..<300
successfulStatusCodeRange: Range<Int> = 200..<300,
interceptor: OpenAPIInterceptor = DefaultOpenAPIInterceptor()
) {
self.basePath = basePath
self.customHeaders = customHeaders
@ -38,6 +41,7 @@ open class OpenAPIClient: @unchecked Sendable {
self.apiResponseQueue = apiResponseQueue
self.codableHelper = codableHelper
self.successfulStatusCodeRange = successfulStatusCodeRange
self.interceptor = interceptor
}
public static let shared = OpenAPIClient()

View File

@ -25,7 +25,7 @@ public protocol URLSessionDataTaskProtocol {
}
// Protocol allowing implementations to alter what is returned or to test their implementations.
public protocol URLSessionProtocol {
public protocol URLSessionProtocol: Sendable {
// Task which performs the network fetch. Expected to be from URLSession.dataTask(with:completionHandler:) such that a network request
// is sent off when `.resume()` is called.
func dataTaskFromProtocol(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTaskProtocol
@ -160,21 +160,46 @@ open class URLSessionRequestBuilder<T>: RequestBuilder<T>, @unchecked Sendable {
do {
let request = try createURLRequest(urlSession: urlSession, method: xMethod, encoding: encoding, headers: headers)
let dataTask = urlSession.dataTaskFromProtocol(with: request) { data, response, error in
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
openAPIClient.interceptor.intercept(urlRequest: request, urlSession: urlSession, openAPIClient: openAPIClient) { result in
switch result {
case .success(let modifiedRequest):
let dataTask = urlSession.dataTaskFromProtocol(with: modifiedRequest) { data, response, error in
self.cleanupRequest()
if let response, let error {
self.openAPIClient.interceptor.retry(urlRequest: modifiedRequest, urlSession: urlSession, openAPIClient: self.openAPIClient, data: data, response: response, error: error) { retry in
switch retry {
case .retry:
self.execute(completion: completion)
case .dontRetry:
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
} else {
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
self.onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = self.taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = self.credential
self.requestTask.set(task: dataTask)
dataTask.resume()
case .failure(let error):
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
}
}
}
onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = credential
requestTask.set(task: dataTask)
dataTask.resume()
} catch {
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
@ -676,3 +701,26 @@ private extension Optional where Wrapped == Data {
}
extension JSONDataEncoding: ParameterEncoding {}
public enum OpenAPIInterceptorRetry {
case retry
case dontRetry
}
public protocol OpenAPIInterceptor {
func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, Error>) -> Void)
func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void)
}
public class DefaultOpenAPIInterceptor: OpenAPIInterceptor {
public init() {}
public func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
completion(.success(urlRequest))
}
public func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void) {
completion(.dontRetry)
}
}

View File

@ -24,6 +24,8 @@ open class OpenAPIClient: @unchecked Sendable {
/// If a HTTP status code is outside of this range the response will be interpreted as failed.
public var successfulStatusCodeRange: Range<Int>
public var interceptor: OpenAPIInterceptor
public init(
basePath: String = "http://petstore.swagger.io:80/v2",
customHeaders: [String: String] = [:],
@ -31,7 +33,8 @@ open class OpenAPIClient: @unchecked Sendable {
requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(),
apiResponseQueue: DispatchQueue = .main,
codableHelper: CodableHelper = CodableHelper(),
successfulStatusCodeRange: Range<Int> = 200..<300
successfulStatusCodeRange: Range<Int> = 200..<300,
interceptor: OpenAPIInterceptor = DefaultOpenAPIInterceptor()
) {
self.basePath = basePath
self.customHeaders = customHeaders
@ -40,6 +43,7 @@ open class OpenAPIClient: @unchecked Sendable {
self.apiResponseQueue = apiResponseQueue
self.codableHelper = codableHelper
self.successfulStatusCodeRange = successfulStatusCodeRange
self.interceptor = interceptor
}
public static let shared = OpenAPIClient()

View File

@ -25,7 +25,7 @@ public protocol URLSessionDataTaskProtocol {
}
// Protocol allowing implementations to alter what is returned or to test their implementations.
public protocol URLSessionProtocol {
public protocol URLSessionProtocol: Sendable {
// Task which performs the network fetch. Expected to be from URLSession.dataTask(with:completionHandler:) such that a network request
// is sent off when `.resume()` is called.
func dataTaskFromProtocol(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTaskProtocol
@ -160,21 +160,46 @@ open class URLSessionRequestBuilder<T>: RequestBuilder<T>, @unchecked Sendable {
do {
let request = try createURLRequest(urlSession: urlSession, method: xMethod, encoding: encoding, headers: headers)
let dataTask = urlSession.dataTaskFromProtocol(with: request) { data, response, error in
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
openAPIClient.interceptor.intercept(urlRequest: request, urlSession: urlSession, openAPIClient: openAPIClient) { result in
switch result {
case .success(let modifiedRequest):
let dataTask = urlSession.dataTaskFromProtocol(with: modifiedRequest) { data, response, error in
self.cleanupRequest()
if let response, let error {
self.openAPIClient.interceptor.retry(urlRequest: modifiedRequest, urlSession: urlSession, openAPIClient: self.openAPIClient, data: data, response: response, error: error) { retry in
switch retry {
case .retry:
self.execute(completion: completion)
case .dontRetry:
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
} else {
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
self.onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = self.taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = self.credential
self.requestTask.set(task: dataTask)
dataTask.resume()
case .failure(let error):
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
}
}
}
onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = credential
requestTask.set(task: dataTask)
dataTask.resume()
} catch {
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
@ -676,3 +701,26 @@ private extension Optional where Wrapped == Data {
}
extension JSONDataEncoding: ParameterEncoding {}
public enum OpenAPIInterceptorRetry {
case retry
case dontRetry
}
public protocol OpenAPIInterceptor {
func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, Error>) -> Void)
func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void)
}
public class DefaultOpenAPIInterceptor: OpenAPIInterceptor {
public init() {}
public func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
completion(.success(urlRequest))
}
public func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void) {
completion(.dontRetry)
}
}

View File

@ -16,7 +16,7 @@
6D4EFBB51C693BE200B96B06 /* PetAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4EFBB41C693BE200B96B06 /* PetAPITests.swift */; };
6D4EFBB71C693BED00B96B06 /* StoreAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4EFBB61C693BED00B96B06 /* StoreAPITests.swift */; };
6D4EFBB91C693BFC00B96B06 /* UserAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4EFBB81C693BFC00B96B06 /* UserAPITests.swift */; };
A5465867259E09C600C3929B /* BearerDecodableRequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5465866259E09C600C3929B /* BearerDecodableRequestBuilder.swift */; };
A5465867259E09C600C3929B /* BearerTokenHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5465866259E09C600C3929B /* BearerTokenHandler.swift */; };
A5782C772664FBA800CAA106 /* PetstoreClient in Frameworks */ = {isa = PBXBuildFile; productRef = A5782C762664FBA800CAA106 /* PetstoreClient */; };
A5EA12642419439700E30FC3 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5EA12622419439700E30FC3 /* FileUtils.swift */; };
A5EA12652419439700E30FC3 /* UIImage+Extras.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5EA12632419439700E30FC3 /* UIImage+Extras.swift */; };
@ -46,7 +46,7 @@
6D4EFBB41C693BE200B96B06 /* PetAPITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PetAPITests.swift; sourceTree = "<group>"; };
6D4EFBB61C693BED00B96B06 /* StoreAPITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreAPITests.swift; sourceTree = "<group>"; };
6D4EFBB81C693BFC00B96B06 /* UserAPITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserAPITests.swift; sourceTree = "<group>"; };
A5465866259E09C600C3929B /* BearerDecodableRequestBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BearerDecodableRequestBuilder.swift; sourceTree = "<group>"; };
A5465866259E09C600C3929B /* BearerTokenHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BearerTokenHandler.swift; sourceTree = "<group>"; };
A5EA12622419439700E30FC3 /* FileUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
A5EA12632419439700E30FC3 /* UIImage+Extras.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Extras.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -101,7 +101,7 @@
children = (
6D4EFB941C692C6300B96B06 /* AppDelegate.swift */,
6D4EFB961C692C6300B96B06 /* ViewController.swift */,
A5465866259E09C600C3929B /* BearerDecodableRequestBuilder.swift */,
A5465866259E09C600C3929B /* BearerTokenHandler.swift */,
6D4EFB981C692C6300B96B06 /* Main.storyboard */,
6D4EFB9B1C692C6300B96B06 /* Assets.xcassets */,
6D4EFB9D1C692C6300B96B06 /* LaunchScreen.storyboard */,
@ -231,7 +231,7 @@
buildActionMask = 2147483647;
files = (
6D4EFB971C692C6300B96B06 /* ViewController.swift in Sources */,
A5465867259E09C600C3929B /* BearerDecodableRequestBuilder.swift in Sources */,
A5465867259E09C600C3929B /* BearerTokenHandler.swift in Sources */,
6D4EFB951C692C6300B96B06 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -17,8 +17,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// Customize requestBuilderFactory
OpenAPIClient.shared.requestBuilderFactory = BearerRequestBuilderFactory()
OpenAPIClient.shared.interceptor = BearerOpenAPIInterceptor()
return true
}

View File

@ -1,171 +0,0 @@
//
// BearerDecodableRequestBuilder.swift
// SwaggerClient
//
// Created by Bruno Coelho on 31/12/2020.
// Copyright © 2020 Swagger. All rights reserved.
//
import Foundation
import PetstoreClient
class BearerRequestBuilderFactory: RequestBuilderFactory {
func getNonDecodableBuilder<T>() -> RequestBuilder<T>.Type {
BearerRequestBuilder<T>.self
}
func getBuilder<T: Decodable>() -> RequestBuilder<T>.Type {
BearerDecodableRequestBuilder<T>.self
}
}
class BearerRequestBuilder<T>: URLSessionRequestBuilder<T>, @unchecked Sendable {
@discardableResult
override func execute(completion: @Sendable @escaping (Result<Response<T>, ErrorResponse>) -> Void) -> RequestTask {
guard self.requiresAuthentication else {
return super.execute(completion: completion)
}
// Before making the request, we can validate if we have a bearer token to be able to make a request
BearerTokenHandler.shared.refreshTokenIfDoesntExist { token in
self.addHeaders(["Authorization": "Bearer \(token)"])
// Here we make the request
super.execute { result in
switch result {
case .success:
// If we got a successful response, we send the response to the completion block
completion(result)
case let .failure(error):
// If we got a failure response, we will analyse the error to see what we should do with it
if case let ErrorResponse.error(_, data, response, error) = error {
// If the error is an ErrorResponse.error() we will analyse it to see if it's a 401, and if it's a 401, we will refresh the token and retry the request
BearerTokenHandler.shared.refreshTokenIfUnauthorizedRequestResponse(
data: data,
response: response,
error: error
) { (wasTokenRefreshed, newToken) in
if wasTokenRefreshed, let newToken = newToken {
// If the token was refreshed, it's because it was a 401 error, so we refreshed the token, and we are going to retry the request by calling self.execute()
self.addHeaders(["Authorization": "Bearer \(newToken)"])
self.execute(completion: completion)
} else {
// If the token was not refreshed, it's because it was not a 401 error, so we send the response to the completion block
completion(result)
}
}
} else {
// If it's an unknown error, we send the response to the completion block
completion(result)
}
}
}
}
return requestTask
}
}
class BearerDecodableRequestBuilder<T: Decodable>: URLSessionDecodableRequestBuilder<T>, @unchecked Sendable {
@discardableResult
override func execute(completion: @Sendable @escaping (Result<Response<T>, ErrorResponse>) -> Void) -> RequestTask {
guard self.requiresAuthentication else {
return super.execute(completion: completion)
}
// Before making the request, we can validate if we have a bearer token to be able to make a request
BearerTokenHandler.shared.refreshTokenIfDoesntExist { token in
self.addHeaders(["Authorization": "Bearer \(token)"])
// Here we make the request
super.execute { result in
switch result {
case .success:
// If we got a successful response, we send the response to the completion block
completion(result)
case let .failure(error):
// If we got a failure response, we will analyse the error to see what we should do with it
if case let ErrorResponse.error(_, data, response, error) = error {
// If the error is an ErrorResponse.error() we will analyse it to see if it's a 401, and if it's a 401, we will refresh the token and retry the request
BearerTokenHandler.shared.refreshTokenIfUnauthorizedRequestResponse(
data: data,
response: response,
error: error
) { (wasTokenRefreshed, newToken) in
if wasTokenRefreshed, let newToken = newToken {
// If the token was refreshed, it's because it was a 401 error, so we refreshed the token, and we are going to retry the request by calling self.execute()
self.addHeaders(["Authorization": "Bearer \(newToken)"])
self.execute(completion: completion)
} else {
// If the token was not refreshed, it's because it was not a 401 error, so we send the response to the completion block
completion(result)
}
}
} else {
// If it's an unknown error, we send the response to the completion block
completion(result)
}
}
}
}
return requestTask
}
}
class BearerTokenHandler: @unchecked Sendable {
private init() {}
static let shared = BearerTokenHandler()
private var bearerToken: String? = nil
func refreshTokenIfDoesntExist(completionHandler: @escaping (String) -> Void) {
if let bearerToken = bearerToken {
completionHandler(bearerToken)
} else {
startRefreshingToken { token in
completionHandler(token)
}
}
}
func refreshTokenIfUnauthorizedRequestResponse(data: Data?, response: URLResponse?, error: Error?, completionHandler: @escaping (Bool, String?) -> Void) {
if let response = response as? HTTPURLResponse, response.statusCode == 401 {
startRefreshingToken { token in
completionHandler(true, token)
}
} else {
completionHandler(false, nil)
}
}
private func startRefreshingToken(completionHandler: @escaping (String) -> Void) {
// Get a bearer token
let dummyBearerToken = "..."
bearerToken = dummyBearerToken
completionHandler(dummyBearerToken)
}
}

View File

@ -0,0 +1,86 @@
//
// BearerTokenHandler.swift
// SwaggerClient
//
// Created by Bruno Coelho on 31/12/2020.
// Copyright © 2020 Swagger. All rights reserved.
//
import Foundation
import PetstoreClient
public class BearerOpenAPIInterceptor: OpenAPIInterceptor {
public init() {}
public func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
BearerTokenHandler.shared.refreshTokenIfDoesntExist { token in
// Change the current url request
var newUrlRequest = urlRequest
newUrlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
// Change the global headers
openAPIClient.customHeaders["Authorization"] = "Bearer \(token)"
completion(.success(newUrlRequest))
}
}
public func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void) {
// We will analyse the response to see if it's a 401, and if it's a 401, we will refresh the token and retry the request
BearerTokenHandler.shared.refreshTokenIfUnauthorizedRequestResponse(
data: data,
response: response,
error: error
) { (wasTokenRefreshed, newToken) in
if wasTokenRefreshed, let newToken = newToken {
// Change the global headers
openAPIClient.customHeaders["Authorization"] = "Bearer \(newToken)"
completion(.retry)
} else {
// If the token was not refreshed, it's because it was not a 401 error, so we send the response to the completion block
completion(.dontRetry)
}
}
}
}
class BearerTokenHandler: @unchecked Sendable {
private init() {}
static let shared = BearerTokenHandler()
private var bearerToken: String? = nil
func refreshTokenIfDoesntExist(completionHandler: @escaping (String) -> Void) {
if let bearerToken = bearerToken {
completionHandler(bearerToken)
} else {
startRefreshingToken { token in
completionHandler(token)
}
}
}
func refreshTokenIfUnauthorizedRequestResponse(data: Data?, response: URLResponse, error: Error, completionHandler: @escaping (Bool, String?) -> Void) {
if let response = response as? HTTPURLResponse, response.statusCode == 401 {
startRefreshingToken { token in
completionHandler(true, token)
}
} else {
completionHandler(false, nil)
}
}
private func startRefreshingToken(completionHandler: @escaping (String) -> Void) {
// Get a bearer token
let dummyBearerToken = "..."
bearerToken = dummyBearerToken
completionHandler(dummyBearerToken)
}
}

View File

@ -22,6 +22,8 @@ open class OpenAPIClient: @unchecked Sendable {
/// If a HTTP status code is outside of this range the response will be interpreted as failed.
public var successfulStatusCodeRange: Range<Int>
public var interceptor: OpenAPIInterceptor
public init(
basePath: String = "http://localhost",
customHeaders: [String: String] = [:],
@ -29,7 +31,8 @@ open class OpenAPIClient: @unchecked Sendable {
requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(),
apiResponseQueue: DispatchQueue = .main,
codableHelper: CodableHelper = CodableHelper(),
successfulStatusCodeRange: Range<Int> = 200..<300
successfulStatusCodeRange: Range<Int> = 200..<300,
interceptor: OpenAPIInterceptor = DefaultOpenAPIInterceptor()
) {
self.basePath = basePath
self.customHeaders = customHeaders
@ -38,6 +41,7 @@ open class OpenAPIClient: @unchecked Sendable {
self.apiResponseQueue = apiResponseQueue
self.codableHelper = codableHelper
self.successfulStatusCodeRange = successfulStatusCodeRange
self.interceptor = interceptor
}
public static let shared = OpenAPIClient()

View File

@ -25,7 +25,7 @@ public protocol URLSessionDataTaskProtocol {
}
// Protocol allowing implementations to alter what is returned or to test their implementations.
public protocol URLSessionProtocol {
public protocol URLSessionProtocol: Sendable {
// Task which performs the network fetch. Expected to be from URLSession.dataTask(with:completionHandler:) such that a network request
// is sent off when `.resume()` is called.
func dataTaskFromProtocol(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTaskProtocol
@ -160,21 +160,46 @@ open class URLSessionRequestBuilder<T>: RequestBuilder<T>, @unchecked Sendable {
do {
let request = try createURLRequest(urlSession: urlSession, method: xMethod, encoding: encoding, headers: headers)
let dataTask = urlSession.dataTaskFromProtocol(with: request) { data, response, error in
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
openAPIClient.interceptor.intercept(urlRequest: request, urlSession: urlSession, openAPIClient: openAPIClient) { result in
switch result {
case .success(let modifiedRequest):
let dataTask = urlSession.dataTaskFromProtocol(with: modifiedRequest) { data, response, error in
self.cleanupRequest()
if let response, let error {
self.openAPIClient.interceptor.retry(urlRequest: modifiedRequest, urlSession: urlSession, openAPIClient: self.openAPIClient, data: data, response: response, error: error) { retry in
switch retry {
case .retry:
self.execute(completion: completion)
case .dontRetry:
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
} else {
self.openAPIClient.apiResponseQueue.async {
self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion)
}
}
}
self.onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = self.taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = self.credential
self.requestTask.set(task: dataTask)
dataTask.resume()
case .failure(let error):
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
}
}
}
onProgressReady?(dataTask.progress)
URLSessionRequestBuilderConfiguration.shared.challengeHandlerStore[dataTask.taskIdentifier] = taskDidReceiveChallenge
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = credential
requestTask.set(task: dataTask)
dataTask.resume()
} catch {
self.openAPIClient.apiResponseQueue.async {
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
@ -676,3 +701,26 @@ private extension Optional where Wrapped == Data {
}
extension JSONDataEncoding: ParameterEncoding {}
public enum OpenAPIInterceptorRetry {
case retry
case dontRetry
}
public protocol OpenAPIInterceptor {
func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, Error>) -> Void)
func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void)
}
public class DefaultOpenAPIInterceptor: OpenAPIInterceptor {
public init() {}
public func intercept(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
completion(.success(urlRequest))
}
public func retry(urlRequest: URLRequest, urlSession: URLSessionProtocol, openAPIClient: OpenAPIClient, data: Data?, response: URLResponse, error: Error, completion: @escaping (OpenAPIInterceptorRetry) -> Void) {
completion(.dontRetry)
}
}