diff --git a/Package.swift b/Package.swift index b7d99bb..2c1c322 100644 --- a/Package.swift +++ b/Package.swift @@ -40,7 +40,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-nio", from: "2.58.0"), .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.19.0"), - .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 b9d442c..666c32b 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/swift-server/swift-openapi-async-http-client", - .upToNextMinor(from: "0.2.0") + .upToNextMinor(from: "0.3.0") ), ``` diff --git a/Sources/OpenAPIAsyncHTTPClient/AsyncHTTPClientTransport.swift b/Sources/OpenAPIAsyncHTTPClient/AsyncHTTPClientTransport.swift index 08d11b2..26efd0d 100644 --- a/Sources/OpenAPIAsyncHTTPClient/AsyncHTTPClientTransport.swift +++ b/Sources/OpenAPIAsyncHTTPClient/AsyncHTTPClientTransport.swift @@ -16,6 +16,7 @@ import AsyncHTTPClient import NIOCore import NIOHTTP1 import NIOFoundationCompat +import HTTPTypes #if canImport(Darwin) import Foundation #else @@ -100,7 +101,7 @@ public struct AsyncHTTPClientTransport: ClientTransport { internal enum Error: Swift.Error, CustomStringConvertible, LocalizedError { /// Invalid URL composed from base URL and received request. - case invalidRequestURL(request: OpenAPIRuntime.Request, baseURL: URL) + case invalidRequestURL(request: HTTPRequest, baseURL: URL) // MARK: CustomStringConvertible @@ -108,7 +109,7 @@ public struct AsyncHTTPClientTransport: ClientTransport { switch self { case let .invalidRequestURL(request: request, 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: \(request.path ?? "") relative to base URL: \(baseURL.absoluteString)" } } @@ -150,13 +151,17 @@ public struct AsyncHTTPClientTransport: ClientTransport { // MARK: ClientTransport public func send( - _ request: OpenAPIRuntime.Request, + _ request: HTTPRequest, + body: HTTPBody?, baseURL: URL, operationID: String - ) async throws -> OpenAPIRuntime.Response { - let httpRequest = try Self.convertRequest(request, baseURL: baseURL) + ) async throws -> (HTTPResponse, HTTPBody?) { + let httpRequest = try Self.convertRequest(request, body: body, baseURL: baseURL) let httpResponse = try await invokeSession(with: httpRequest) - let response = try await Self.convertResponse(httpResponse) + let response = try await Self.convertResponse( + method: request.method, + httpResponse: httpResponse + ) return response } @@ -164,43 +169,79 @@ public struct AsyncHTTPClientTransport: ClientTransport { /// Converts the shared Request type into URLRequest. internal static func convertRequest( - _ request: OpenAPIRuntime.Request, + _ request: HTTPRequest, + body: HTTPBody?, baseURL: URL ) throws -> HTTPClientRequest { - guard var baseUrlComponents = URLComponents(string: baseURL.absoluteString) else { + guard + var baseUrlComponents = URLComponents(string: baseURL.absoluteString), + let requestUrlComponents = URLComponents(string: request.path ?? "") + else { throw Error.invalidRequestURL(request: request, baseURL: baseURL) } - baseUrlComponents.percentEncodedPath += request.path - baseUrlComponents.percentEncodedQuery = request.query + baseUrlComponents.percentEncodedPath += requestUrlComponents.percentEncodedPath + baseUrlComponents.percentEncodedQuery = requestUrlComponents.percentEncodedQuery guard let url = baseUrlComponents.url else { throw Error.invalidRequestURL(request: request, baseURL: baseURL) } var clientRequest = HTTPClientRequest(url: url.absoluteString) clientRequest.method = request.method.asHTTPMethod for header in request.headerFields { - clientRequest.headers.add(name: header.name.lowercased(), value: header.value) + clientRequest.headers.add(name: header.name.canonicalName, value: header.value) } - if let body = request.body { - clientRequest.body = .bytes(body) + if let body { + let length: HTTPClientRequest.Body.Length + switch body.length { + case .unknown: + length = .unknown + case .known(let count): + length = .known(count) + } + clientRequest.body = .stream( + body.map { .init(bytes: $0) }, + length: length + ) } return clientRequest } /// Converts the received URLResponse into the shared Response. internal static func convertResponse( - _ httpResponse: HTTPClientResponse - ) async throws -> OpenAPIRuntime.Response { - let headerFields: [OpenAPIRuntime.HeaderField] = httpResponse - .headers - .map { .init(name: $0, value: $1) } - let body = try await httpResponse.body.collect(upTo: .max) - let bodyData = Data(buffer: body, byteTransferStrategy: .noCopy) - let response = OpenAPIRuntime.Response( - statusCode: Int(httpResponse.status.code), - headerFields: headerFields, - body: bodyData + method: HTTPRequest.Method, + httpResponse: HTTPClientResponse + ) async throws -> (HTTPResponse, HTTPBody?) { + + var headerFields: HTTPFields = [:] + for header in httpResponse.headers { + headerFields[.init(header.name)!] = header.value + } + + let length: HTTPBody.Length + if let lengthHeaderString = headerFields[.contentLength], + let lengthHeader = Int(lengthHeaderString) + { + length = .known(lengthHeader) + } else { + length = .unknown + } + + let body: HTTPBody? + switch method { + case .head, .connect, .trace: + body = nil + default: + body = HTTPBody( + httpResponse.body.map { $0.readableBytesView }, + length: length, + iterationBehavior: .single + ) + } + + let response = HTTPResponse( + status: .init(code: Int(httpResponse.status.code)), + headerFields: headerFields ) - return response + return (response, body) } // MARK: Private @@ -215,7 +256,7 @@ public struct AsyncHTTPClientTransport: ClientTransport { } } -extension OpenAPIRuntime.HTTPMethod { +extension HTTPTypes.HTTPRequest.Method { var asHTTPMethod: NIOHTTP1.HTTPMethod { switch self { case .get: diff --git a/Sources/OpenAPIAsyncHTTPClient/Documentation.docc/Documentation.md b/Sources/OpenAPIAsyncHTTPClient/Documentation.docc/Documentation.md index 054a552..25d9032 100644 --- a/Sources/OpenAPIAsyncHTTPClient/Documentation.docc/Documentation.md +++ b/Sources/OpenAPIAsyncHTTPClient/Documentation.docc/Documentation.md @@ -20,7 +20,7 @@ Add the package dependency in your `Package.swift`: ```swift .package( url: "https://github.com/swift-server/swift-openapi-async-http-client", - .upToNextMinor(from: "0.2.0") + .upToNextMinor(from: "0.3.0") ), ``` diff --git a/Tests/OpenAPIAsyncHTTPClientTests/Test_AsyncHTTPClientTransport.swift b/Tests/OpenAPIAsyncHTTPClientTests/Test_AsyncHTTPClientTransport.swift index 594a828..e899261 100644 --- a/Tests/OpenAPIAsyncHTTPClientTests/Test_AsyncHTTPClientTransport.swift +++ b/Tests/OpenAPIAsyncHTTPClientTests/Test_AsyncHTTPClientTransport.swift @@ -17,6 +17,7 @@ import NIOCore import NIOPosix import AsyncHTTPClient @testable import OpenAPIAsyncHTTPClient +import HTTPTypes class Test_AsyncHTTPClientTransport: XCTestCase { @@ -37,17 +38,19 @@ class Test_AsyncHTTPClientTransport: XCTestCase { } func testConvertRequest() throws { - let request: OpenAPIRuntime.Request = .init( - path: "/hello%20world/Maria", - query: "greeting=Howdy", + let request: HTTPRequest = .init( method: .post, + scheme: nil, + authority: nil, + path: "/hello%20world/Maria?greeting=Howdy", headerFields: [ - .init(name: "content-type", value: "application/json") - ], - body: try Self.testData + .contentType: "application/json" + ] ) + let requestBody = try HTTPBody(Self.testData) let httpRequest = try AsyncHTTPClientTransport.convertRequest( request, + body: requestBody, baseURL: try XCTUnwrap(URL(string: "http://example.com/api/v1")) ) XCTAssertEqual(httpRequest.url, "http://example.com/api/v1/hello%20world/Maria?greeting=Howdy") @@ -70,15 +73,20 @@ class Test_AsyncHTTPClientTransport: XCTestCase { ], body: .bytes(Self.testBuffer) ) - let response = try await AsyncHTTPClientTransport.convertResponse(httpResponse) - XCTAssertEqual(response.statusCode, 200) + let (response, maybeResponseBody) = try await AsyncHTTPClientTransport.convertResponse( + method: .get, + httpResponse: httpResponse + ) + let responseBody = try XCTUnwrap(maybeResponseBody) + XCTAssertEqual(response.status.code, 200) XCTAssertEqual( response.headerFields, [ - .init(name: "content-type", value: "application/json") + .contentType: "application/json" ] ) - XCTAssertEqual(response.body, try Self.testData) + let bufferedResponseBody = try await Data(collecting: responseBody, upTo: .max) + XCTAssertEqual(bufferedResponseBody, try Self.testData) } func testSend() async throws { @@ -86,19 +94,25 @@ class Test_AsyncHTTPClientTransport: XCTestCase { configuration: .init(), requestSender: TestSender.test ) - let request: OpenAPIRuntime.Request = .init( - path: "/api/v1/hello/Maria", + let request: HTTPRequest = .init( method: .get, + scheme: nil, + authority: nil, + path: "/api/v1/hello/Maria", headerFields: [ - .init(name: "x-request", value: "yes") + .init("x-request")!: "yes" ] ) - let response = try await transport.send( + let (response, maybeResponseBody) = try await transport.send( request, + body: nil, baseURL: Self.testUrl, operationID: "sayHello" ) - XCTAssertEqual(response.statusCode, 200) + let responseBody = try XCTUnwrap(maybeResponseBody) + let bufferedResponseBody = try await String(collecting: responseBody, upTo: .max) + XCTAssertEqual(bufferedResponseBody, "[{}]") + XCTAssertEqual(response.status.code, 200) } }