Files
swift-openapi-async-http-cl…/Sources/OpenAPIAsyncHTTPClient/AsyncHTTPClientTransport.swift
T
Honza Dvorsky abfe558a66 Prep 1.0 (#37)
### Motivation

Prep 1.0 - docs.

### Modifications

See above.

### Result

Updated for 1.0.

### Test Plan

Previewed locally.
2023-12-11 05:04:24 -08:00

247 lines
9.5 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()
///
/// Instantiate the `Client` type generated by the Swift OpenAPI Generator for
/// your provided OpenAPI document. For example:
///
/// let client = Client(
/// serverURL: URL(string: "https://example.com")!,
/// 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()
///
/// ### 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 = .init()) {
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):
if let intValue = Int(exactly: count) { length = .known(intValue) } else { length = .unknown }
}
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 = Int64(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) }
}