Skip to content

Commit 61be1c2

Browse files
authored
Support Hummingbird 2.x.x (#8)
* Support Hummingbird 2.x.x * Update platform support * Support a newer build of HB2 * Propagate HB's authority and scheme * Conform HBRouter to ServerTransport * HB2 now uses openapi-style capture syntax * Process Adam's feedback
1 parent b948c97 commit 61be1c2

File tree

4 files changed

+74
-142
lines changed

4 files changed

+74
-142
lines changed

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import PackageDescription
66
let package = Package(
77
name: "swift-openapi-hummingbird",
88
platforms: [
9-
.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6),
9+
.macOS(.v14), .iOS(.v17), .tvOS(.v17), .watchOS(.v10),
1010
],
1111
products: [
1212
.library(name: "OpenAPIHummingbird", targets: ["OpenAPIHummingbird"]),
1313
],
1414
dependencies: [
1515
.package(url: "https://github.com/apple/swift-openapi-runtime.git", from: "1.0.0"),
16-
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "1.8.3"),
16+
.package(url: "https://github.com/hummingbird-project/hummingbird.git", branch: "2.x.x"),
1717
],
1818
targets: [
1919
.target(

Sources/OpenAPIHummingbird/AsyncStreamerToByteChunkSequence.swift

Lines changed: 0 additions & 40 deletions
This file was deleted.

Sources/OpenAPIHummingbird/OpenAPITransport.swift

Lines changed: 20 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,10 @@
1515
import Foundation
1616
import HTTPTypes
1717
import Hummingbird
18-
import NIOFoundationCompat
1918
import NIOHTTP1
2019
import OpenAPIRuntime
2120

22-
/// Hummingbird Transport for OpenAPI generator
23-
public struct HBOpenAPITransport: ServerTransport {
24-
let application: HBApplication
25-
26-
/// Initialise ``HBOpenAPITransport``
27-
/// - Parameter application: Hummingbird application
28-
public init(_ application: HBApplication) {
29-
self.application = application
30-
}
31-
}
32-
33-
extension HBOpenAPITransport {
21+
extension HBRouter: ServerTransport {
3422
/// Registers an HTTP operation handler at the provided path and method.
3523
/// - Parameters:
3624
/// - handler: A handler to be invoked when an HTTP request is received.
@@ -45,65 +33,38 @@ extension HBOpenAPITransport {
4533
method: HTTPRequest.Method,
4634
path: String
4735
) throws {
48-
self.application.router.on(
49-
Self.makeHummingbirdPath(from: path),
50-
method: .init(rawValue: method.rawValue),
51-
options: .streamBody
52-
) { request in
53-
let (openAPIRequest, openAPIRequestBody) = try request.makeOpenAPIRequest()
54-
let openAPIRequestMetadata = request.makeOpenAPIRequestMetadata()
55-
let (openAPIResponse, openAPIResponseBody) = try await handler(
56-
openAPIRequest,
57-
openAPIRequestBody,
58-
openAPIRequestMetadata
59-
)
36+
self.on(
37+
path,
38+
method: method
39+
) { request, context in
40+
let (openAPIRequest, openAPIRequestBody) = try request.makeOpenAPIRequest(context: context)
41+
let openAPIRequestMetadata = context.makeOpenAPIRequestMetadata()
42+
let (openAPIResponse, openAPIResponseBody) = try await handler(openAPIRequest, openAPIRequestBody, openAPIRequestMetadata)
6043
return HBResponse(openAPIResponse, body: openAPIResponseBody)
6144
}
6245
}
63-
64-
/// Make hummingbird path string from OpenAPI path
65-
static func makeHummingbirdPath(from path: String) -> String {
66-
// frustratingly hummingbird supports `${parameter}` style path which is oh so close
67-
// to the OpenAPI `{parameter}` format
68-
return path.replacingOccurrences(of: "{", with: "${")
69-
}
7046
}
7147

7248
extension HBRequest {
7349
/// Construct ``OpenAPIRuntime.Request`` from Hummingbird ``HBRequest``
74-
func makeOpenAPIRequest() throws -> (HTTPRequest, HTTPBody?) {
75-
guard let method = HTTPRequest.Method(rawValue: self.method.rawValue) else {
76-
// if we cannot create an OpenAPI http method then we can't create a
77-
// a request and there is no handler for this method
78-
throw HBHTTPError(.notFound)
79-
}
80-
var httpFields = HTTPFields()
81-
for header in self.headers {
82-
if let fieldName = HTTPField.Name(header.name) {
83-
httpFields[fieldName] = header.value
84-
}
85-
}
86-
let request = HTTPRequest(
87-
method: method,
88-
scheme: nil,
89-
authority: nil,
90-
path: self.uri.string,
91-
headerFields: httpFields
92-
)
50+
func makeOpenAPIRequest<Context: HBBaseRequestContext>(context: Context) throws -> (HTTPRequest, HTTPBody?) {
51+
let request = self.head
9352
let body: HTTPBody?
9453
switch self.body {
9554
case .byteBuffer(let buffer):
96-
body = buffer.map { HTTPBody([UInt8](buffer: $0)) }
55+
body = HTTPBody([UInt8](buffer: buffer))
9756
case .stream(let streamer):
9857
body = .init(
99-
AsyncStreamerToByteChunkSequence(streamer: streamer),
58+
streamer.map { [UInt8](buffer: $0) },
10059
length: .unknown,
10160
iterationBehavior: .single
10261
)
10362
}
10463
return (request, body)
10564
}
65+
}
10666

67+
extension HBBaseRequestContext {
10768
/// Construct ``OpenAPIRuntime.ServerRequestMetadata`` from Hummingbird ``HBRequest``
10869
func makeOpenAPIRequestMetadata() -> ServerRequestMetadata {
10970
let keyAndValues = self.parameters.map { (key: String($0.0), value: $0.1) }
@@ -118,14 +79,15 @@ extension HBResponse {
11879
init(_ response: HTTPResponse, body: HTTPBody?) {
11980
let responseBody: HBResponseBody
12081
if let body = body {
121-
let bufferSequence = body.map { ByteBuffer(bytes: $0) }
122-
responseBody = .stream(AsyncSequenceResponseBodyStreamer(bufferSequence))
82+
let bufferSequence = body.map { ByteBuffer(bytes: $0)}
83+
responseBody = .init(asyncSequence: bufferSequence)
12384
} else {
124-
responseBody = .empty
85+
responseBody = .init(byteBuffer: ByteBuffer())
12586
}
87+
12688
self.init(
127-
status: .init(statusCode: response.status.code, reasonPhrase: response.status.reasonPhrase),
128-
headers: .init(response.headerFields.map { (key: $0.name.canonicalName, value: $0.value) }),
89+
status: response.status,
90+
headers: response.headerFields,
12991
body: responseBody
13092
)
13193
}

Tests/OpenAPIHummingbirdTests/OpenAPITransportTests.swift

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414

1515
import HTTPTypes
1616
import Hummingbird
17+
import HummingbirdCore
1718
import HummingbirdXCT
1819
import NIOCore
1920
import NIOHTTP1
2021
import OpenAPIRuntime
22+
import NIOHTTPTypes
2123
import XCTest
2224

2325
@testable import OpenAPIHummingbird
@@ -29,26 +31,25 @@ extension HTTPField.Name {
2931

3032
final class HBOpenAPITransportTests: XCTestCase {
3133
func test_requestConversion() async throws {
32-
let app = HBApplication(testing: .live)
34+
let router = HBRouter()
3335

34-
app.router.post("/hello/:name") { hbRequest -> HBResponse in
36+
router.post("/hello/:name") { hbRequest, context -> HBResponse in
3537
// Hijack the request handler to test the request-conversion functions.
3638
let expectedRequest = HTTPRequest(
3739
method: .post,
38-
scheme: nil,
39-
authority: nil,
40+
scheme: "http",
41+
authority: "localhost",
4042
path: "/hello/Maria?greeting=Howdy",
4143
headerFields: [
4244
.xMumble: "mumble",
4345
.connection: "keep-alive",
44-
.host: "localhost",
4546
.contentLength: "4",
4647
]
4748
)
4849
let expectedRequestMetadata = ServerRequestMetadata(
4950
pathParameters: ["name": "Maria"]
5051
)
51-
let (request, body) = try hbRequest.makeOpenAPIRequest()
52+
let (request, body) = try hbRequest.makeOpenAPIRequest(context: context)
5253
let collectedBody: [UInt8]
5354
if let body = body {
5455
collectedBody = try await .init(collecting: body, upTo: .max)
@@ -57,80 +58,89 @@ final class HBOpenAPITransportTests: XCTestCase {
5758
}
5859
XCTAssertEqual(request, expectedRequest)
5960
XCTAssertEqual(collectedBody, [UInt8]("👋".utf8))
60-
XCTAssertEqual(hbRequest.makeOpenAPIRequestMetadata(), expectedRequestMetadata)
61+
XCTAssertEqual(context.makeOpenAPIRequestMetadata(), expectedRequestMetadata)
6162

6263
// Use the response-conversion to create the HBRequest for returning.
6364
let response = HTTPResponse(status: .created, headerFields: [.xMumble: "mumble"])
6465
return HBResponse(response, body: .init([UInt8]("👋".utf8)))
6566
}
6667

67-
try app.XCTStart()
68-
defer { app.XCTStop() }
69-
70-
try app.XCTExecute(
71-
uri: "/hello/Maria?greeting=Howdy",
72-
method: .POST,
73-
headers: ["X-Mumble": "mumble"],
74-
body: ByteBuffer(string: "👋")
75-
) { hbResponse in
76-
// Check the HBResponse (created from the Response) is what meets expectations.
77-
XCTAssertEqual(hbResponse.status, .created)
78-
XCTAssertEqual(hbResponse.headers.first(name: "X-Mumble"), "mumble")
79-
XCTAssertEqual(try String(buffer: XCTUnwrap(hbResponse.body)), "👋")
68+
let app = HBApplication(responder: router.buildResponder())
69+
70+
try await app.test(.live) { client in
71+
try await client.XCTExecute(
72+
uri: "/hello/Maria?greeting=Howdy",
73+
method: .post,
74+
headers: [
75+
.xMumble: "mumble",
76+
],
77+
body: ByteBuffer(string: "👋")
78+
) { hbResponse in
79+
// Check the HBResponse (created from the Response) is what meets expectations.
80+
XCTAssertEqual(hbResponse.status, .created)
81+
XCTAssertEqual(hbResponse.headers[.xMumble], "mumble")
82+
XCTAssertEqual(try String(buffer: XCTUnwrap(hbResponse.body)), "👋")
83+
}
8084
}
8185
}
8286

8387
func test_largeBody() async throws {
84-
let app = HBApplication(testing: .live)
85-
app.server.addChannelHandler(BreakupHTTPBodyChannelHandler())
86-
let bytes = (0..<1_000_000).map { _ in UInt8.random(in: 0...255) }
88+
let router = HBRouter()
89+
let bytes = (0..<1_000_000).map { _ in UInt8.random(in: 0...255)}
8790
let byteBuffer = ByteBuffer(bytes: bytes)
8891

89-
app.router.post("/hello/:name") { hbRequest -> HBResponse in
92+
router.post("/hello/:name") { hbRequest, context -> HBResponse in
9093
// Hijack the request handler to test the request-conversion functions.
9194
let expectedRequest = HTTPRequest(
9295
method: .post,
93-
scheme: nil,
94-
authority: nil,
96+
scheme: "http",
97+
authority: "localhost",
9598
path: "/hello/Maria?greeting=Howdy",
9699
headerFields: [
97100
.connection: "keep-alive",
98-
.host: "localhost",
99101
.contentLength: "1000000",
100102
]
101103
)
102104
let expectedRequestMetadata = ServerRequestMetadata(
103105
pathParameters: ["name": "Maria"]
104106
)
105-
let (request, body) = try hbRequest.makeOpenAPIRequest()
107+
let (request, body) = try hbRequest.makeOpenAPIRequest(context: context)
106108
XCTAssertEqual(request, expectedRequest)
107-
XCTAssertEqual(hbRequest.makeOpenAPIRequestMetadata(), expectedRequestMetadata)
109+
XCTAssertEqual(context.makeOpenAPIRequestMetadata(), expectedRequestMetadata)
108110

109111
// Use the response-conversion to create the HBRequest for returning.
110112
let response = HTTPResponse(status: .ok)
111113
return HBResponse(response, body: body)
112114
}
113115

114-
try app.XCTStart()
115-
defer { app.XCTStop() }
116-
117-
try app.XCTExecute(
118-
uri: "/hello/Maria?greeting=Howdy",
119-
method: .POST,
120-
body: byteBuffer
121-
) { hbResponse in
122-
// Check the HBResponse (created from the Response) is what meets expectations.
123-
XCTAssertEqual(hbResponse.status, .ok)
124-
XCTAssertEqual(byteBuffer, hbResponse.body)
116+
let app = HBApplication(
117+
router: router,
118+
server: .http1(
119+
additionalChannelHandlers: [
120+
BreakupHTTPBodyChannelHandler()
121+
]
122+
)
123+
)
124+
125+
try await app.test(.live) { client in
126+
try await client.XCTExecute(
127+
uri: "/hello/Maria?greeting=Howdy",
128+
method: .post,
129+
body: byteBuffer
130+
) { hbResponse in
131+
// Check the HBResponse (created from the Response) is what meets expectations.
132+
XCTAssertEqual(hbResponse.status, .ok)
133+
XCTAssertEqual(byteBuffer, hbResponse.body)
134+
}
125135
}
126136
}
127137
}
128138

129139
/// To test streaming we need to break up the HTTP body into multiple chunks. This channel handler
130140
/// breaks up the incoming HTTP body into multiple chunks
131141
class BreakupHTTPBodyChannelHandler: ChannelInboundHandler, RemovableChannelHandler {
132-
typealias InboundIn = HTTPServerRequestPart
133-
typealias InboundOut = HTTPServerRequestPart
142+
typealias InboundIn = HTTPRequestPart
143+
typealias InboundOut = HTTPRequestPart
134144

135145
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
136146
let part = unwrapInboundIn(data)

0 commit comments

Comments
 (0)