Skip to content

Commit 3726c09

Browse files
committed
chore: HTTP layer from OpenAPIRuntime
1 parent 1fc2013 commit 3726c09

19 files changed

+4698
-1
lines changed

Package.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ let package = Package(
2626
dependencies: [
2727
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"4.0.0"),
2828
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.3.0"),
29+
.package(url: "https://github.com/apple/swift-log", from: "1.0.0"),
30+
.package(url: "https://github.com/apple/swift-collections", from: "1.0.0"),
2931
.package(url: "https://github.com/pointfreeco/swift-clocks", from: "1.0.0"),
3032
.package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.1.0"),
3133
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"),
@@ -39,6 +41,9 @@ let package = Package(
3941
dependencies: [
4042
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
4143
.product(name: "HTTPTypes", package: "swift-http-types"),
44+
.product(name: "HTTPTypesFoundation", package: "swift-http-types"),
45+
.product(name: "Logging", package: "swift-log"),
46+
.product(name: "DequeModule", package: "swift-collections"),
4247
.product(name: "Clocks", package: "swift-clocks"),
4348
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
4449
]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
/// A helper protocol for customizing descriptions.
16+
internal protocol PrettyStringConvertible {
17+
18+
/// A pretty string description.
19+
var prettyDescription: String { get }
20+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import HTTPTypes
16+
17+
import protocol Foundation.LocalizedError
18+
19+
#if canImport(Darwin)
20+
import struct Foundation.URL
21+
#else
22+
@preconcurrency import struct Foundation.URL
23+
#endif
24+
25+
/// An error thrown by a client performing an OpenAPI operation.
26+
///
27+
/// Use a `ClientError` to inspect details about the request and response
28+
/// that resulted in an error.
29+
///
30+
/// You don't create or throw instances of `ClientError` yourself; they are
31+
/// created and thrown on your behalf by the runtime library when a client
32+
/// operation fails.
33+
struct ClientError: Error {
34+
/// The HTTP request created during the operation.
35+
///
36+
/// Will be nil if the error resulted before the request was generated,
37+
/// for example if generating the request from the Input failed.
38+
var request: HTTPTypes.HTTPRequest?
39+
40+
/// The HTTP request body created during the operation.
41+
///
42+
/// Will be nil if the error resulted before the request was generated,
43+
/// for example if generating the request from the Input failed.
44+
var requestBody: HTTPBody?
45+
46+
/// The base URL for HTTP requests.
47+
///
48+
/// Will be nil if the error resulted before the request was generated,
49+
/// for example if generating the request from the Input failed.
50+
var baseURL: URL?
51+
52+
/// The HTTP response received during the operation.
53+
///
54+
/// Will be nil if the error resulted before the response was received.
55+
var response: HTTPTypes.HTTPResponse?
56+
57+
/// The HTTP response body received during the operation.
58+
///
59+
/// Will be nil if the error resulted before the response was received.
60+
var responseBody: HTTPBody?
61+
62+
/// A user-facing description of what caused the underlying error
63+
/// to be thrown.
64+
var causeDescription: String
65+
66+
/// The underlying error that caused the operation to fail.
67+
var underlyingError: any Error
68+
69+
/// Creates a new error.
70+
/// - Parameters:
71+
/// - request: The HTTP request created during the operation.
72+
/// - requestBody: The HTTP request body created during the operation.
73+
/// - baseURL: The base URL for HTTP requests.
74+
/// - response: The HTTP response received during the operation.
75+
/// - responseBody: The HTTP response body received during the operation.
76+
/// - causeDescription: A user-facing description of what caused
77+
/// the underlying error to be thrown.
78+
/// - underlyingError: The underlying error that caused the operation
79+
/// to fail.
80+
init(
81+
request: HTTPTypes.HTTPRequest? = nil,
82+
requestBody: HTTPBody? = nil,
83+
baseURL: URL? = nil,
84+
response: HTTPTypes.HTTPResponse? = nil,
85+
responseBody: HTTPBody? = nil,
86+
causeDescription: String,
87+
underlyingError: any Error
88+
) {
89+
self.request = request
90+
self.requestBody = requestBody
91+
self.baseURL = baseURL
92+
self.response = response
93+
self.responseBody = responseBody
94+
self.causeDescription = causeDescription
95+
self.underlyingError = underlyingError
96+
}
97+
98+
// MARK: Private
99+
100+
fileprivate var underlyingErrorDescription: String {
101+
guard let prettyError = underlyingError as? (any PrettyStringConvertible) else {
102+
return "\(underlyingError)"
103+
}
104+
return prettyError.prettyDescription
105+
}
106+
}
107+
108+
extension ClientError: CustomStringConvertible {
109+
/// A human-readable description of the client error.
110+
///
111+
/// This computed property returns a string that includes information about the client error.
112+
///
113+
/// - Returns: A string describing the client error and its associated details.
114+
var description: String {
115+
"Client error - cause description: '\(causeDescription)', underlying error: \(underlyingErrorDescription), request: \(request?.prettyDescription ?? "<nil>"), requestBody: \(requestBody?.prettyDescription ?? "<nil>"), baseURL: \(baseURL?.absoluteString ?? "<nil>"), response: \(response?.prettyDescription ?? "<nil>"), responseBody: \(responseBody?.prettyDescription ?? "<nil>")"
116+
}
117+
}
118+
119+
extension ClientError: LocalizedError {
120+
/// A localized description of the client error.
121+
///
122+
/// This computed property provides a localized human-readable description of the client error, which is suitable for displaying to users.
123+
///
124+
/// - Returns: A localized string describing the client error.
125+
var errorDescription: String? {
126+
"Client encountered an error, caused by \"\(causeDescription)\", underlying error: \(underlyingError.localizedDescription)."
127+
}
128+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import HTTPTypes
2+
3+
import struct Foundation.Data
4+
//===----------------------------------------------------------------------===//
5+
//
6+
// This source file is part of the SwiftOpenAPIGenerator open source project
7+
//
8+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
9+
// Licensed under Apache License v2.0
10+
//
11+
// See LICENSE.txt for license information
12+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
13+
//
14+
// SPDX-License-Identifier: Apache-2.0
15+
//
16+
//===----------------------------------------------------------------------===//
17+
import protocol Foundation.LocalizedError
18+
19+
/// Error thrown by generated code.
20+
internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, PrettyStringConvertible
21+
{
22+
23+
// Transport/Handler
24+
case transportFailed(any Error)
25+
case middlewareFailed(middlewareType: Any.Type, any Error)
26+
27+
/// A wrapped root cause error, if one was thrown by other code.
28+
var underlyingError: (any Error)? {
29+
switch self {
30+
case .transportFailed(let error), .middlewareFailed(_, let error):
31+
return error
32+
}
33+
}
34+
35+
// MARK: CustomStringConvertible
36+
37+
var description: String { prettyDescription }
38+
39+
var prettyDescription: String {
40+
switch self {
41+
case .transportFailed: return "Transport threw an error."
42+
case .middlewareFailed(middlewareType: let type, _):
43+
return "Middleware of type '\(type)' threw an error."
44+
}
45+
}
46+
47+
// MARK: - LocalizedError
48+
49+
var errorDescription: String? { description }
50+
}
51+
52+
/// HTTP Response status definition for ``RuntimeError``.
53+
extension RuntimeError: HTTPResponseConvertible {
54+
/// HTTP Status code corresponding to each error case
55+
var httpStatus: HTTPTypes.HTTPResponse.Status {
56+
switch self {
57+
case .middlewareFailed, .transportFailed:
58+
.internalServerError
59+
}
60+
}
61+
}
62+
63+
/// A value that can be converted to an HTTP response and body.
64+
///
65+
/// Conform your error type to this protocol to convert it to an `HTTPResponse` and ``HTTPBody``.
66+
///
67+
/// Used by ``ErrorHandlingMiddleware``.
68+
protocol HTTPResponseConvertible {
69+
70+
/// An HTTP status to return in the response.
71+
var httpStatus: HTTPTypes.HTTPResponse.Status { get }
72+
73+
/// The HTTP header fields of the response.
74+
/// This is optional as default values are provided in the extension.
75+
var httpHeaderFields: HTTPTypes.HTTPFields { get }
76+
77+
/// The body of the HTTP response.
78+
var httpBody: HTTPBody? { get }
79+
}
80+
81+
extension HTTPResponseConvertible {
82+
83+
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
84+
var httpHeaderFields: HTTPTypes.HTTPFields { [:] }
85+
86+
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
87+
var httpBody: HTTPBody? { nil }
88+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@_exported import HTTPTypes
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
/// Describes how many times the provided sequence can be iterated.
16+
public enum IterationBehavior: Sendable {
17+
18+
/// The input sequence can only be iterated once.
19+
///
20+
/// If a retry or a redirect is encountered, fail the call with
21+
/// a descriptive error.
22+
case single
23+
24+
/// The input sequence can be iterated multiple times.
25+
///
26+
/// Supports retries and redirects, as a new iterator is created each
27+
/// time.
28+
case multiple
29+
}
30+
31+
// MARK: - Internal
32+
33+
/// A type-erasing closure-based iterator.
34+
@usableFromInline struct AnyIterator<Element: Sendable>: AsyncIteratorProtocol {
35+
36+
/// The closure that produces the next element.
37+
private let produceNext: () async throws -> Element?
38+
39+
/// Creates a new type-erased iterator from the provided iterator.
40+
/// - Parameter iterator: The iterator to type-erase.
41+
@usableFromInline init<Iterator: AsyncIteratorProtocol>(_ iterator: Iterator)
42+
where Iterator.Element == Element {
43+
var iterator = iterator
44+
self.produceNext = { try await iterator.next() }
45+
}
46+
47+
/// Advances the iterator to the next element and returns it asynchronously.
48+
///
49+
/// - Returns: The next element in the sequence, or `nil` if there are no more elements.
50+
/// - Throws: An error if there is an issue advancing the iterator or retrieving the next element.
51+
public mutating func next() async throws -> Element? { try await produceNext() }
52+
}
53+
54+
/// A type-erased async sequence that wraps input sequences.
55+
@usableFromInline struct AnySequence<Element: Sendable>: AsyncSequence, Sendable {
56+
57+
/// The type of the type-erased iterator.
58+
@usableFromInline typealias AsyncIterator = AnyIterator<Element>
59+
60+
/// A closure that produces a new iterator.
61+
@usableFromInline let produceIterator: @Sendable () -> AsyncIterator
62+
63+
/// Creates a new sequence.
64+
/// - Parameter sequence: The input sequence to type-erase.
65+
@usableFromInline init<Upstream: AsyncSequence>(_ sequence: Upstream)
66+
where Upstream.Element == Element, Upstream: Sendable {
67+
self.produceIterator = { .init(sequence.makeAsyncIterator()) }
68+
}
69+
70+
@usableFromInline func makeAsyncIterator() -> AsyncIterator { produceIterator() }
71+
}
72+
73+
/// An async sequence wrapper for a sync sequence.
74+
@usableFromInline struct WrappedSyncSequence<Upstream: Sequence & Sendable>: AsyncSequence, Sendable
75+
where Upstream.Element: Sendable {
76+
77+
/// The type of the iterator.
78+
@usableFromInline typealias AsyncIterator = Iterator<Element>
79+
80+
/// The element type.
81+
@usableFromInline typealias Element = Upstream.Element
82+
83+
/// An iterator type that wraps a sync sequence iterator.
84+
@usableFromInline struct Iterator<IteratorElement: Sendable>: AsyncIteratorProtocol {
85+
86+
/// The element type.
87+
@usableFromInline typealias Element = IteratorElement
88+
89+
/// The underlying sync sequence iterator.
90+
var iterator: any IteratorProtocol<Element>
91+
92+
@usableFromInline mutating func next() async throws -> IteratorElement? { iterator.next() }
93+
}
94+
95+
/// The underlying sync sequence.
96+
@usableFromInline let sequence: Upstream
97+
98+
/// Creates a new async sequence with the provided sync sequence.
99+
/// - Parameter sequence: The sync sequence to wrap.
100+
@usableFromInline init(sequence: Upstream) { self.sequence = sequence }
101+
102+
@usableFromInline func makeAsyncIterator() -> AsyncIterator {
103+
Iterator(iterator: sequence.makeIterator())
104+
}
105+
}
106+
107+
/// An empty async sequence.
108+
@usableFromInline struct EmptySequence<Element: Sendable>: AsyncSequence, Sendable {
109+
110+
/// The type of the empty iterator.
111+
@usableFromInline typealias AsyncIterator = EmptyIterator<Element>
112+
113+
/// An async iterator of an empty sequence.
114+
@usableFromInline struct EmptyIterator<IteratorElement: Sendable>: AsyncIteratorProtocol {
115+
116+
@usableFromInline mutating func next() async throws -> IteratorElement? { nil }
117+
}
118+
119+
/// Creates a new empty async sequence.
120+
@usableFromInline init() {}
121+
122+
@usableFromInline func makeAsyncIterator() -> AsyncIterator { EmptyIterator() }
123+
}

0 commit comments

Comments
 (0)