diff --git a/HTTPService.xcodeproj/project.pbxproj b/HTTPService.xcodeproj/project.pbxproj index 92740c5..a586e47 100644 --- a/HTTPService.xcodeproj/project.pbxproj +++ b/HTTPService.xcodeproj/project.pbxproj @@ -55,7 +55,6 @@ 3F43766F1AD1A14100FFC40C /* HTTPServiceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HTTPServiceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3F4376751AD1A14100FFC40C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3F43768B1AD1A19300FFC40C /* HTTPService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPService.swift; sourceTree = ""; }; - 3F64449C1B0A102200AD34A0 /* HTTPServiceTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "HTTPServiceTests-Bridging-Header.h"; sourceTree = ""; }; 3F98C32F1B0AAD5600AC23D4 /* PullRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullRequest.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -203,7 +202,6 @@ 3F4376741AD1A14100FFC40C /* Supporting Files */ = { isa = PBXGroup; children = ( - 3F64449C1B0A102200AD34A0 /* HTTPServiceTests-Bridging-Header.h */, 3F4376751AD1A14100FFC40C /* Info.plist */, ); name = "Supporting Files"; @@ -560,7 +558,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "com.jeremyfox.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "HTTPServiceTests/HTTPServiceTests-Bridging-Header.h"; + SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; @@ -580,7 +578,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "com.jeremyfox.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "HTTPServiceTests/HTTPServiceTests-Bridging-Header.h"; + SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/HTTPService/HTTPRequest.swift b/HTTPService/HTTPRequest.swift index 9d43c6e..28dc1df 100644 --- a/HTTPService/HTTPRequest.swift +++ b/HTTPService/HTTPRequest.swift @@ -51,6 +51,48 @@ public protocol HTTPRequest { init(id: String?) } +public protocol HTTPBatchRequest { + associatedtype Request: HTTPRequest + + var requests: [Request] { get } + + init(requests: [Request]) +} + +struct Req: HTTPRequest { + typealias ResultType = String + + typealias BodyType = HTTPRequestNoBody + + var endpoint: String = "" + + var method: HTTPMethod = .get + + var params: [String : Any]? = nil + + var body: HTTPRequestNoBody? = nil + + var headers: [String : String]? = nil + + var includeServiceLevelHeaders: Bool = true + + var includeServiceLevelAuthorization: Bool = true + + init(id: String?) { + + } +} + +struct BatchReq: HTTPBatchRequest { + typealias Request = Req + + var requests: [Req] + + init(requests: [Req]) { + self.requests = requests + } +} + /// Represents a response with no content, typically used for HTTP 204 (No Content) responses. public struct HTTPResponseNoContent: Decodable {} @@ -174,7 +216,7 @@ extension HTTPRequest { request.addValue(auth.value, forHTTPHeaderField: "Authorization") } - // Add the additional HTTP Headers passed in (most likely from the HTTPService) + // Add the additional HTTP Headers passed in (most likely from the NetworkService) if includeServiceLevelHeaders { // Add the service level headers additionalHeaders?.forEach { (key, value) in diff --git a/HTTPService/HTTPService.swift b/HTTPService/HTTPService.swift index 9fc1c23..949e089 100644 --- a/HTTPService/HTTPService.swift +++ b/HTTPService/HTTPService.swift @@ -29,7 +29,7 @@ public typealias HTTPHeaders = [String: String] public typealias BaseURL = URL /// A typealias representing the result of an HTTP operation, which can either be a success with an optional result of type `T?`, -/// or a failure with an `HTTPServiceError`. +/// or a failure with an `NetworkServiceError`. /// /// This typealias simplifies handling the outcome of HTTP requests. /// @@ -44,10 +44,10 @@ public typealias BaseURL = URL /// } /// } /// ``` -public typealias HTTPResult = Result +public typealias HTTPResult = Result /// An extension to `HTTPURLResponse` that provides additional functionality for checking failure status -/// and converting HTTP status codes to `HTTPServiceError`. +/// and converting HTTP status codes to `NetworkServiceError`. extension HTTPURLResponse { /// A Boolean value indicating whether the response status code represents a failure. @@ -57,14 +57,14 @@ extension HTTPURLResponse { return statusCode >= 400 } - /// Converts the response status code to a corresponding `HTTPServiceError`. + /// Converts the response status code to a corresponding `NetworkServiceError`. /// - /// This method checks the response status code and returns an appropriate `HTTPServiceError` if the status code indicates a failure. + /// This method checks the response status code and returns an appropriate `NetworkServiceError` if the status code indicates a failure. /// If the status code does not represent a failure, this method returns `nil`. /// /// - Parameter message: An optional message to include with the error. - /// - Returns: An `HTTPServiceError` corresponding to the response status code, or `nil` if the status code is not a failure. - func httpServiceError(with message: String? = nil) -> HTTPServiceError? { + /// - Returns: An `NetworkServiceError` corresponding to the response status code, or `nil` if the status code is not a failure. + func networkServiceError(with message: String? = nil) -> NetworkServiceError? { guard isFailure else { return nil } switch statusCode { @@ -81,7 +81,7 @@ extension HTTPURLResponse { /// A protocol that defines the requirements for an HTTP-based service. /// -/// Types that conform to `HTTPService` must implement various methods to execute different types of HTTP requests, +/// Types that conform to `NetworkService` must implement various methods to execute different types of HTTP requests, /// such as `HTTPRequest`, `HTTPPagedRequest`, and `HTTPDownloadRequest`. This protocol provides a flexible foundation /// for building services that interact with HTTP APIs. /// @@ -90,10 +90,10 @@ extension HTTPURLResponse { /// - `Authorization`: A type conforming to `HTTPAuthorization`, which handles the authorization logic for the service. /// /// - SeeAlso: `HTTPServiceBuildable`, `HTTPAuthorization` -public protocol HTTPService: AnyObject { +public protocol NetworkService: AnyObject { - /// The associated builder type, which must conform to `HTTPServiceBuildable`. - associatedtype Builder: HTTPServiceBuildable + /// The associated builder type, which must conform to `NetworkServiceBuildable`. + associatedtype Builder: NetworkServiceBuildable /// The associated authorization type, which must conform to `HTTPAuthorization`. associatedtype Authorization: HTTPAuthorization @@ -127,7 +127,7 @@ public protocol HTTPService: AnyObject { /// - Parameter request: The HTTP request to execute. /// - Returns: A `Task` that returns a `Result` containing the result of the request or an error if the request fails. @discardableResult - func executeWithCancelation(request: T) -> Task, Never> where T : HTTPRequest + func executeWithCancelation(request: T) -> Task, Never> where T : HTTPRequest /// Executes a given paged HTTP request asynchronously and returns the result. /// @@ -247,26 +247,11 @@ public protocol HTTPService: AnyObject { /// - Parameter requests: An array of HTTP requests conforming to `HTTPRequest`. /// - Returns: An array of `HTTPResult` objects corresponding to each request's result type. /// - Throws: An error if any of the requests fail. - func execute(_ requests: [T]) async throws -> [HTTPResult] where T : HTTPRequest - - /// Executes a batch of HTTP data requests and returns their results as an array of `HTTPResult`. - /// This method is used for batching multiple HTTP requests that deal with raw data processing. - /// - /// - Parameter requests: An array of HTTP data requests conforming to `HTTPDataRequest`. - /// - Returns: An array of `HTTPResult` objects corresponding to each request's result type. - /// - Throws: An error if any of the requests fail. - func execute(_ requests: [T]) async throws -> [HTTPResult] where T : HTTPDataRequest - - /// Executes a batch of chainable HTTP requests and returns their results as an array of `HTTPResult`. - /// This method supports batching multiple requests where each request can be chained to another. - /// - /// - Parameter requests: An array of HTTP requests conforming to `HTTPRequestChainable`. - /// - Returns: An array of `HTTPResult` objects corresponding to each request's result type. - /// - Throws: An error if any of the requests fail. - func execute(_ requests: [T]) async throws -> [HTTPResult] where T : HTTPRequestChainable + @discardableResult + func execute(batch request: T) async -> [HTTPResult] } -extension HTTPService { +extension NetworkService { private func logRequestInfo(for request: URLRequest) { var info = """ @@ -295,7 +280,7 @@ extension HTTPService { } guard !response.isFailure else { - return .failure(response.httpServiceError(with: "") ?? .requestFailed("")) + return .failure(response.networkServiceError(with: "") ?? .requestFailed("")) } guard !(T.ResultType.self is HTTPResponseNoContent.Type) else { @@ -336,7 +321,7 @@ extension HTTPService { } guard !response.isFailure else { - return .failure(response.httpServiceError(with: "") ?? .requestFailed("")) + return .failure(response.networkServiceError(with: "") ?? .requestFailed("")) } guard !(T.ResultType.self is HTTPResponseNoContent.Type) else { @@ -386,7 +371,7 @@ extension HTTPService { } guard !response.isFailure else { - return .failure(response.httpServiceError(with: "") ?? .requestFailed("")) + return .failure(response.networkServiceError(with: "") ?? .requestFailed("")) } guard data.count > 0 else { @@ -415,19 +400,19 @@ extension HTTPService { let (data, urlResponse) = try await urlSession.data(for: urlRequest) guard let response = urlResponse as? HTTPURLResponse else { - let error = HTTPServiceError.serverError("") + let error = NetworkServiceError.serverError("") request.didComplete(request: urlRequest, with: error) return .failure(error) } guard !response.isFailure else { - let error = response.httpServiceError(with: "") ?? .requestFailed("") + let error = response.networkServiceError(with: "") ?? .requestFailed("") request.didComplete(request: urlRequest, with: error) return .failure(error) } guard data.count > 0 else { - let error = HTTPServiceError.emptyResponseData(response.url?.absoluteString ?? "") + let error = NetworkServiceError.emptyResponseData(response.url?.absoluteString ?? "") request.didComplete(request: urlRequest, with: error) return .failure(error) } @@ -473,19 +458,19 @@ extension HTTPService { let (data, urlResponse) = try await urlSession.data(for: urlRequest) guard let response = urlResponse as? HTTPURLResponse else { - let error = HTTPServiceError.serverError("") + let error = NetworkServiceError.serverError("") request.didComplete(request: urlRequest, with: error) return .failure(error) } guard !response.isFailure else { - let error = response.httpServiceError(with: "") ?? .requestFailed("") + let error = response.networkServiceError(with: "") ?? .requestFailed("") request.didComplete(request: urlRequest, with: error) return .failure(error) } guard data.count > 0 else { - let error = HTTPServiceError.emptyResponseData(response.url?.absoluteString ?? "") + let error = NetworkServiceError.emptyResponseData(response.url?.absoluteString ?? "") request.didComplete(request: urlRequest, with: error) return .failure(error) } @@ -531,19 +516,19 @@ extension HTTPService { let (data, urlResponse) = try await urlSession.data(for: urlRequest) guard let response = urlResponse as? HTTPURLResponse else { - let error = HTTPServiceError.serverError("") + let error = NetworkServiceError.serverError("") request.didComplete(request: urlRequest, with: error) return .failure(error) } guard !response.isFailure else { - let error = response.httpServiceError(with: "") ?? .requestFailed("") + let error = response.networkServiceError(with: "") ?? .requestFailed("") request.didComplete(request: urlRequest, with: error) return .failure(error) } guard data.count > 0 else { - let error = HTTPServiceError.emptyResponseData(response.url?.absoluteString ?? "") + let error = NetworkServiceError.emptyResponseData(response.url?.absoluteString ?? "") request.didComplete(request: urlRequest, with: error) return .failure(error) } @@ -579,7 +564,7 @@ extension HTTPService { } guard !response.isFailure else { - return .failure(response.httpServiceError(with: "") ?? .requestFailed("")) + return .failure(response.networkServiceError(with: "") ?? .requestFailed("")) } guard !url.absoluteString.isEmpty else { @@ -608,19 +593,19 @@ extension HTTPService { let (url, urlResponse) = try await urlSession.download(for: urlRequest) guard let response = urlResponse as? HTTPURLResponse else { - let error = HTTPServiceError.serverError("") + let error = NetworkServiceError.serverError("") request.didComplete(request: urlRequest, with: error) return .failure(error) } guard !response.isFailure else { - let error = response.httpServiceError(with: "") ?? .requestFailed("") + let error = response.networkServiceError(with: "") ?? .requestFailed("") request.didComplete(request: urlRequest, with: error) return .failure(error) } guard !url.absoluteString.isEmpty else { - let error = HTTPServiceError.downloadFailed("Failing URL: \(response.url?.absoluteString ?? "")") + let error = NetworkServiceError.downloadFailed("Failing URL: \(response.url?.absoluteString ?? "")") request.didComplete(request: urlRequest, with: error) return .failure(error) } @@ -654,7 +639,7 @@ extension HTTPService { } guard !response.isFailure else { - return .failure(response.httpServiceError(with: "") ?? .requestFailed("")) + return .failure(response.networkServiceError(with: "") ?? .requestFailed("")) } guard data.count > 0 else { @@ -682,71 +667,19 @@ extension HTTPService { // MARK: Batching -extension HTTPService { - func execute(_ requests: [T]) async throws -> [HTTPResult] where T : HTTPRequest { - do { - return try await withThrowingTaskGroup(of: HTTPResult.self) { [weak self] group in - guard let self = self else { - return [.failure(.requestFailed("Service was deallocated"))] - } - - var results: [HTTPResult] = [] - - for request in requests { - group.addTask { [weak self] in - guard let self = self else { return .failure(.requestFailed("Service was deallocated")) } - return await self.execute(request: request) - } - } - - for try await result in group { - results.append(result) - } - - return results - } - } catch { - return [.failure(.requestFailed(error.localizedDescription))] - } - } +extension NetworkService { - func execute(_ requests: [T]) async throws -> [HTTPResult] where T : HTTPDataRequest { - do { - return try await withThrowingTaskGroup(of: HTTPResult.self) { [weak self] group in - guard let self = self else { - return [.failure(.requestFailed("Service was deallocated"))] - } - - var results: [HTTPResult] = [] - - for request in requests { - group.addTask { [weak self] in - guard let self = self else { return .failure(.requestFailed("Service was deallocated")) } - return await self.execute(request: request) - } - } - - for try await result in group { - results.append(result) - } - - return results - } - } catch { - return [.failure(.requestFailed(error.localizedDescription))] - } - } - - func execute(_ requests: [T]) async throws -> [HTTPResult] where T : HTTPRequestChainable { + @discardableResult + public func execute(batch request: T) async -> [HTTPResult] { do { - return try await withThrowingTaskGroup(of: HTTPResult.self) { [weak self] group in + return try await withThrowingTaskGroup(of: HTTPResult.self) { [weak self] group in guard let self = self else { - return [.failure(.requestFailed("Service was deallocated"))] + throw NetworkServiceError.requestFailed("Service was deallocated") } - var results: [HTTPResult] = [] + var results: [HTTPResult] = [] - for request in requests { + for request in request.requests { group.addTask { [weak self] in guard let self = self else { return .failure(.requestFailed("Service was deallocated")) } return await self.execute(request: request) @@ -763,4 +696,5 @@ extension HTTPService { return [.failure(.requestFailed(error.localizedDescription))] } } + } diff --git a/HTTPService/HTTPServiceBuildable.swift b/HTTPService/HTTPServiceBuildable.swift index 8a8750a..9d2e487 100644 --- a/HTTPService/HTTPServiceBuildable.swift +++ b/HTTPService/HTTPServiceBuildable.swift @@ -6,21 +6,21 @@ // Copyright © 2019 Jeremy Fox. All rights reserved. // -/// A protocol that extends `ServiceBuildable` for building services that conform to `HTTPService`. +/// A protocol that extends `ServiceBuildable` for building services that conform to `NetworkService`. /// -/// `HTTPServiceBuildable` inherits from `ServiceBuildable` and adds the constraint that the associated `Service` type must conform to the `HTTPService` protocol. -/// This protocol is intended for use with builders that create HTTP-based services, ensuring that the services adhere to the `HTTPService` protocol. +/// `NetworkServiceBuildable` inherits from `ServiceBuildable` and adds the constraint that the associated `Service` type must conform to the `NetworkService` protocol. +/// This protocol is intended for use with builders that create HTTP-based services, ensuring that the services adhere to the `NetworkService` protocol. /// /// ### Example Conformance: /// ```swift -/// struct MyHTTPServiceBuilder: HTTPServiceBuildable { -/// static func build() -> MyHTTPService? { -/// // Implementation to build and return an instance of MyHTTPService +/// struct MyServiceBuilder: NetworkServiceBuildable { +/// static func build() -> MyService? { +/// // Implementation to build and return an instance of MyService /// } /// } /// ``` /// -/// - Note: The associated `Service` type must conform to `HTTPService`. +/// - Note: The associated `Service` type must conform to `NetworkService`. /// -/// - SeeAlso: `ServiceBuildable`, `HTTPService` -public protocol HTTPServiceBuildable: ServiceBuildable where Service: HTTPService {} +/// - SeeAlso: `ServiceBuildable`, `NetworkService` +public protocol NetworkServiceBuildable: ServiceBuildable where Service: NetworkService {} diff --git a/HTTPService/HTTPServiceError.swift b/HTTPService/HTTPServiceError.swift index 3833879..46b62ba 100644 --- a/HTTPService/HTTPServiceError.swift +++ b/HTTPService/HTTPServiceError.swift @@ -8,7 +8,7 @@ import Foundation -public enum HTTPServiceError: Error { +public enum NetworkServiceError: Error { /// Used as a catchall for any error codes not defined below case requestFailed(String) /// Used when response data is expected but not received diff --git a/HTTPService/ServiceBuilder.swift b/HTTPService/ServiceBuilder.swift index 79750af..b29dc32 100644 --- a/HTTPService/ServiceBuilder.swift +++ b/HTTPService/ServiceBuilder.swift @@ -8,7 +8,7 @@ import Foundation -/// A generic builder for creating and managing instances of type `T` that conform to `HTTPService`. +/// A generic builder for creating and managing instances of type `T` that conform to `NetworkService`. /// /// `ServiceBuilder` provides methods for building instances of `T`, optionally using a cached version, /// and for purging the cached instance from the service cache. This class is designed to streamline the @@ -21,10 +21,10 @@ import Foundation /// await ServiceBuilder.purgeCache() /// ``` /// -/// - Note: The `ServiceBuilder` class is only available for types that conform to `HTTPService`. +/// - Note: The `ServiceBuilder` class is only available for types that conform to `NetworkService`. /// -/// - SeeAlso: `HTTPService`, `ServiceCache` -public class ServiceBuilder { +/// - SeeAlso: `NetworkService`, `ServiceCache` +public class ServiceBuilder { /// Purges the cached instance of type `T` from the service cache. /// @@ -65,7 +65,7 @@ public class ServiceBuilder { /// A thread-safe, actor-based cache for storing and retrieving instances of services. /// -/// `ServiceCache` is designed to cache instances of services that conform to the `HTTPService` protocol. +/// `ServiceCache` is designed to cache instances of services that conform to the `NetworkService` protocol. /// This cache is managed as a singleton, providing a shared instance that can be used across the application. /// The actor-based approach ensures that access to the cache is safe and synchronized in concurrent environments. fileprivate actor ServiceCache { @@ -83,7 +83,7 @@ fileprivate actor ServiceCache { /// /// - Parameter key: A unique string identifier used to look up the cached service. /// - Returns: An optional instance of type `T` if it exists in the cache; otherwise, `nil`. - func get(key: String) -> T? { + func get(key: String) -> T? { return cache[key] as? T } @@ -94,7 +94,7 @@ fileprivate actor ServiceCache { /// - Parameters: /// - service: The service instance to be cached. /// - key: A unique string identifier used to store the service in the cache. - func set(service: T, for key: String) { + func set(service: T, for key: String) { cache[key] = service } diff --git a/HTTPServiceTests/HTTPServiceTests-Bridging-Header.h b/HTTPServiceTests/HTTPServiceTests-Bridging-Header.h deleted file mode 100644 index 1b2cb5d..0000000 --- a/HTTPServiceTests/HTTPServiceTests-Bridging-Header.h +++ /dev/null @@ -1,4 +0,0 @@ -// -// Use this file to import your target's public headers that you would like to expose to Swift. -// - diff --git a/HTTPServiceTests/HTTPServiceTests.swift b/HTTPServiceTests/HTTPServiceTests.swift index 64a4108..f3f939c 100644 --- a/HTTPServiceTests/HTTPServiceTests.swift +++ b/HTTPServiceTests/HTTPServiceTests.swift @@ -29,4 +29,31 @@ class HTTPServiceTests: XCTestCase { waitForExpectations(timeout: 5) } + func testServiceExecutesBatchRequestsAndReturnsParsedResults() { + let expectation = expectation(description: "Batch requests complete") + let batchRequest = GetPullRequests(requests: [ + GitHubGetPullRequest(id: "123"), + GitHubGetPullRequest(id: "456") + ]) + + Task { + let service = await ServiceBuilder.build()! + let results: [HTTPResult] = await service.execute(batch: batchRequest) + + XCTAssert(results.count == batchRequest.requests.count) + results.forEach { result in + switch result { + case let .success(pr): + XCTAssertTrue(batchRequest.requests.contains(where: { $0.prId == "\(pr!.id)" })) + case .failure(_): + XCTFail() + } + } + + expectation.fulfill() + } + + waitForExpectations(timeout: 5) + } + } diff --git a/HTTPServiceTests/Test Requests/GitHubGetPullRequest.swift b/HTTPServiceTests/Test Requests/GitHubGetPullRequest.swift index ea4ed4b..f6c07e7 100644 --- a/HTTPServiceTests/Test Requests/GitHubGetPullRequest.swift +++ b/HTTPServiceTests/Test Requests/GitHubGetPullRequest.swift @@ -14,7 +14,9 @@ struct GitHubGetPullRequest: HTTPRequest { typealias ResultType = PullRequest typealias BodyType = HTTPRequestNoBody - var endpoint = "/something" + var endpoint: String { + "/something/\(prId!)" + } var method: HTTPMethod = .get var params: [String : Any]? var body: HTTPRequestNoBody? @@ -28,3 +30,13 @@ struct GitHubGetPullRequest: HTTPRequest { prId = id } } + +struct GetPullRequests: HTTPBatchRequest { + var requests: [GitHubGetPullRequest] + + init(requests: [GitHubGetPullRequest]) { + self.requests = requests + } + + typealias Request = GitHubGetPullRequest +} diff --git a/HTTPServiceTests/Test Service/GitHubService.swift b/HTTPServiceTests/Test Service/GitHubService.swift index b957c80..06de78b 100644 --- a/HTTPServiceTests/Test Service/GitHubService.swift +++ b/HTTPServiceTests/Test Service/GitHubService.swift @@ -9,7 +9,8 @@ import Foundation import HTTPService -final class GitHubService: HTTPService { +final class GitHubService: NetworkService { + typealias Builder = GitHubService typealias Authorization = HTTPTokenAuthorization @@ -26,7 +27,7 @@ final class GitHubService: HTTPService { } } -extension GitHubService: HTTPServiceBuildable { +extension GitHubService: NetworkServiceBuildable { typealias Service = GitHubService static func build() -> GitHubService? { @@ -39,7 +40,16 @@ extension GitHubService { @discardableResult func execute(request: T) async -> HTTPResult where T : HTTPRequest { do { - let data = try JSONSerialization.data(withJSONObject: ["id": 123, "name": "PR Name"], options: .init(rawValue: 0)) + var prJson: [String: Any] = [:] + switch request.endpoint { + case let endpoint where endpoint.contains("123"): + prJson = ["id": 123, "name": "PR Name"] + case let endpoint where endpoint.contains("456"): + prJson = ["id": 456, "name": "PR Name"] + default: + break + } + let data = try JSONSerialization.data(withJSONObject: prJson, options: .init(rawValue: 0)) let pr = try JSONDecoder().decode(T.ResultType.self, from: data) return .success(pr) } catch _ { diff --git a/Package.swift b/Package.swift index 67c635f..9f92485 100644 --- a/Package.swift +++ b/Package.swift @@ -10,6 +10,7 @@ let package = Package( products: [ .library( name: "HTTPService", + type: .dynamic, targets: ["HTTPService"] ) ], @@ -18,7 +19,10 @@ let package = Package( .target( name: "HTTPService", path: "HTTPService", - exclude: ["Info.plist"] + exclude: ["Info.plist"], + swiftSettings: [ + .unsafeFlags(["-enable-library-evolution"]) + ] ), .testTarget( name: "HTTPServiceTests", diff --git a/README.md b/README.md index 8f250ec..cbe6382 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ A super simple networking library which utilizes a service builder to construct and cache services. ## Usage -NOTE: These instructions are for v3.0+ only. Prior versions are no longer supported. +NOTE: These instructions are for v4.2.0+ only. Prior versions are no longer supported. ### Creating a Service -Start by creating a class that will represent a service and have it conform to the HTTPService protocol. +Start by creating a class that will represent a service and have it conform to the `NetworkService` protocol. ```swift -public class GitHubService: HTTPService { +public class GitHubService: NetworkService { typealias Builder = GitHubService typealias Authorization = HTTPTokenAuthorization @@ -25,9 +25,9 @@ public class GitHubService: HTTPService { } ``` -Now, in order for `ServiceBuilder` to be able to build this service, we'll conform to the `HTTPServiceBuildable` protocol. +Now, in order for `ServiceBuilder` to be able to build this service, we'll conform to the `NetworkServiceBuildable` protocol. ```swift -extension GitHubService: HTTPServiceBuildable { +extension GitHubService: NetworkServiceBuildable { typealias Service = GitHubService static func build() -> GitHubService? { @@ -65,7 +65,7 @@ class GitHubGetPRsRequest: HTTPRequest { typealias ResultType = [GitHubPR] typealias BodyType = HTTPRequestNoBody // Only needed for HTTPMethod.post, HTTPMethod.put, or HTTPMethod.patch requests - // HTTPService required attributes + // NetworkService required attributes var endpoint: String { return "repos/\(owner)/\(repo)/pulls" } @@ -80,7 +80,7 @@ class GitHubGetPRsRequest: HTTPRequest { let owner: String let repo: String - // HTTPService required init + // NetworkService required init required init(id: String?) { fatalError("Use init(owner:repo) instead)") } @@ -196,3 +196,41 @@ class GitHubGetPagedPRsRequest: HTTPPagedRequest { ``` This allows you to easily retrieve and navigate through paginated results from your API. + +### Request Batching + +If you need to send mutliple requests at once, use `HTTPBatchRequest` to send them in an efficient mannor and get back the result of each request. + +Start by creating your `HTTPBatchRequest`: +```swift +struct GetPullRequests: HTTPBatchRequest { + typealias Request = GitHubGetPullRequest + + var requests: [GitHubGetPullRequest] + + init(requests: [GitHubGetPullRequest]) { + self.requests = requests + } +} +``` + +Then create an instance of your batch request, passing it the necessary requests, and call `execute(batch:)` on your `NetworkService`: +```swift +let batchRequest = GetPullRequests(requests: [ + GitHubGetPullRequest(id: "123"), + GitHubGetPullRequest(id: "456") +]) + +let service = await ServiceBuilder.build()! +let results: [HTTPResult] = await service.execute(batch: batchRequest) + +// Do something with the `results` +results.forEach { result in + switch result { + case let .success(pr): + print(pr) + case .failure(error): + print(error) + } +} +```