Skip to content

Commit 96676c3

Browse files
authored
Merge pull request #7 from swift-server/openapi-http-body
2 parents c7d5712 + 04c3049 commit 96676c3

File tree

4 files changed

+173
-162
lines changed

4 files changed

+173
-162
lines changed

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ let package = Package(
1212
.library(name: "OpenAPIHummingbird", targets: ["OpenAPIHummingbird"]),
1313
],
1414
dependencies: [
15-
.package(url: "https://github.com/apple/swift-openapi-runtime", "0.1.3" ..< "0.3.0"),
16-
.package(url: "https://github.com/hummingbird-project/hummingbird", from: "1.0.0"),
15+
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")),
16+
.package(url: "https://github.com/hummingbird-project/hummingbird", from: "1.8.3"),
1717
],
1818
targets: [
1919
.target(
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import HummingbirdCore
2+
import NIOCore
3+
import OpenAPIRuntime
4+
5+
/// Convert HBByteBufferStreamer to an AsyncSequence of HTTPBody.ByteChunks
6+
struct AsyncStreamerToByteChunkSequence: AsyncSequence {
7+
typealias Element = HTTPBody.ByteChunk
8+
9+
struct AsyncIterator: AsyncIteratorProtocol {
10+
let streamer: HBByteBufferStreamer
11+
12+
mutating func next() async throws -> Element? {
13+
if case .byteBuffer(let buffer) = try await streamer.consume() {
14+
let byteChunk = [UInt8](buffer: buffer)[...]
15+
return byteChunk
16+
}
17+
return nil
18+
}
19+
}
20+
21+
func makeAsyncIterator() -> AsyncIterator {
22+
.init(streamer: self.streamer)
23+
}
24+
25+
let streamer: HBByteBufferStreamer
26+
}
Lines changed: 52 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import HTTPTypes
23
import Hummingbird
34
import NIOFoundationCompat
45
import NIOHTTP1
@@ -24,124 +25,84 @@ extension HBOpenAPITransport {
2425
/// - queryItemNames: The names of query items to be extracted
2526
/// from the request URL that matches the provided HTTP operation.
2627
public func register(
27-
_ handler: @escaping @Sendable (Request, ServerRequestMetadata) async throws -> Response,
28-
method: OpenAPIRuntime.HTTPMethod,
29-
path: [OpenAPIRuntime.RouterPathComponent],
30-
queryItemNames: Set<String>
28+
_ handler: @escaping @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?),
29+
method: HTTPRequest.Method,
30+
path: String
3131
) throws {
3232
self.application.router.on(
3333
Self.makeHummingbirdPath(from: path),
34-
method: .init(method)
34+
method: .init(rawValue: method.rawValue),
35+
options: .streamBody
3536
) { request in
36-
let openAPIRequest = try request.makeOpenAPIRequest()
37+
let (openAPIRequest, openAPIRequestBody) = try request.makeOpenAPIRequest()
3738
let openAPIRequestMetadata = request.makeOpenAPIRequestMetadata()
38-
let openAPIResponse: Response = try await handler(openAPIRequest, openAPIRequestMetadata)
39-
return openAPIResponse.makeHBResponse()
39+
let (openAPIResponse, openAPIResponseBody) = try await handler(openAPIRequest, openAPIRequestBody, openAPIRequestMetadata)
40+
return HBResponse(openAPIResponse, body: openAPIResponseBody)
4041
}
4142
}
4243

43-
/// Make hummingbird path string from RouterPathComponent array
44-
static func makeHummingbirdPath(from path: [OpenAPIRuntime.RouterPathComponent]) -> String {
45-
path.map(\.hbPathComponent).joined(separator: "/")
46-
}
47-
}
48-
49-
extension RouterPathComponent {
50-
/// Return path component as String
51-
var hbPathComponent: String {
52-
switch self {
53-
case .constant(let string):
54-
return string
55-
case .parameter(let parameter):
56-
return "${\(parameter)}"
57-
}
44+
/// Make hummingbird path string from OpenAPI path
45+
static func makeHummingbirdPath(from path: String) -> String {
46+
// frustratingly hummingbird supports `${parameter}` style path which is oh so close
47+
// to the OpenAPI `{parameter}` format
48+
return path.replacingOccurrences(of: "{", with: "${")
5849
}
5950
}
6051

6152
extension HBRequest {
6253
/// Construct ``OpenAPIRuntime.Request`` from Hummingbird ``HBRequest``
63-
func makeOpenAPIRequest() throws -> Request {
64-
guard let method = OpenAPIRuntime.HTTPMethod(self.method) else {
54+
func makeOpenAPIRequest() throws -> (HTTPRequest, HTTPBody?) {
55+
guard let method = HTTPRequest.Method(rawValue: self.method.rawValue) else {
6556
// if we cannot create an OpenAPI http method then we can't create a
6657
// a request and there is no handler for this method
6758
throw HBHTTPError(.notFound)
6859
}
69-
let headers: [HeaderField] = self.headers.map { .init(name: $0.name, value: $0.value) }
70-
let body = self.body.buffer.map { Data(buffer: $0, byteTransferStrategy: .noCopy) }
71-
return .init(
72-
path: self.uri.path,
73-
query: self.uri.query,
74-
method: method,
75-
headerFields: headers,
76-
body: body
60+
var httpFields = HTTPFields()
61+
for header in self.headers {
62+
if let fieldName = HTTPField.Name(header.name) {
63+
httpFields[fieldName] = header.value
64+
}
65+
}
66+
let request = HTTPRequest(
67+
method: method,
68+
scheme: nil,
69+
authority: nil,
70+
path: self.uri.string,
71+
headerFields: httpFields
7772
)
73+
let body: HTTPBody?
74+
switch self.body {
75+
case .byteBuffer(let buffer):
76+
body = buffer.map { HTTPBody([UInt8](buffer: $0)) }
77+
case .stream(let streamer):
78+
body = .init(AsyncStreamerToByteChunkSequence(streamer: streamer), length: .unknown, iterationBehavior: .single)
79+
}
80+
return (request, body)
7881
}
7982

8083
/// Construct ``OpenAPIRuntime.ServerRequestMetadata`` from Hummingbird ``HBRequest``
8184
func makeOpenAPIRequestMetadata() -> ServerRequestMetadata {
82-
let keyAndValues = self.parameters.map { (key: String($0.0), value: String($0.1)) }
83-
let openAPIParameters = [String: String](keyAndValues) { first, _ in first }
84-
let openAPIQueryItems = self.uri.queryParameters.map {
85-
URLQueryItem(name: String($0.key), value: String($0.value))
86-
}
87-
return .init(
88-
pathParameters: openAPIParameters,
89-
queryParameters: openAPIQueryItems
90-
)
91-
}
92-
}
93-
94-
extension Response {
95-
/// Construct Hummingbird ``HBResponse`` from ``OpenAPIRuntime.Response``
96-
func makeHBResponse() -> HBResponse {
97-
let statusCode = HTTPResponseStatus(statusCode: self.statusCode)
98-
let headers = HTTPHeaders(self.headerFields.map { (name: $0.name, value: $0.value) })
99-
let body = ByteBuffer(data: self.body)
85+
let keyAndValues = self.parameters.map { (key: String($0.0), value: $0.1) }
86+
let openAPIParameters = [String: Substring](keyAndValues) { first, _ in first }
10087
return .init(
101-
status: statusCode,
102-
headers: headers,
103-
body: .byteBuffer(body)
88+
pathParameters: openAPIParameters
10489
)
10590
}
10691
}
10792

108-
extension OpenAPIRuntime.HTTPMethod {
109-
init?(_ method: NIOHTTP1.HTTPMethod) {
110-
switch method {
111-
case .GET: self = .get
112-
case .PUT: self = .put
113-
case .POST: self = .post
114-
case .DELETE: self = .delete
115-
case .OPTIONS: self = .options
116-
case .HEAD: self = .head
117-
case .PATCH: self = .patch
118-
case .TRACE: self = .trace
119-
default: return nil
120-
}
121-
}
122-
}
123-
124-
extension NIOHTTP1.HTTPMethod {
125-
init(_ method: OpenAPIRuntime.HTTPMethod) {
126-
switch method {
127-
case .get:
128-
self = .GET
129-
case .put:
130-
self = .PUT
131-
case .post:
132-
self = .POST
133-
case .delete:
134-
self = .DELETE
135-
case .options:
136-
self = .OPTIONS
137-
case .head:
138-
self = .HEAD
139-
case .patch:
140-
self = .PATCH
141-
case .trace:
142-
self = .TRACE
143-
default:
144-
self = .RAW(value: method.name)
93+
extension HBResponse {
94+
init(_ response: HTTPResponse, body: HTTPBody?) {
95+
let responseBody: HBResponseBody
96+
if let body = body {
97+
let bufferSequence = body.map { ByteBuffer(bytes: $0)}
98+
responseBody = .stream(AsyncSequenceResponseBodyStreamer(bufferSequence))
99+
} else {
100+
responseBody = .empty
145101
}
102+
self.init(
103+
status: .init(statusCode: response.status.code, reasonPhrase: response.status.reasonPhrase) ,
104+
headers: .init(response.headerFields.map { (key: $0.name.canonicalName, value: $0.value) }),
105+
body: responseBody
106+
)
146107
}
147108
}

0 commit comments

Comments
 (0)