Skip to content

[AHC Transport] Async bodies + swift-http-types adoption #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Sep 27, 2023
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
),
```

Expand Down
93 changes: 67 additions & 26 deletions Sources/OpenAPIAsyncHTTPClient/AsyncHTTPClientTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import AsyncHTTPClient
import NIOCore
import NIOHTTP1
import NIOFoundationCompat
import HTTPTypes
#if canImport(Darwin)
import Foundation
#else
Expand Down Expand Up @@ -100,15 +101,15 @@ 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

var description: String {
switch self {
case let .invalidRequestURL(request: request, baseURL: baseURL):
return
"Invalid request URL from request path: \(request.path), query: \(request.query ?? "<nil>") relative to base URL: \(baseURL.absoluteString)"
"Invalid request URL from request path: \(request.path ?? "<nil>") relative to base URL: \(baseURL.absoluteString)"
}
}

Expand Down Expand Up @@ -150,57 +151,97 @@ 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
}

// MARK: Internal

/// 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
Expand All @@ -215,7 +256,7 @@ public struct AsyncHTTPClientTransport: ClientTransport {
}
}

extension OpenAPIRuntime.HTTPMethod {
extension HTTPTypes.HTTPRequest.Method {
var asHTTPMethod: NIOHTTP1.HTTPMethod {
switch self {
case .get:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
),
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import NIOCore
import NIOPosix
import AsyncHTTPClient
@testable import OpenAPIAsyncHTTPClient
import HTTPTypes

class Test_AsyncHTTPClientTransport: XCTestCase {

Expand All @@ -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")
Expand All @@ -70,35 +73,46 @@ 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 {
let transport = AsyncHTTPClientTransport(
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)
}
}

Expand Down