Skip to content

Commit

Permalink
[Runtime] Async bodies + swift-http-types adoption (#47)
Browse files Browse the repository at this point in the history
[Runtime] Async bodies + swift-http-types adoption

### Motivation

Runtime changes of the approved proposals apple/swift-openapi-generator#255 and apple/swift-openapi-generator#254.

### Blockers of merging this
- [x] 1.0 of swift-http-types

### Modifications

⚠️ Contains breaking changes, this will land to main and then 0.3.0 will be tagged, so not backwards compatible with 0.2.0.

- add a dependency on https://github.com/apple/swift-http-types
- remove our currency types `Request`, `Response`, `HeaderField`
- replace them with the types provided by `HTTPTypes`
- remove `...AsString` and the whole string-based serialization strategy, which was only ever used by bodies, but with streaming, we can't safely stream string chunks, only byte chunks, so we instead provide utils on `HTTPBody` to create it from string, and to collect it into a string. This means that the underlying type for the `text/plain` content type is now `HTTPBody` (a sequence of byte chunks) as opposed to `String`
- adapted Converter extensions to work with the new types
- added some internal utils for working with the query section of a path, as `HTTPTypes` doesn't provide that, the `path` property contains both the path and the query components (in `setEscapedQueryItem`)
- adapted error types
- adapted printing of request/response types, now no bytes of the body are printed, as they cannot be assumed to be consumable more than once
- adjusted the transport and middleware protocols, as described in the proposal
- removed the `queryParameters` property from `ServerRequestMetadata`, as now we parse the full query ourselves, instead of relying on the server transport to do it for us
- removed `RouterPathComponent` as now we pass the full path string to the server transport in the `register` function, allowing transport with more flexible routers to handle mixed path components, e.g. `/foo/{bar}.zip`.
- introduced `HTTPBody`, as described by the proposal
- adjusted UniversalClient and UniversalServer
- moved from String to Substring in a few types, to allow more passthrough of parsed string data without copying

### Result

SOAR-0004 and SOAR-0005 implemented.

### Test Plan

Introduced and adjusted tests for all of the above.


Reviewed by: 

Builds:
     ✔︎ pull request validation (5.8) - Build finished. 
     ✔︎ pull request validation (5.9) - Build finished. 
     ✔︎ pull request validation (docc test) - Build finished. 
     ✔︎ pull request validation (nightly) - Build finished. 
     ✔︎ pull request validation (soundness) - Build finished. 
     ✖︎ pull request validation (api breakage) - Build finished. 
     ✖︎ pull request validation (integration test) - Build finished. 

#47
  • Loading branch information
czechboy0 authored Sep 27, 2023
1 parent ef2b34c commit 1eaf236
Show file tree
Hide file tree
Showing 33 changed files with 1,976 additions and 2,219 deletions.
2 changes: 1 addition & 1 deletion .swift-format
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"UseLetInEveryBoundCaseVariable" : false,
"UseShorthandTypeNames" : true,
"UseSingleLinePropertyGetter" : false,
"UseSynthesizedInitializer" : true,
"UseSynthesizedInitializer" : false,
"UseTripleSlashForDocumentationComments" : true,
"UseWhereClausesInForLoops" : false,
"ValidateDocumentationComments" : false
Expand Down
7 changes: 5 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,18 @@ let package = Package(
.library(
name: "OpenAPIRuntime",
targets: ["OpenAPIRuntime"]
),
)
],
dependencies: [
.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: [
.target(
name: "OpenAPIRuntime",
dependencies: [],
dependencies: [
.product(name: "HTTPTypes", package: "swift-http-types")
],
swiftSettings: swiftSettings
),
.testTarget(
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Add the package dependency in your `Package.swift`:
```swift
.package(
url: "https://github.com/apple/swift-openapi-runtime",
.upToNextMinor(from: "0.2.0")
.upToNextMinor(from: "0.3.0")
),
```

Expand Down
108 changes: 34 additions & 74 deletions Sources/OpenAPIRuntime/Conversion/Converter+Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//
//===----------------------------------------------------------------------===//
import Foundation
import HTTPTypes

extension Converter {

Expand All @@ -20,15 +21,10 @@ extension Converter {
/// - headerFields: The header fields where to add the "accept" header.
/// - contentTypes: The array of acceptable content types by the client.
public func setAcceptHeader<T: AcceptableProtocol>(
in headerFields: inout [HeaderField],
in headerFields: inout HTTPFields,
contentTypes: [AcceptHeaderContentType<T>]
) {
headerFields.append(
.init(
name: "accept",
value: contentTypes.map(\.rawValue).joined(separator: ", ")
)
)
headerFields[.accept] = contentTypes.map(\.rawValue).joined(separator: ", ")
}

// | client | set | request path | URI | required | renderedPath |
Expand Down Expand Up @@ -60,7 +56,7 @@ extension Converter {

// | client | set | request query | URI | both | setQueryItemAsURI |
public func setQueryItemAsURI<T: Encodable>(
in request: inout Request,
in request: inout HTTPRequest,
style: ParameterStyle?,
explode: Bool?,
name: String,
Expand All @@ -84,40 +80,12 @@ extension Converter {
)
}

// | client | set | request body | string | optional | setOptionalRequestBodyAsString |
public func setOptionalRequestBodyAsString<T: Encodable>(
_ value: T?,
headerFields: inout [HeaderField],
contentType: String
) throws -> Data? {
try setOptionalRequestBody(
value,
headerFields: &headerFields,
contentType: contentType,
convert: convertToStringData
)
}

// | client | set | request body | string | required | setRequiredRequestBodyAsString |
public func setRequiredRequestBodyAsString<T: Encodable>(
_ value: T,
headerFields: inout [HeaderField],
contentType: String
) throws -> Data {
try setRequiredRequestBody(
value,
headerFields: &headerFields,
contentType: contentType,
convert: convertToStringData
)
}

// | client | set | request body | JSON | optional | setOptionalRequestBodyAsJSON |
public func setOptionalRequestBodyAsJSON<T: Encodable>(
_ value: T?,
headerFields: inout [HeaderField],
headerFields: inout HTTPFields,
contentType: String
) throws -> Data? {
) throws -> HTTPBody? {
try setOptionalRequestBody(
value,
headerFields: &headerFields,
Expand All @@ -129,9 +97,9 @@ extension Converter {
// | client | set | request body | JSON | required | setRequiredRequestBodyAsJSON |
public func setRequiredRequestBodyAsJSON<T: Encodable>(
_ value: T,
headerFields: inout [HeaderField],
headerFields: inout HTTPFields,
contentType: String
) throws -> Data {
) throws -> HTTPBody {
try setRequiredRequestBody(
value,
headerFields: &headerFields,
Expand All @@ -142,38 +110,38 @@ extension Converter {

// | client | set | request body | binary | optional | setOptionalRequestBodyAsBinary |
public func setOptionalRequestBodyAsBinary(
_ value: Data?,
headerFields: inout [HeaderField],
_ value: HTTPBody?,
headerFields: inout HTTPFields,
contentType: String
) throws -> Data? {
) throws -> HTTPBody? {
try setOptionalRequestBody(
value,
headerFields: &headerFields,
contentType: contentType,
convert: convertDataToBinary
convert: { $0 }
)
}

// | client | set | request body | binary | required | setRequiredRequestBodyAsBinary |
public func setRequiredRequestBodyAsBinary(
_ value: Data,
headerFields: inout [HeaderField],
_ value: HTTPBody,
headerFields: inout HTTPFields,
contentType: String
) throws -> Data {
) throws -> HTTPBody {
try setRequiredRequestBody(
value,
headerFields: &headerFields,
contentType: contentType,
convert: convertDataToBinary
convert: { $0 }
)
}

// | client | set | request body | urlEncodedForm | codable | optional | setOptionalRequestBodyAsURLEncodedForm |
public func setOptionalRequestBodyAsURLEncodedForm<T: Encodable>(
_ value: T,
headerFields: inout [HeaderField],
headerFields: inout HTTPFields,
contentType: String
) throws -> Data? {
) throws -> HTTPBody? {
try setOptionalRequestBody(
value,
headerFields: &headerFields,
Expand All @@ -185,9 +153,9 @@ extension Converter {
// | client | set | request body | urlEncodedForm | codable | required | setRequiredRequestBodyAsURLEncodedForm |
public func setRequiredRequestBodyAsURLEncodedForm<T: Encodable>(
_ value: T,
headerFields: inout [HeaderField],
headerFields: inout HTTPFields,
contentType: String
) throws -> Data {
) throws -> HTTPBody {
try setRequiredRequestBody(
value,
headerFields: &headerFields,
Expand All @@ -196,27 +164,16 @@ extension Converter {
)
}

// | client | get | response body | string | required | getResponseBodyAsString |
public func getResponseBodyAsString<T: Decodable, C>(
_ type: T.Type,
from data: Data,
transforming transform: (T) -> C
) throws -> C {
try getResponseBody(
type,
from: data,
transforming: transform,
convert: convertFromStringData
)
}

// | client | get | response body | JSON | required | getResponseBodyAsJSON |
public func getResponseBodyAsJSON<T: Decodable, C>(
_ type: T.Type,
from data: Data,
from data: HTTPBody?,
transforming transform: (T) -> C
) throws -> C {
try getResponseBody(
) async throws -> C {
guard let data else {
throw RuntimeError.missingRequiredResponseBody
}
return try await getBufferingResponseBody(
type,
from: data,
transforming: transform,
Expand All @@ -226,15 +183,18 @@ extension Converter {

// | client | get | response body | binary | required | getResponseBodyAsBinary |
public func getResponseBodyAsBinary<C>(
_ type: Data.Type,
from data: Data,
transforming transform: (Data) -> C
_ type: HTTPBody.Type,
from data: HTTPBody?,
transforming transform: (HTTPBody) -> C
) throws -> C {
try getResponseBody(
guard let data else {
throw RuntimeError.missingRequiredResponseBody
}
return try getResponseBody(
type,
from: data,
transforming: transform,
convert: convertBinaryToData
convert: { $0 }
)
}
}
17 changes: 9 additions & 8 deletions Sources/OpenAPIRuntime/Conversion/Converter+Common.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//
//===----------------------------------------------------------------------===//
import Foundation
import HTTPTypes

extension Converter {

Expand All @@ -21,8 +22,8 @@ extension Converter {
/// - Parameter headerFields: The header fields to inspect for the content
/// type header.
/// - Returns: The content type value, or nil if not found or invalid.
public func extractContentTypeIfPresent(in headerFields: [HeaderField]) -> OpenAPIMIMEType? {
guard let rawValue = headerFields.firstValue(name: "content-type") else {
public func extractContentTypeIfPresent(in headerFields: HTTPFields) -> OpenAPIMIMEType? {
guard let rawValue = headerFields[.contentType] else {
return nil
}
return OpenAPIMIMEType(rawValue)
Expand Down Expand Up @@ -72,7 +73,7 @@ extension Converter {

// | common | set | header field | URI | both | setHeaderFieldAsURI |
public func setHeaderFieldAsURI<T: Encodable>(
in headerFields: inout [HeaderField],
in headerFields: inout HTTPFields,
name: String,
value: T?
) throws {
Expand All @@ -97,7 +98,7 @@ extension Converter {

// | common | set | header field | JSON | both | setHeaderFieldAsJSON |
public func setHeaderFieldAsJSON<T: Encodable>(
in headerFields: inout [HeaderField],
in headerFields: inout HTTPFields,
name: String,
value: T?
) throws {
Expand All @@ -111,7 +112,7 @@ extension Converter {

// | common | get | header field | URI | optional | getOptionalHeaderFieldAsURI |
public func getOptionalHeaderFieldAsURI<T: Decodable>(
in headerFields: [HeaderField],
in headerFields: HTTPFields,
name: String,
as type: T.Type
) throws -> T? {
Expand All @@ -133,7 +134,7 @@ extension Converter {

// | common | get | header field | URI | required | getRequiredHeaderFieldAsURI |
public func getRequiredHeaderFieldAsURI<T: Decodable>(
in headerFields: [HeaderField],
in headerFields: HTTPFields,
name: String,
as type: T.Type
) throws -> T {
Expand All @@ -155,7 +156,7 @@ extension Converter {

// | common | get | header field | JSON | optional | getOptionalHeaderFieldAsJSON |
public func getOptionalHeaderFieldAsJSON<T: Decodable>(
in headerFields: [HeaderField],
in headerFields: HTTPFields,
name: String,
as type: T.Type
) throws -> T? {
Expand All @@ -169,7 +170,7 @@ extension Converter {

// | common | get | header field | JSON | required | getRequiredHeaderFieldAsJSON |
public func getRequiredHeaderFieldAsJSON<T: Decodable>(
in headerFields: [HeaderField],
in headerFields: HTTPFields,
name: String,
as type: T.Type
) throws -> T {
Expand Down
Loading

0 comments on commit 1eaf236

Please sign in to comment.