Skip to content
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

[Runtime] Async bodies + swift-http-types adoption #47

Merged
merged 60 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
fd47bd2
[WIP] Async body currency type
czechboy0 Sep 1, 2023
beca630
Rename Body to HTTPBody to avoid confusion with the generated Body enums
czechboy0 Sep 4, 2023
34f7c59
[WIP] Adopt swift-http-types
czechboy0 Sep 4, 2023
d046bf9
WIP
czechboy0 Sep 4, 2023
164f84e
wip
czechboy0 Sep 5, 2023
3db782e
Remove mapChunks, rename initializer parameters
czechboy0 Sep 5, 2023
6c97d48
wip
czechboy0 Sep 5, 2023
466b701
Remove String coder, fix up client unit tests
czechboy0 Sep 5, 2023
00ec189
Got the other tests working
czechboy0 Sep 5, 2023
fe08164
Just use Substring in more places
czechboy0 Sep 5, 2023
63f05d6
wip
czechboy0 Sep 6, 2023
982fe13
wip
czechboy0 Sep 6, 2023
1317631
Added more docs
czechboy0 Sep 6, 2023
faba5ce
Add back docs
czechboy0 Sep 6, 2023
6a71613
Improve loggability of the currency types
czechboy0 Sep 7, 2023
1c07795
Bring back comments on ServerError
czechboy0 Sep 7, 2023
2540b77
WIP
czechboy0 Sep 7, 2023
a165558
Add comments back
czechboy0 Sep 7, 2023
c4186f8
Prefix extensions on types we don't own
czechboy0 Sep 7, 2023
5e1d21e
WIP on documentation
czechboy0 Sep 7, 2023
0ee065f
Add docs for HTTPBody
czechboy0 Sep 7, 2023
99d8252
Add doc comments
czechboy0 Sep 7, 2023
d85eb55
More docs
czechboy0 Sep 7, 2023
844be3a
Merge branch 'main' into hd-adopt-http-types
czechboy0 Sep 7, 2023
1406537
Fixes
czechboy0 Sep 7, 2023
84361a8
Make iterator next() method mutating
czechboy0 Sep 8, 2023
9f10325
Represent no responses as a nil HTTPBody in server transport and midd…
czechboy0 Sep 8, 2023
dbda450
Feedback: make the AsyncSequences Sendable, which adds the requiremen…
czechboy0 Sep 12, 2023
fbe8eea
Feedback: further cleanup of the HTTPBody API
czechboy0 Sep 12, 2023
8b432b2
Remove unnecessary initializers
czechboy0 Sep 12, 2023
3f59f44
Review feedback:
czechboy0 Sep 13, 2023
16917ce
Review feedback: make response body optional
czechboy0 Sep 13, 2023
4830821
A few more changes
czechboy0 Sep 13, 2023
213addf
Wording and a refactor fix
czechboy0 Sep 14, 2023
a5e22b6
Merge branch 'main' into hd-adopt-http-types
czechboy0 Sep 18, 2023
c314914
Fix a few more missing sendable annotations
czechboy0 Sep 18, 2023
0cf76f2
Update docs
czechboy0 Sep 18, 2023
af10f7d
Update HTTPBody.swift
czechboy0 Sep 18, 2023
b2fb79e
Update HTTPBody.swift
czechboy0 Sep 18, 2023
1778209
Update HTTPBody.swift
czechboy0 Sep 18, 2023
b58a3b6
Update HTTPBody.swift
czechboy0 Sep 18, 2023
b2d4ec3
Update HTTPBody.swift
czechboy0 Sep 18, 2023
66b602f
Update HTTPBody.swift
czechboy0 Sep 18, 2023
b0ccaae
Update ServerTransport.swift
czechboy0 Sep 18, 2023
3f5b3d3
Merge remote-tracking branch 'apple/main' into hd-adopt-http-types
czechboy0 Sep 25, 2023
a129b6b
Fixing up the merge from main
czechboy0 Sep 25, 2023
a3516b7
Merge branch 'hd-adopt-http-types' of github.com:czechboy0/swift-open…
czechboy0 Sep 25, 2023
f5d8c1c
Merge branch 'main' into hd-adopt-http-types
czechboy0 Sep 25, 2023
a89bced
Use swift-http-types 1.0
czechboy0 Sep 25, 2023
b19a787
Provide a closure for accessing the locked value HTTPBody.iteratorCre…
czechboy0 Sep 25, 2023
481e6bb
Fix up encoding comment
czechboy0 Sep 25, 2023
dc209d3
Update Sources/OpenAPIRuntime/Interface/HTTPBody.swift
czechboy0 Sep 25, 2023
05082f9
Update Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift
czechboy0 Sep 25, 2023
03a9f2d
Update Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift
czechboy0 Sep 26, 2023
ed46fa5
Explicitly test substring support
czechboy0 Sep 26, 2023
669a7f3
Add a test case with an unknown length
czechboy0 Sep 26, 2023
72d241d
PR feedback: improve the iteration behavior checking in HTTPBody, ren…
czechboy0 Sep 26, 2023
0704673
Added a test for the collect method based on a known length
czechboy0 Sep 26, 2023
492cf0c
Move extensions of http types into the generated SPI
czechboy0 Sep 26, 2023
24f8870
Update version in docs to 0.3.0
czechboy0 Sep 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ let package = Package(
)
],
dependencies: [
.package(url: "https://github.com/apple/swift-http-types", branch: "main"),
.package(url: "https://github.com/apple/swift-http-types", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
],
targets: [
Expand Down
28 changes: 28 additions & 0 deletions Sources/OpenAPIRuntime/Conversion/Converter+Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,34 @@ extension Converter {
)
}

// | client | set | request body | urlEncodedForm | codable | optional | setOptionalRequestBodyAsURLEncodedForm |
public func setOptionalRequestBodyAsURLEncodedForm<T: Encodable>(
_ value: T,
headerFields: inout HTTPFields,
contentType: String
) throws -> HTTPBody? {
try setOptionalRequestBody(
value,
headerFields: &headerFields,
contentType: contentType,
convert: convertBodyCodableToURLFormData
)
}

// | client | set | request body | urlEncodedForm | codable | required | setRequiredRequestBodyAsURLEncodedForm |
public func setRequiredRequestBodyAsURLEncodedForm<T: Encodable>(
_ value: T,
headerFields: inout HTTPFields,
contentType: String
) throws -> HTTPBody {
try setRequiredRequestBody(
value,
headerFields: &headerFields,
contentType: contentType,
convert: convertBodyCodableToURLFormData
)
}

// | client | get | response body | JSON | required | getResponseBodyAsJSON |
public func getResponseBodyAsJSON<T: Decodable, C>(
_ type: T.Type,
Expand Down
28 changes: 28 additions & 0 deletions Sources/OpenAPIRuntime/Conversion/Converter+Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,34 @@ extension Converter {
)
}

// | server | get | request body | URLEncodedForm | codable | optional | getOptionalRequestBodyAsURLEncodedForm |
public func getOptionalRequestBodyAsURLEncodedForm<T: Decodable, C>(
_ type: T.Type,
from data: HTTPBody?,
transforming transform: (T) -> C
) async throws -> C? {
try await getOptionalBufferingRequestBody(
type,
from: data,
transforming: transform,
convert: convertURLEncodedFormToCodable
)
}

// | server | get | request body | URLEncodedForm | codable | required | getRequiredRequestBodyAsURLEncodedForm |
public func getRequiredRequestBodyAsURLEncodedForm<T: Decodable, C>(
_ type: T.Type,
from data: HTTPBody?,
transforming transform: (T) -> C
) async throws -> C {
try await getRequiredBufferingRequestBody(
type,
from: data,
transforming: transform,
convert: convertURLEncodedFormToCodable
)
}

// | server | set | response body | JSON | required | setResponseBodyAsJSON |
public func setResponseBodyAsJSON<T: Encodable>(
_ value: T,
Expand Down
41 changes: 39 additions & 2 deletions Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ extension HTTPField.Name {
/// - Parameter name: A field name.
/// - Throws: If the name isn't a valid field name.
init(validated name: String) throws {
guard let fieldName = Self.init(name) else {
guard let fieldName = Self(name) else {
throw RuntimeError.invalidHeaderFieldName(name)
}
self = fieldName
Expand Down Expand Up @@ -171,7 +171,44 @@ extension Converter {
_ value: T
) throws -> HTTPBody {
let data = try encoder.encode(value)
return HTTPBody(data: data)
return HTTPBody(data)
}

/// Returns a value decoded from a URL-encoded form body.
/// - Parameter body: The body containing the raw URL-encoded form bytes.
/// - Returns: A decoded value.
func convertURLEncodedFormToCodable<T: Decodable>(
_ body: HTTPBody
) async throws -> T {
let decoder = URIDecoder(
configuration: .init(
style: .form,
explode: true,
spaceEscapingCharacter: .plus,
dateTranscoder: configuration.dateTranscoder
)
)
let data = try await Data(collecting: body, upTo: .max)
let uriString = Substring(decoding: data, as: UTF8.self)
return try decoder.decode(T.self, from: uriString)
}

/// Returns a URL-encoded form string for the provided encodable value.
/// - Parameter value: The value to encode.
/// - Returns: The raw URL-encoded form body.
func convertBodyCodableToURLFormData<T: Encodable>(
_ value: T
) throws -> HTTPBody {
let encoder = URIEncoder(
configuration: .init(
style: .form,
explode: true,
spaceEscapingCharacter: .plus,
dateTranscoder: configuration.dateTranscoder
)
)
let encodedString = try encoder.encode(value, forKey: "")
return HTTPBody(encodedString)
}

/// Returns a JSON string for the provided encodable value.
Expand Down
57 changes: 32 additions & 25 deletions Sources/OpenAPIRuntime/Interface/HTTPBody.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,19 @@ public final class HTTPBody: @unchecked Sendable {
/// A flag indicating whether an iterator has already been created.
private var locked_iteratorCreated: Bool = false
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved

/// Runs the provided closure in a scope where the `iteratorCreated` value
/// is locked and available both for reading and writing.
/// - Parameter work: A closure that is provided with a read-write reference
/// to the `iteratorCreated` value.
/// - Returns: Whatever value the closure returned.
private func withIteratorCreated<R>(_ work: (inout Bool) throws -> R) rethrows -> R {
lock.lock()
defer {
lock.unlock()
}
return try work(&locked_iteratorCreated)
}

/// Creates a new body.
/// - Parameters:
/// - sequence: The input sequence providing the byte chunks.
Expand Down Expand Up @@ -325,7 +338,7 @@ extension HTTPBody {
/// Creates a new body with the provided async sequence.
/// - Parameters:
/// - sequence: An async sequence that provides the byte chunks.
/// - length: The total lenght of the body.
/// - length: The total length of the body.
/// - iterationBehavior: The iteration behavior of the sequence, which
/// indicates whether it can be iterated multiple times.
@inlinable public convenience init<Bytes: AsyncSequence>(
Expand All @@ -343,7 +356,7 @@ extension HTTPBody {
/// Creates a new body with the provided async sequence of byte sequences.
/// - Parameters:
/// - sequence: An async sequence that provides the byte chunks.
/// - length: The total lenght of the body.
/// - length: The total length of the body.
/// - iterationBehavior: The iteration behavior of the sequence, which
/// indicates whether it can be iterated multiple times.
@inlinable public convenience init<Bytes: AsyncSequence>(
Expand All @@ -366,16 +379,14 @@ extension HTTPBody: AsyncSequence {
public typealias AsyncIterator = Iterator
public func makeAsyncIterator() -> AsyncIterator {
if iterationBehavior == .single {
lock.lock()
defer {
lock.unlock()
}
guard !locked_iteratorCreated else {
fatalError(
"OpenAPIRuntime.HTTPBody attempted to create a second iterator, but the underlying sequence is only safe to be iterated once."
)
withIteratorCreated { iteratorCreated in
guard !iteratorCreated else {
fatalError(
"OpenAPIRuntime.HTTPBody attempted to create a second iterator, but the underlying sequence is only safe to be iterated once."
)
}
iteratorCreated = true
}
locked_iteratorCreated = true
}
return sequence.makeAsyncIterator()
}
Expand Down Expand Up @@ -427,18 +438,14 @@ extension HTTPBody {
// iterate a sequence for the second time, if it's only safe to be
// iterated once.
if iterationBehavior == .single {
try {
lock.lock()
defer {
lock.unlock()
}
guard !locked_iteratorCreated else {
try withIteratorCreated { iteratorCreated in
guard !iteratorCreated else {
throw TooManyIterationsError()
}
}()
}
}
simonjbeaumont marked this conversation as resolved.
Show resolved Hide resolved

var buffer = ByteChunk.init()
var buffer = ByteChunk()
for try await chunk in self {
guard buffer.count + chunk.count <= maxBytes else {
throw TooManyBytesError(maxBytes: maxBytes)
Expand Down Expand Up @@ -490,7 +497,7 @@ extension HTTPBody {
length: Length
) {
self.init(
ByteChunk.init(string),
ByteChunk(string),
length: length
)
}
Expand All @@ -502,7 +509,7 @@ extension HTTPBody {
_ string: some StringProtocol & Sendable
) {
self.init(
ByteChunk.init(string),
ByteChunk(string),
length: .known(string.count)
)
}
Expand Down Expand Up @@ -540,7 +547,7 @@ extension HTTPBody {
/// Creates a new body with the provided async sequence of string chunks.
/// - Parameters:
/// - sequence: An async sequence that provides the string chunks.
/// - length: The total lenght of the body.
/// - length: The total length of the body.
/// - iterationBehavior: The iteration behavior of the sequence, which
/// indicates whether it can be iterated multiple times.
@inlinable public convenience init<Strings: AsyncSequence>(
Expand All @@ -567,7 +574,7 @@ extension HTTPBody.ByteChunk {

extension String {
/// Creates a string by accumulating the full body in-memory into a single buffer up to
/// the provided maximum number of bytes, converting it to string using the provided encoding.
/// the provided maximum number of bytes, converting it to string using UTF-8 encoding.
/// - Parameters:
/// - body: The HTTP body to collect.
/// - maxBytes: The maximum number of bytes this method is allowed
Expand Down Expand Up @@ -610,13 +617,13 @@ extension HTTPBody {

/// Creates a new body from the provided data chunk.
/// - Parameter data: A single data chunk.
public convenience init(data: Data) {
public convenience init(_ data: Data) {
self.init(ArraySlice(data))
}
}

extension Data {
/// Creates a string by accumulating the full body in-memory into a single buffer up to
/// Creates a Data by accumulating the full body in-memory into a single buffer up to
/// the provided maximum number of bytes and converting it to `Data`.
/// - Parameters:
/// - body: The HTTP body to collect.
Expand Down
2 changes: 1 addition & 1 deletion Sources/OpenAPIRuntime/Interface/ServerTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public protocol ServerTransport {
/// - handler: A handler to be invoked when an HTTP request is received.
/// - method: An HTTP request method.
/// - path: A URL template for the path, for example `/pets/{petId}`.
/// - Important: The `path` can have mixed component, such
/// - Important: The `path` can have mixed components, such
/// as `/file/{name}.zip`.
func register(
_ handler: @Sendable @escaping (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (
Expand Down
22 changes: 11 additions & 11 deletions Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//
import XCTest
@_spi(Generated)@testable import OpenAPIRuntime
@_spi(Generated) import OpenAPIRuntime
import HTTPTypes

final class Test_ClientConverterExtensions: Test_Runtime {
Expand Down Expand Up @@ -170,8 +170,8 @@ final class Test_ClientConverterExtensions: Test_Runtime {
}

// | client | set | request body | urlEncodedForm | codable | optional | setRequiredRequestBodyAsURLEncodedForm |
func test_setOptionalRequestBodyAsURLEncodedForm_codable() throws {
var headerFields: [HeaderField] = []
func test_setOptionalRequestBodyAsURLEncodedForm_codable() async throws {
var headerFields: HTTPFields = [:]
let body = try converter.setOptionalRequestBodyAsURLEncodedForm(
testStructDetailed,
headerFields: &headerFields,
Expand All @@ -183,28 +183,28 @@ final class Test_ClientConverterExtensions: Test_Runtime {
return
}

XCTAssertEqualStringifiedData(body, testStructURLFormString)
try await XCTAssertEqualStringifiedData(body, testStructURLFormString)
XCTAssertEqual(
headerFields,
[
.init(name: "content-type", value: "application/x-www-form-urlencoded")
.contentType: "application/x-www-form-urlencoded"
]
)
}

// | client | set | request body | urlEncodedForm | codable | required | setRequiredRequestBodyAsURLEncodedForm |
func test_setRequiredRequestBodyAsURLEncodedForm_codable() throws {
var headerFields: [HeaderField] = []
func test_setRequiredRequestBodyAsURLEncodedForm_codable() async throws {
var headerFields: HTTPFields = [:]
let body = try converter.setRequiredRequestBodyAsURLEncodedForm(
testStructDetailed,
headerFields: &headerFields,
contentType: "application/x-www-form-urlencoded"
)
XCTAssertEqualStringifiedData(body, testStructURLFormString)
try await XCTAssertEqualStringifiedData(body, testStructURLFormString)
XCTAssertEqual(
headerFields,
[
.init(name: "content-type", value: "application/x-www-form-urlencoded")
.contentType: "application/x-www-form-urlencoded"
]
)
}
Expand All @@ -213,7 +213,7 @@ final class Test_ClientConverterExtensions: Test_Runtime {
func test_setOptionalRequestBodyAsBinary_data() async throws {
var headerFields: HTTPFields = [:]
let body = try converter.setOptionalRequestBodyAsBinary(
.init(data: testStringData),
.init(testStringData),
headerFields: &headerFields,
contentType: "application/octet-stream"
)
Expand Down Expand Up @@ -247,7 +247,7 @@ final class Test_ClientConverterExtensions: Test_Runtime {
func test_getResponseBodyAsJSON_codable() async throws {
let value: TestPet = try await converter.getResponseBodyAsJSON(
TestPet.self,
from: .init(data: testStructData),
from: .init(testStructData),
transforming: { $0 }
)
XCTAssertEqual(value, testStruct)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
//
//===----------------------------------------------------------------------===//
import XCTest
@_spi(Generated)@testable import OpenAPIRuntime
@_spi(Generated) import OpenAPIRuntime
import HTTPTypes

extension HTTPField.Name {
static var foo: Self {
.init("foo")!
Self("foo")!
}
}

Expand Down
Loading