diff --git a/Example/Example/ExampleTarget.swift b/Example/Example/ExampleTarget.swift index a276e22..b028f2a 100644 --- a/Example/Example/ExampleTarget.swift +++ b/Example/Example/ExampleTarget.swift @@ -41,6 +41,17 @@ extension ExampleTarget: TargetType { } } + var queryParameters: [String: String]? { + switch self { + case .gitHubOrganizations: + [ + "per_page": "5", + ] + case .httpbinPOST: + nil + } + } + var method: HTTPMethod { switch self { case .gitHubOrganizations: diff --git a/Sources/SwiftAPIGate/APIGate.swift b/Sources/SwiftAPIGate/APIGate.swift index 570023e..3333c36 100644 --- a/Sources/SwiftAPIGate/APIGate.swift +++ b/Sources/SwiftAPIGate/APIGate.swift @@ -23,15 +23,25 @@ public final class APIGate: APIGateType { let updatedTarget: TargetType = try await middleware?.target(target) ?? target let baseURL = updatedTarget.baseURL let path = updatedTarget.path + let queryParameters = updatedTarget.queryParameters let method = updatedTarget.method let validationType = updatedTarget.validationType let headers = updatedTarget.headers - let requestURL: URL = if let path { - baseURL.appendingPathComponent(path) - } else { - baseURL + guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else { + throw URLError(.badURL) } + if let path = path { + components.path = path + } + if let queryParameters = queryParameters { + components.queryItems = queryParameters.map { URLQueryItem(name: $0.key, value: $0.value) } + } + + guard let requestURL = components.url else { + throw URLError(.badURL) + } + var request = URLRequest(url: requestURL) request.httpMethod = method.rawValue headers?.forEach { diff --git a/Sources/SwiftAPIGate/AbstractTarget.swift b/Sources/SwiftAPIGate/AbstractTarget.swift index c04124f..a97e804 100644 --- a/Sources/SwiftAPIGate/AbstractTarget.swift +++ b/Sources/SwiftAPIGate/AbstractTarget.swift @@ -3,6 +3,7 @@ import Foundation public struct AbstractTarget: TargetType { public let baseURL: URL public let path: String? + public let queryParameters: [String: String]? public let method: HTTPMethod public let validationType: ValidationType public let headers: [String: String]? @@ -11,6 +12,7 @@ public struct AbstractTarget: TargetType { public init( baseURL: URL, path: String?, + queryParameters: [String: String]? = nil, method: HTTPMethod, validationType: ValidationType = .none, headers: [String: String]? = nil, @@ -18,6 +20,7 @@ public struct AbstractTarget: TargetType { ) { self.baseURL = baseURL self.path = path + self.queryParameters = queryParameters self.method = method self.validationType = validationType self.headers = headers @@ -28,6 +31,7 @@ public struct AbstractTarget: TargetType { from target: TargetType, withBaseURL baseURL: URL? = nil, withPath path: String? = nil, + withQueryParameters queryParameters: [String: String]? = nil, withMethod method: HTTPMethod? = nil, withValidationType validationType: ValidationType? = nil, withHeaders headers: [String: String]? = nil, @@ -36,6 +40,7 @@ public struct AbstractTarget: TargetType { self.init( baseURL: baseURL ?? target.baseURL, path: path ?? target.path, + queryParameters: queryParameters ?? target.queryParameters, method: method ?? target.method, validationType: validationType ?? target.validationType, headers: headers ?? target.headers, diff --git a/Sources/SwiftAPIGate/TargetType.swift b/Sources/SwiftAPIGate/TargetType.swift index 12c52d7..a3add47 100644 --- a/Sources/SwiftAPIGate/TargetType.swift +++ b/Sources/SwiftAPIGate/TargetType.swift @@ -7,6 +7,9 @@ public protocol TargetType: CustomStringConvertible { /// The path to be appended to `baseURL` to form the full `URL`. var path: String? { get } + /// The query parameters to be appended to the `URL`. + var queryParameters: [String: String]? { get } + /// The HTTP method used. var method: HTTPMethod { get } diff --git a/Tests/SwiftAPIGateTests/APIGateTests.swift b/Tests/SwiftAPIGateTests/APIGateTests.swift index 86f529d..6b6c744 100644 --- a/Tests/SwiftAPIGateTests/APIGateTests.swift +++ b/Tests/SwiftAPIGateTests/APIGateTests.swift @@ -22,13 +22,31 @@ final class APIGateTests: XCTestCase { response: .init(url: Fixture.url, mimeType: nil, expectedContentLength: 0, textEncodingName: nil) ) + // Act + let apiResponse = try await sut.request(.userProfile("test")) + + // Assert + XCTAssertEqual(try XCTUnwrap(apiResponse.data), fakeData) + XCTAssertEqual(sessionMock.dataInvocations.map(\.url), [ + .init(string: "https://api.github.com/users/test")!, + ]) + } + + func testLoadWithQueryParameters() async throws { + // Arrange + let fakeData = #function.data(using: .utf8) + sessionMock.setDataValue( + data: fakeData, + response: .init(url: Fixture.url, mimeType: nil, expectedContentLength: 0, textEncodingName: nil) + ) + // Act let apiResponse = try await sut.request(.organizations) // Assert XCTAssertEqual(try XCTUnwrap(apiResponse.data), fakeData) - XCTAssertEqual(sessionMock.dataInvocations, [ - .init(url: .init(string: "https://api.github.com/organizations")!), + XCTAssertEqual(sessionMock.dataInvocations.map(\.url), [ + .init(string: "https://api.github.com/organizations?per_page=5")!, ]) } @@ -88,8 +106,8 @@ final class APIGateTests: XCTestCase { // Assert XCTAssertEqual(decodedResponse, exampleDecodable) - XCTAssertEqual(sessionMock.dataInvocations, [ - .init(url: .init(string: "https://api.github.com/organizations")!), + XCTAssertEqual(sessionMock.dataInvocations.map(\.url), [ + .init(string: "https://api.github.com/organizations?per_page=5")!, ]) } @@ -130,8 +148,8 @@ final class APIGateTests: XCTestCase { // Assert await XCTAssertThrowsAsyncError(try await act()) - XCTAssertEqual(sessionMock.dataInvocations, [ - .init(url: .init(string: "https://api.github.com/organizations")!), + XCTAssertEqual(sessionMock.dataInvocations.map(\.url), [ + .init(string: "https://api.github.com/organizations?per_page=5")!, ]) } } diff --git a/Tests/SwiftAPIGateTests/Fixtures/GitHubTarget.swift b/Tests/SwiftAPIGateTests/Fixtures/GitHubTarget.swift index 5e18ec0..720165c 100644 --- a/Tests/SwiftAPIGateTests/Fixtures/GitHubTarget.swift +++ b/Tests/SwiftAPIGateTests/Fixtures/GitHubTarget.swift @@ -23,6 +23,17 @@ extension GitHubTarget: TargetType { } } + var queryParameters: [String: String]? { + switch self { + case .organizations: + [ + "per_page": "5", + ] + default: + nil + } + } + var method: HTTPMethod { switch self { case .organizations: