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
Next Next commit
[WIP] [AHC Transport] Async bodies + swift-http-types adoption
  • Loading branch information
czechboy0 committed Sep 7, 2023
commit 5718552c2f81af7c8a3375ed52bae08e62b14c9e
3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ let package = Package(
.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-docc-plugin", from: "1.0.0"),
// .package(url: "https://github.com/guoye-zhang/swift-nio-extras", branch: "http-types"),
],
targets: [
.target(
Expand All @@ -50,6 +51,8 @@ let package = Package(
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
.product(name: "AsyncHTTPClient", package: "async-http-client"),
.product(name: "NIOFoundationCompat", package: "swift-nio"),
// .product(name: "NIOHTTPTypes", package: "swift-nio-extras"),
// .product(name: "NIOHTTPTypesHTTP1", package: "swift-nio-extras"),
],
swiftSettings: swiftSettings
),
Expand Down
73 changes: 50 additions & 23 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 @@ -91,15 +92,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 @@ -141,11 +142,12 @@ 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)
return response
Expand All @@ -155,43 +157,68 @@ 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 {
throw Error.invalidRequestURL(request: request, baseURL: baseURL)
}
baseUrlComponents.percentEncodedPath += request.path
baseUrlComponents.percentEncodedQuery = request.query
baseUrlComponents.percentEncodedPath += request.soar_pathOnly
baseUrlComponents.percentEncodedQuery = request.soar_query.map(String.init)
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
) 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(
sequence: httpResponse.body.map { HTTPBody.ByteChunk($0.readableBytesView) },
length: length,
iterationBehavior: .single
)
return response
let response = HTTPResponse(
status: .init(code: Int(httpResponse.status.code)),
headerFields: headerFields
)
return (response, body)
}

// MARK: Private
Expand All @@ -206,7 +233,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 @@ -17,6 +17,7 @@ import NIOCore
import NIOPosix
import AsyncHTTPClient
@testable import OpenAPIAsyncHTTPClient
import HTTPTypes

class Test_AsyncHTTPClientTransport: XCTestCase {

Expand All @@ -37,17 +38,17 @@ class Test_AsyncHTTPClientTransport: XCTestCase {
}

func testConvertRequest() throws {
let request: OpenAPIRuntime.Request = .init(
path: "/hello%20world/Maria",
query: "greeting=Howdy",
let request: HTTPRequest = .init(
soar_path: "/hello%20world/Maria?greeting=Howdy",
method: .post,
headerFields: [
.init(name: "content-type", value: "application/json")
],
body: try Self.testData
.contentType: "application/json"
]
)
let requestBody = try HTTPBody(data: 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,43 +71,43 @@ class Test_AsyncHTTPClientTransport: XCTestCase {
],
body: .bytes(Self.testBuffer)
)
let response = try await AsyncHTTPClientTransport.convertResponse(httpResponse)
XCTAssertEqual(response.statusCode, 200)
let (response, responseBody) = try await AsyncHTTPClientTransport.convertResponse(httpResponse)
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 responseBody.collectAsData(upTo: .max)
XCTAssertEqual(bufferedResponseBody, try Self.testData)
}

func testSend() async throws {
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let httpClient = HTTPClient(
eventLoopGroupProvider: .shared(eventLoopGroup),
configuration: .init()
)
let httpClient = HTTPClient()
defer {
try! httpClient.syncShutdown()
}
let transport = AsyncHTTPClientTransport(
configuration: .init(client: httpClient),
requestSender: TestSender.test
)
let request: OpenAPIRuntime.Request = .init(
path: "/api/v1/hello/Maria",
let request: HTTPRequest = .init(
soar_path: "/api/v1/hello/Maria",
method: .get,
headerFields: [
.init(name: "x-request", value: "yes")
.init("x-request")!: "yes"
]
)
let response = try await transport.send(
let (response, responseBody) = try await transport.send(
request,
body: nil,
baseURL: Self.testUrl,
operationID: "sayHello"
)
XCTAssertEqual(response.statusCode, 200)
let bufferedResponseBody = try await responseBody.collectAsString(upTo: .max)
XCTAssertEqual(bufferedResponseBody, "[{}]")
XCTAssertEqual(response.status.code, 200)
}
}

Expand Down