From 2b67ea4d5b65d26302e5b504060613c96a0e03fc Mon Sep 17 00:00:00 2001 From: dankinsoid <30962149+dankinsoid@users.noreply.github.com> Date: Sat, 23 Mar 2024 18:33:07 +0300 Subject: [PATCH] 0.45.0 --- Example/Sources/PetStore/Models.swift | 6 +- Example/Sources/PetStore/PetStore.swift | 22 +++---- README.md | 2 +- .../Clients/NetworkClientCaller.swift | 25 -------- .../Modifiers/RequestCompression.swift | 63 ++++++++++++------- .../Modifiers/RequestValidator.swift | 18 +++++- .../TokenRefresher/TokenRefresher.swift | 4 +- .../Modifiers/RequestCompressionTests.swift | 10 ++- .../TestUtils/TestHTTPClient.swift | 11 ++-- 9 files changed, 87 insertions(+), 74 deletions(-) diff --git a/Example/Sources/PetStore/Models.swift b/Example/Sources/PetStore/Models.swift index f3e2c47..9323a89 100644 --- a/Example/Sources/PetStore/Models.swift +++ b/Example/Sources/PetStore/Models.swift @@ -43,7 +43,7 @@ public enum PetStatus: String, Codable { public struct Tokens: Codable { - public var accessToken: String - public var refreshToken: String - public var expiryDate: Date + public var accessToken: String + public var refreshToken: String + public var expiryDate: Date } diff --git a/Example/Sources/PetStore/PetStore.swift b/Example/Sources/PetStore/PetStore.swift index 29a8b21..254281e 100644 --- a/Example/Sources/PetStore/PetStore.swift +++ b/Example/Sources/PetStore/PetStore.swift @@ -12,20 +12,20 @@ public struct PetStore { .fileIDLine(fileID: fileID, line: line) .bodyDecoder(PetStoreDecoder()) .tokenRefresher { refreshToken, client, _ in - guard let refreshToken else { - throw Errors.noRefreshToken - } - let tokens: Tokens = try await client("auth", "token") - .body(["refresh_token": refreshToken]) - .post() - return (tokens.accessToken, tokens.refreshToken, tokens.expiryDate) + guard let refreshToken else { + throw Errors.noRefreshToken + } + let tokens: Tokens = try await client("auth", "token") + .body(["refresh_token": refreshToken]) + .post() + return (tokens.accessToken, tokens.refreshToken, tokens.expiryDate) } } - public enum Errors: Error { - - case noRefreshToken - } + public enum Errors: Error { + + case noRefreshToken + } } // MARK: - "pet" path diff --git a/README.md b/README.md index 55ac1dd..dff11bb 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,7 @@ import PackageDescription let package = Package( name: "SomeProject", dependencies: [ - .package(url: "https://github.com/dankinsoid/swift-api-client.git", from: "0.44.1") + .package(url: "https://github.com/dankinsoid/swift-api-client.git", from: "0.45.0") ], targets: [ .target( diff --git a/Sources/SwiftAPIClient/Clients/NetworkClientCaller.swift b/Sources/SwiftAPIClient/Clients/NetworkClientCaller.swift index ebc9a68..3694a07 100644 --- a/Sources/SwiftAPIClient/Clients/NetworkClientCaller.swift +++ b/Sources/SwiftAPIClient/Clients/NetworkClientCaller.swift @@ -148,8 +148,6 @@ public extension APIClient { do { return try withRequest { request, configs in let fileIDLine = configs.fileIDLine ?? FileIDLine(fileID: fileID, line: line) - var request = request - try configs.beforeCall(&request, configs) if !configs.loggingComponents.isEmpty { let message = configs.loggingComponents.requestMessage(for: request, uuid: uuid, fileIDLine: fileIDLine) @@ -187,27 +185,4 @@ public extension APIClient { throw error } } - - /// Sets a closure to be executed before making a network call. - /// - /// - Parameters: - /// - closure: The closure to be executed before making a network call. It takes in an `inout URLRequest` and `APIClient.Configs` as parameters and can modify the request. - /// - Returns: The `APIClient` instance. - func beforeCall(_ closure: @escaping (inout URLRequest, APIClient.Configs) throws -> Void) -> APIClient { - configs { - let beforeCall = $0.beforeCall - $0.beforeCall = { request, configs in - try beforeCall(&request, configs) - try closure(&request, configs) - } - } - } -} - -public extension APIClient.Configs { - - var beforeCall: (inout URLRequest, APIClient.Configs) throws -> Void { - get { self[\.beforeCall] ?? { _, _ in } } - set { self[\.beforeCall] = newValue } - } } diff --git a/Sources/SwiftAPIClient/Modifiers/RequestCompression.swift b/Sources/SwiftAPIClient/Modifiers/RequestCompression.swift index 5575cca..e5447d4 100644 --- a/Sources/SwiftAPIClient/Modifiers/RequestCompression.swift +++ b/Sources/SwiftAPIClient/Modifiers/RequestCompression.swift @@ -24,32 +24,51 @@ public extension APIClient { duplicateHeaderBehavior: DuplicateHeaderBehavior = .skip, shouldCompressBodyData: @escaping (_ bodyData: Data) -> Bool = { _ in true } ) -> APIClient { - beforeCall { urlRequest, _ in - // No need to compress unless we have body data. No support for compressing streams. - guard let bodyData = urlRequest.httpBody else { - return - } + httpClientMiddleware( + CompressionMiddleware( + duplicateHeaderBehavior: duplicateHeaderBehavior, + shouldCompressBodyData: shouldCompressBodyData + ) + ) + } +} - guard shouldCompressBodyData(bodyData) else { - return - } +private struct CompressionMiddleware: HTTPClientMiddleware { - let contentEncodingKey = HTTPHeader.Key.contentEncoding.rawValue - if urlRequest.value(forHTTPHeaderField: contentEncodingKey) != nil { - switch duplicateHeaderBehavior { - case .error: - throw Errors.duplicateHeader(.contentEncoding) - case .replace: - // Header will be replaced once the body data is compressed. - break - case .skip: - return - } - } + let duplicateHeaderBehavior: DuplicateHeaderBehavior + let shouldCompressBodyData: (_ bodyData: Data) -> Bool - urlRequest.httpBody = try deflate(bodyData) - urlRequest.setValue("deflate", forHTTPHeaderField: contentEncodingKey) + func execute( + request: URLRequest, + configs: APIClient.Configs, + next: (URLRequest, APIClient.Configs) async throws -> (T, HTTPURLResponse) + ) async throws -> (T, HTTPURLResponse) { + // No need to compress unless we have body data. No support for compressing streams. + guard let bodyData = request.httpBody else { + return try await next(request, configs) } + + guard shouldCompressBodyData(bodyData) else { + return try await next(request, configs) + } + + let contentEncodingKey = HTTPHeader.Key.contentEncoding.rawValue + if request.value(forHTTPHeaderField: contentEncodingKey) != nil { + switch duplicateHeaderBehavior { + case .error: + throw Errors.duplicateHeader(.contentEncoding) + case .replace: + // Header will be replaced once the body data is compressed. + break + case .skip: + return try await next(request, configs) + } + } + + var urlRequest = request + urlRequest.httpBody = try deflate(bodyData) + urlRequest.setValue("deflate", forHTTPHeaderField: contentEncodingKey) + return try await next(urlRequest, configs) } } diff --git a/Sources/SwiftAPIClient/Modifiers/RequestValidator.swift b/Sources/SwiftAPIClient/Modifiers/RequestValidator.swift index 7d25bfe..bf9ba29 100644 --- a/Sources/SwiftAPIClient/Modifiers/RequestValidator.swift +++ b/Sources/SwiftAPIClient/Modifiers/RequestValidator.swift @@ -31,8 +31,20 @@ public extension APIClient { /// - Parameter validator: The `RequestValidator` to be used for validating `URLRequest` instances. /// - Returns: An instance of `APIClient` configured with the specified request validator. func requestValidator(_ validator: RequestValidator) -> APIClient { - beforeCall { - try validator.validate($0, $1) - } + httpClientMiddleware(RequestValidatorMiddleware(validator: validator)) + } +} + +private struct RequestValidatorMiddleware: HTTPClientMiddleware { + + let validator: RequestValidator + + func execute( + request: URLRequest, + configs: APIClient.Configs, + next: (URLRequest, APIClient.Configs) async throws -> (T, HTTPURLResponse) + ) async throws -> (T, HTTPURLResponse) { + try validator.validate(request, configs) + return try await next(request, configs) } } diff --git a/Sources/SwiftAPIClient/Modifiers/TokenRefresher/TokenRefresher.swift b/Sources/SwiftAPIClient/Modifiers/TokenRefresher/TokenRefresher.swift index 8e678c5..7e5276a 100644 --- a/Sources/SwiftAPIClient/Modifiers/TokenRefresher/TokenRefresher.swift +++ b/Sources/SwiftAPIClient/Modifiers/TokenRefresher/TokenRefresher.swift @@ -10,8 +10,8 @@ public extension APIClient { /// - request: The closure to use for requesting a new token and refresh token first time. Set to `nil` if you want to request and cache tokens manually. /// - refresh: The closure to use for refreshing a new token with refresh token. /// - auth: The closure that creates an `AuthModifier` for the new token. Default to `.bearer(token:)`. - /// - /// - Warning: Don't use this modifier with `.auth(_ modifier:)` as it will be override it. + /// + /// - Warning: Don't use this modifier with `.auth(_ modifier:)` as it will be override it. func tokenRefresher( cacheService: SecureCacheService = valueFor(live: .keychain, test: .mock), expiredStatusCodes: Set = [.unauthorized], diff --git a/Tests/SwiftAPIClientTests/Modifiers/RequestCompressionTests.swift b/Tests/SwiftAPIClientTests/Modifiers/RequestCompressionTests.swift index ddee3b2..f5970e7 100644 --- a/Tests/SwiftAPIClientTests/Modifiers/RequestCompressionTests.swift +++ b/Tests/SwiftAPIClientTests/Modifiers/RequestCompressionTests.swift @@ -10,11 +10,15 @@ import XCTest final class APIClientCompressionTests: XCTestCase { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - func testThatRequestCompressorProperlyCalculatesAdler32() throws { + func testThatRequestCompressorProperlyCalculatesAdler32() async throws { let client = APIClient(baseURL: URL(string: "https://example.com")!).compressRequest() - let request = try client.body(Data("Wikipedia".utf8)).request() + let body: Data = try await client + .body(Data([0])) + .httpTest { request, _ in + request.httpBody! + } // From https://en.wikipedia.org/wiki/Adler-32 - XCTAssertEqual(request.httpBody, Data([87, 105, 107, 105, 112, 101, 100, 105, 97])) + XCTAssertEqual(body, Data([0x78, 0x5E, 0x63, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01])) } } #endif diff --git a/Tests/SwiftAPIClientTests/TestUtils/TestHTTPClient.swift b/Tests/SwiftAPIClientTests/TestUtils/TestHTTPClient.swift index 4e3d23c..6c6b0d5 100644 --- a/Tests/SwiftAPIClientTests/TestUtils/TestHTTPClient.swift +++ b/Tests/SwiftAPIClientTests/TestUtils/TestHTTPClient.swift @@ -25,28 +25,31 @@ private struct Unimplemented: Error {} extension APIClient { + @discardableResult func httpTest( test: @escaping (URLRequest, APIClient.Configs) throws -> Void = { _, _ in } - ) async throws { + ) async throws -> Data { try await httpTest { try test($0, $1) return Data() } } + @discardableResult func httpTest( test: @escaping (URLRequest, APIClient.Configs) throws -> (Data, HTTPURLResponse) - ) async throws { + ) async throws -> Data { try await configs(\.testHTTPClient) { try test($0, $1) } .httpClient(.test()) - .call(.http, as: .void) + .call(.http, as: .identity) } + @discardableResult func httpTest( test: @escaping (URLRequest, APIClient.Configs) throws -> Data - ) async throws { + ) async throws -> Data { try await httpTest { let data = try test($0, $1) guard let response = HTTPURLResponse(