Files
swift-openapi-async-http-cl…/Sources/OpenAPIAsyncHTTPClient/AsyncHTTPClientTransport.swift
T

315 lines
10 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import OpenAPIRuntime
import AsyncHTTPClient
import NIOCore
import NIOHTTP1
import NIOFoundationCompat
import HTTPTypes
#if canImport(Darwin)
import Foundation
#else
@preconcurrency import struct Foundation.URL
import struct Foundation.URLComponents
import struct Foundation.Data
import protocol Foundation.LocalizedError
#endif
/// A client transport that performs HTTP operations using the HTTPClient type
/// provided by the AsyncHTTPClient library.
///
/// ### Use the AsyncHTTPClient transport
///
/// Instantiate the transport:
///
/// let transport = AsyncHTTPClientTransport()
///
/// Create the base URL of the server to call using your client. If the server
/// URL was defined in the OpenAPI document, you find a generated method for it
/// on the `Servers` type, for example:
///
/// let serverURL = try Servers.server1()
///
/// Instantiate the `Client` type generated by the Swift OpenAPI Generator for
/// your provided OpenAPI document. For example:
///
/// let client = Client(
/// serverURL: serverURL,
/// transport: transport
/// )
///
/// Use the client to make HTTP calls defined in your OpenAPI document. For
/// example, if the OpenAPI document contains an HTTP operation with
/// the identifier `checkHealth`, call it from Swift with:
///
/// let response = try await client.checkHealth(.init())
/// // ...
///
/// ### Provide a custom Client
///
/// The ``AsyncHTTPClientTransport/Configuration-swift.struct`` type allows you
/// to provide a custom `HTTPClient` and tweak behaviors such as the default
/// timeout.
public struct AsyncHTTPClientTransport: ClientTransport {
/// A set of configuration values for the AsyncHTTPClient transport.
public struct Configuration: Sendable {
/// The HTTP client used for performing HTTP calls.
public var client: HTTPClient
/// The default shared HTTP client.
///
/// This is a workaround for the lack of a shared client
/// in AsyncHTTPClient. Do not use this value directly, outside of
/// the `Configuration.init(client:timeout:)` initializer, as it will
/// likely be removed in the future.
private static let sharedClient: HTTPClient = .init()
/// The default request timeout.
public var timeout: TimeAmount
/// Creates a new configuration with the specified client and timeout.
/// - Parameters:
/// - client: The underlying client used to perform HTTP operations.
/// Provide nil to use the shared internal client.
/// - timeout: The request timeout, defaults to 1 minute.
public init(client: HTTPClient? = nil, timeout: TimeAmount = .minutes(1)) {
self.client = client ?? Self.sharedClient
self.timeout = timeout
}
}
/// A request to be sent by the transport.
internal typealias Request = HTTPClientRequest
/// A response returned by the transport.
internal typealias Response = HTTPClientResponse
/// Specialized error thrown by the transport.
internal enum Error: Swift.Error, CustomStringConvertible, LocalizedError {
/// Invalid URL composed from base URL and received request.
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 ?? "<nil>") relative to base URL: \(baseURL.absoluteString)"
}
}
// MARK: LocalizedError
var errorDescription: String? {
description
}
}
/// A set of configuration values used by the transport.
public var configuration: Configuration
/// Underlying request sender for the transport.
internal let requestSender: any HTTPRequestSending
/// Creates a new transport.
/// - Parameters:
/// - configuration: A set of configuration values used by the transport.
/// - requestSender: The underlying request sender.
internal init(
configuration: Configuration,
requestSender: any HTTPRequestSending
) {
self.configuration = configuration
self.requestSender = requestSender
}
/// Creates a new transport.
/// - Parameter configuration: A set of configuration values used by the transport.
public init(configuration: Configuration) {
self.init(
configuration: configuration,
requestSender: AsyncHTTPRequestSender()
)
}
// MARK: ClientTransport
/// Sends an HTTP request and returns the corresponding HTTP response.
///
/// - Parameters:
/// - request: The HTTP request to send.
/// - body: The HTTP body to include in the request (optional).
/// - baseURL: The base URL for the request.
/// - operationID: The identifier for the operation.
///
/// - Returns: A tuple containing the HTTP response and an optional HTTP body in the response.
/// - Throws: An error if the request or response handling encounters any issues.
public func send(
_ request: HTTPRequest,
body: HTTPBody?,
baseURL: URL,
operationID: String
) 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(
method: request.method,
httpResponse: httpResponse
)
return response
}
// MARK: Internal
/// Converts the shared Request type into URLRequest.
internal static func convertRequest(
_ request: HTTPRequest,
body: HTTPBody?,
baseURL: URL
) throws -> HTTPClientRequest {
guard
var baseUrlComponents = URLComponents(string: baseURL.absoluteString),
let requestUrlComponents = URLComponents(string: request.path ?? "")
else {
throw Error.invalidRequestURL(request: request, baseURL: baseURL)
}
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.canonicalName, value: header.value)
}
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(
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, body)
}
// MARK: Private
/// Makes the underlying HTTP call.
private func invokeSession(with request: Request) async throws -> Response {
try await requestSender.send(
request: request,
with: configuration.client,
timeout: configuration.timeout
)
}
}
extension HTTPTypes.HTTPRequest.Method {
var asHTTPMethod: NIOHTTP1.HTTPMethod {
switch self {
case .get:
return .GET
case .put:
return .PUT
case .post:
return .POST
case .delete:
return .DELETE
case .options:
return .OPTIONS
case .head:
return .HEAD
case .patch:
return .PATCH
case .trace:
return .TRACE
default:
return .RAW(value: rawValue)
}
}
}
/// A type that performs HTTP operations using the HTTP client.
internal protocol HTTPRequestSending: Sendable {
func send(
request: AsyncHTTPClientTransport.Request,
with client: HTTPClient,
timeout: TimeAmount
) async throws -> AsyncHTTPClientTransport.Response
}
/// Performs HTTP calls using AsyncHTTPClient
internal struct AsyncHTTPRequestSender: HTTPRequestSending {
func send(
request: AsyncHTTPClientTransport.Request,
with client: AsyncHTTPClient.HTTPClient,
timeout: TimeAmount
) async throws -> AsyncHTTPClientTransport.Response {
try await client.execute(
request,
timeout: timeout
)
}
}