diff --git a/Package.swift b/Package.swift index 1222c48..f1816f0 100644 --- a/Package.swift +++ b/Package.swift @@ -38,7 +38,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/apple/swift-openapi-runtime", "0.1.3" ..< "0.3.0"), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), ], targets: [ diff --git a/README.md b/README.md index b297385..7f46fae 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Add the package dependency in your `Package.swift`: ```swift .package( url: "https://github.com/apple/swift-openapi-urlsession", - .upToNextMinor(from: "0.2.0") + .upToNextMinor(from: "0.3.0") ), ``` diff --git a/Sources/OpenAPIURLSession/Documentation.docc/Documentation.md b/Sources/OpenAPIURLSession/Documentation.docc/Documentation.md index c1559a3..551611b 100644 --- a/Sources/OpenAPIURLSession/Documentation.docc/Documentation.md +++ b/Sources/OpenAPIURLSession/Documentation.docc/Documentation.md @@ -11,7 +11,7 @@ Use the transport with client code generated by [Swift OpenAPI Generator](https: ### Supported platforms and minimum versions | macOS | Linux | iOS | tvOS | watchOS | | :-: | :-: | :-: | :-: | :-: | -| ✅ 10.15+ | ✅ | ✅ 13+ | ✅ 13+ | ✅ 6+ | +| ✅ 10.15+ | ✅ | ✅ 13+ | ✅ 13+ | ✅ 6+ | ### Usage @@ -20,7 +20,7 @@ Add the package dependency in your `Package.swift`: ```swift .package( url: "https://github.com/apple/swift-openapi-urlsession", - .upToNextMinor(from: "0.2.0") + .upToNextMinor(from: "0.3.0") ), ``` diff --git a/Sources/OpenAPIURLSession/URLSessionTransport.swift b/Sources/OpenAPIURLSession/URLSessionTransport.swift index 1561969..abadad7 100644 --- a/Sources/OpenAPIURLSession/URLSessionTransport.swift +++ b/Sources/OpenAPIURLSession/URLSessionTransport.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// import OpenAPIRuntime +import HTTPTypes #if canImport(Darwin) import Foundation #else @@ -90,13 +91,19 @@ public struct URLSessionTransport: ClientTransport { } public func send( - _ request: OpenAPIRuntime.Request, + _ request: HTTPRequest, + body: HTTPBody?, baseURL: URL, operationID: String - ) async throws -> OpenAPIRuntime.Response { - let urlRequest = try URLRequest(request, baseURL: baseURL) + ) async throws -> (HTTPResponse, HTTPBody?) { + // TODO: https://github.com/apple/swift-openapi-generator/issues/301 + let urlRequest = try await URLRequest(request, body: body, baseURL: baseURL) let (responseBody, urlResponse) = try await invokeSession(urlRequest) - return try OpenAPIRuntime.Response(from: urlResponse, body: responseBody) + return try HTTPResponse.response( + method: request.method, + urlResponse: urlResponse, + data: responseBody + ) } private func invokeSession(_ urlRequest: URLRequest) async throws -> (Data, URLResponse) { @@ -129,7 +136,7 @@ public struct URLSessionTransport: ClientTransport { internal enum URLSessionTransportError: Error { /// Invalid URL composed from base URL and received request. - case invalidRequestURL(request: OpenAPIRuntime.Request, baseURL: URL) + case invalidRequestURL(path: String, method: HTTPRequest.Method, baseURL: URL) /// Returned `URLResponse` could not be converted to `HTTPURLResponse`. case notHTTPResponse(URLResponse) @@ -138,40 +145,74 @@ internal enum URLSessionTransportError: Error { case noResponse(url: URL?) } -extension OpenAPIRuntime.Response { - init(from urlResponse: URLResponse, body: Data) throws { +extension HTTPResponse { + static func response( + method: HTTPRequest.Method, + urlResponse: URLResponse, + data: Data + ) throws -> (HTTPResponse, HTTPBody?) { guard let httpResponse = urlResponse as? HTTPURLResponse else { throw URLSessionTransportError.notHTTPResponse(urlResponse) } - let headerFields: [HeaderField] = httpResponse - .allHeaderFields - .compactMap { headerName, headerValue in - guard let name = headerName as? String, let value = headerValue as? String else { - return nil - } - return HeaderField(name: name, value: value) + var headerFields = HTTPFields() + for (headerName, headerValue) in httpResponse.allHeaderFields { + guard + let rawName = headerName as? String, + let name = HTTPField.Name(rawName), + let value = headerValue as? String + else { + continue } - self.init(statusCode: httpResponse.statusCode, headerFields: headerFields, body: body) + headerFields[name] = value + } + let body: HTTPBody? + switch method { + case .head, .connect, .trace: + body = nil + default: + body = .init(data) + } + return ( + HTTPResponse( + status: .init(code: httpResponse.statusCode), + headerFields: headerFields + ), + body + ) } } extension URLRequest { - init(_ request: OpenAPIRuntime.Request, baseURL: URL) throws { - guard var baseUrlComponents = URLComponents(string: baseURL.absoluteString) else { - throw URLSessionTransportError.invalidRequestURL(request: request, baseURL: baseURL) + init(_ request: HTTPRequest, body: HTTPBody?, baseURL: URL) async throws { + guard + var baseUrlComponents = URLComponents(string: baseURL.absoluteString), + let requestUrlComponents = URLComponents(string: request.path ?? "") + else { + throw URLSessionTransportError.invalidRequestURL( + path: request.path ?? "", + method: request.method, + baseURL: baseURL + ) } - baseUrlComponents.percentEncodedPath += request.path - baseUrlComponents.percentEncodedQuery = request.query + + let path = requestUrlComponents.percentEncodedPath + baseUrlComponents.percentEncodedPath += path + baseUrlComponents.percentEncodedQuery = requestUrlComponents.percentEncodedQuery guard let url = baseUrlComponents.url else { - throw URLSessionTransportError.invalidRequestURL(request: request, baseURL: baseURL) + throw URLSessionTransportError.invalidRequestURL( + path: path, + method: request.method, + baseURL: baseURL + ) } self.init(url: url) - self.httpMethod = request.method.name + self.httpMethod = request.method.rawValue for header in request.headerFields { - self.addValue(header.value, forHTTPHeaderField: header.name) + self.setValue(header.value, forHTTPHeaderField: header.name.canonicalName) } - if let body = request.body { - self.httpBody = body + if let body { + // TODO: https://github.com/apple/swift-openapi-generator/issues/301 + self.httpBody = try await Data(collecting: body, upTo: .max) } } } @@ -183,9 +224,9 @@ extension URLSessionTransportError: LocalizedError { extension URLSessionTransportError: CustomStringConvertible { public var description: String { switch self { - case let .invalidRequestURL(request: request, baseURL: baseURL): + case let .invalidRequestURL(path: path, method: method, baseURL: baseURL): return - "Invalid request URL from request path: \(request.path), query: \(request.query ?? "") relative to base URL: \(baseURL.absoluteString)" + "Invalid request URL from request path: \(path), method: \(method), relative to base URL: \(baseURL.absoluteString)" case .notHTTPResponse(let response): return "Received a non-HTTP response, of type: \(String(describing: type(of: response)))" case .noResponse(let url): diff --git a/Tests/OpenAPIURLSessionTests/URLSessionTransportTests.swift b/Tests/OpenAPIURLSessionTests/URLSessionTransportTests.swift index 25411b8..c06f11d 100644 --- a/Tests/OpenAPIURLSessionTests/URLSessionTransportTests.swift +++ b/Tests/OpenAPIURLSessionTests/URLSessionTransportTests.swift @@ -27,41 +27,54 @@ import Foundation @preconcurrency import class FoundationNetworking.URLSessionConfiguration #endif @testable import OpenAPIURLSession +import HTTPTypes class URLSessionTransportTests: XCTestCase { - func testRequestConversion() throws { - let request = OpenAPIRuntime.Request( - path: "/hello%20world/Maria", - query: "greeting=Howdy", + func testRequestConversion() async throws { + let request = HTTPRequest( method: .post, + scheme: nil, + authority: nil, + path: "/hello%20world/Maria?greeting=Howdy", headerFields: [ - .init(name: "X-Mumble", value: "mumble") - ], - body: Data("👋".utf8) + .init("x-mumble2")!: "mumble" + ] + ) + let body: HTTPBody = "👋" + let urlRequest = try await URLRequest( + request, + body: body, + baseURL: URL(string: "http://example.com/api")! ) - let urlRequest = try URLRequest(request, baseURL: URL(string: "http://example.com/api")!) XCTAssertEqual(urlRequest.url, URL(string: "http://example.com/api/hello%20world/Maria?greeting=Howdy")) XCTAssertEqual(urlRequest.httpMethod, "POST") - XCTAssertEqual(urlRequest.allHTTPHeaderFields, ["X-Mumble": "mumble"]) + XCTAssertEqual(urlRequest.allHTTPHeaderFields?.count, 1) + XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "x-mumble2"), "mumble") XCTAssertEqual(urlRequest.httpBody, Data("👋".utf8)) } - func testResponseConversion() throws { + func testResponseConversion() async throws { let urlResponse: URLResponse = HTTPURLResponse( - url: URL(string: "http://example.com/api/hello/Maria?greeting=Howdy")!, + url: URL(string: "http://example.com/api/hello%20world/Maria?greeting=Howdy")!, statusCode: 201, httpVersion: "HTTP/1.1", - headerFields: ["X-Mumble": "mumble"] + headerFields: ["x-mumble3": "mumble"] )! - let response = try OpenAPIRuntime.Response(from: urlResponse, body: Data("👋".utf8)) - XCTAssertEqual(response.statusCode, 201) - XCTAssertEqual(response.headerFields, [.init(name: "X-Mumble", value: "mumble")]) - XCTAssertEqual(response.body, Data("👋".utf8)) + let (response, maybeResponseBody) = try HTTPResponse.response( + method: .get, + urlResponse: urlResponse, + data: Data("👋".utf8) + ) + let responseBody = try XCTUnwrap(maybeResponseBody) + XCTAssertEqual(response.status.code, 201) + XCTAssertEqual(response.headerFields, [.init("x-mumble3")!: "mumble"]) + let bufferedResponseBody = try await String(collecting: responseBody, upTo: .max) + XCTAssertEqual(bufferedResponseBody, "👋") } func testSend() async throws { - let endpointURL = URL(string: "http://example.com/api/hello/Maria?greeting=Howdy")! + let endpointURL = URL(string: "http://example.com/api/hello%20world/Maria?greeting=Howdy")! MockURLProtocol.mockHTTPResponses.withValue { map in map[endpointURL] = .success( ( @@ -73,21 +86,26 @@ class URLSessionTransportTests: XCTestCase { let transport: any ClientTransport = URLSessionTransport( configuration: .init(session: MockURLProtocol.mockURLSession) ) - let request = OpenAPIRuntime.Request( - path: "/hello/Maria", - query: "greeting=Howdy", + let request = HTTPRequest( method: .post, + scheme: nil, + authority: nil, + path: "/hello%20world/Maria?greeting=Howdy", headerFields: [ - .init(name: "X-Mumble", value: "mumble") + .init("x-mumble1")!: "mumble" ] ) - let response = try await transport.send( + let requestBody: HTTPBody = "👋" + let (response, maybeResponseBody) = try await transport.send( request, + body: requestBody, baseURL: URL(string: "http://example.com/api")!, operationID: "postGreeting" ) - XCTAssertEqual(response.statusCode, 201) - XCTAssertEqual(response.body, Data("👋".utf8)) + let responseBody = try XCTUnwrap(maybeResponseBody) + XCTAssertEqual(response.status.code, 201) + let bufferedResponseBody = try await String(collecting: responseBody, upTo: .max) + XCTAssertEqual(bufferedResponseBody, "👋") } }