Skip to content

Commit 7f923de

Browse files
authored
Merge pull request #2 from MahdiBM/mmbm-update-openapi-0.3.0
Update to OpenAPI 0.3.0
2 parents bd57eb0 + a5a4861 commit 7f923de

File tree

3 files changed

+123
-91
lines changed

3 files changed

+123
-91
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ let package = Package(
1414
.library(name: "OpenAPIVapor", targets: ["OpenAPIVapor"])
1515
],
1616
dependencies: [
17-
.package(url: "https://github.com/apple/swift-openapi-runtime", "0.1.3" ..< "0.3.0"),
17+
.package(url: "https://github.com/apple/swift-openapi-runtime.git", .upToNextMinor(from: "0.3.0")),
1818
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0")
1919
],
2020
targets: [

Sources/OpenAPIVapor/VaporTransport.swift

Lines changed: 95 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import Foundation
1616
import OpenAPIRuntime
17+
import HTTPTypes
1718
import Vapor
1819
import NIOFoundationCompat
1920

@@ -31,24 +32,24 @@ public final class VaporTransport {
3132

3233
extension VaporTransport: ServerTransport {
3334
public func register(
34-
_ handler: @Sendable @escaping (OpenAPIRuntime.Request, OpenAPIRuntime.ServerRequestMetadata)
35-
async throws -> OpenAPIRuntime.Response,
36-
method: OpenAPIRuntime.HTTPMethod,
37-
path: [RouterPathComponent],
38-
queryItemNames: Set<String>
35+
_ handler: @Sendable @escaping (
36+
HTTPTypes.HTTPRequest, OpenAPIRuntime.HTTPBody?, OpenAPIRuntime.ServerRequestMetadata
37+
) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?),
38+
method: HTTPRequest.Method,
39+
path: String
3940
) throws {
4041
self.routesBuilder.on(
4142
HTTPMethod(method),
42-
path.map(Vapor.PathComponent.init(_:))
43+
[PathComponent](path)
4344
) { vaporRequest in
44-
let request = try await OpenAPIRuntime.Request(vaporRequest)
45+
let request = try HTTPTypes.HTTPRequest(vaporRequest)
46+
let body = OpenAPIRuntime.HTTPBody(vaporRequest)
4547
let requestMetadata = try OpenAPIRuntime.ServerRequestMetadata(
4648
from: vaporRequest,
47-
forPath: path,
48-
extractingQueryItemNamed: queryItemNames
49+
forPath: path
4950
)
50-
let response = try await handler(request, requestMetadata)
51-
return Vapor.Response(response)
51+
let response = try await handler(request, body, requestMetadata)
52+
return Vapor.Response(response: response.0, body: response.1)
5253
}
5354
}
5455
}
@@ -59,56 +60,65 @@ enum VaporTransportError: Error {
5960
case missingRequiredPathParameter(String)
6061
}
6162

62-
extension Vapor.PathComponent {
63-
init(_ pathComponent: OpenAPIRuntime.RouterPathComponent) {
64-
switch pathComponent {
65-
case .constant(let value): self = .constant(value)
66-
case .parameter(let value): self = .parameter(value)
63+
extension [Vapor.PathComponent] {
64+
init(_ path: String) {
65+
self = path.split(
66+
separator: "/",
67+
omittingEmptySubsequences: false
68+
).map { parameter in
69+
if parameter.first == "{", parameter.last == "}" {
70+
return .parameter(String(parameter.dropFirst().dropLast()))
71+
} else {
72+
return .constant(String(parameter))
73+
}
6774
}
6875
}
6976
}
7077

71-
extension OpenAPIRuntime.Request {
72-
init(_ vaporRequest: Vapor.Request) async throws {
73-
let headerFields: [OpenAPIRuntime.HeaderField] = .init(vaporRequest.headers)
74-
75-
let bodyData = Data(buffer: try await vaporRequest.body.collect(upTo: .max), byteTransferStrategy: .noCopy)
76-
77-
let method = try OpenAPIRuntime.HTTPMethod(vaporRequest.method)
78-
78+
extension HTTPTypes.HTTPRequest {
79+
init(_ vaporRequest: Vapor.Request) throws {
80+
let headerFields: HTTPTypes.HTTPFields = .init(vaporRequest.headers)
81+
let method = try HTTPTypes.HTTPRequest.Method(vaporRequest.method)
82+
let queries = vaporRequest.url.query.map { "?\($0)" } ?? ""
7983
self.init(
80-
path: vaporRequest.url.path,
81-
query: vaporRequest.url.query,
8284
method: method,
83-
headerFields: headerFields,
84-
body: bodyData
85+
scheme: vaporRequest.url.scheme,
86+
authority: vaporRequest.url.host,
87+
path: vaporRequest.url.path + queries,
88+
headerFields: headerFields
8589
)
8690
}
8791
}
8892

89-
extension OpenAPIRuntime.ServerRequestMetadata {
90-
init(
91-
from vaporRequest: Vapor.Request,
92-
forPath path: [RouterPathComponent],
93-
extractingQueryItemNamed queryItemNames: Set<String>
94-
) throws {
93+
extension OpenAPIRuntime.HTTPBody {
94+
convenience init(_ vaporRequest: Vapor.Request) {
95+
let contentLength = vaporRequest.headers.first(name: "content-length").map(Int.init)
9596
self.init(
96-
pathParameters: try .init(from: vaporRequest, forPath: path),
97-
queryParameters: .init(from: vaporRequest, queryItemNames: queryItemNames)
97+
vaporRequest.body.map(\.readableBytesView),
98+
length: contentLength?.map { .known($0) } ?? .unknown,
99+
iterationBehavior: .single
98100
)
99101
}
100102
}
101103

102-
extension Dictionary where Key == String, Value == String {
103-
init(from vaporRequest: Vapor.Request, forPath path: [RouterPathComponent]) throws {
104-
let keysAndValues = try path.compactMap { item -> (String, String)? in
105-
guard case let .parameter(name) = item else {
104+
extension OpenAPIRuntime.ServerRequestMetadata {
105+
init(from vaporRequest: Vapor.Request, forPath path: String) throws {
106+
self.init(pathParameters: try .init(from: vaporRequest, forPath: path))
107+
}
108+
}
109+
110+
extension Dictionary<String, Substring> {
111+
init(from vaporRequest: Vapor.Request, forPath path: String) throws {
112+
let keysAndValues = try [PathComponent](path).compactMap { component throws -> String? in
113+
guard case let .parameter(parameter) = component else {
106114
return nil
107115
}
108-
guard let value = vaporRequest.parameters.get(name) else {
109-
throw VaporTransportError.missingRequiredPathParameter(name)
116+
return parameter
117+
}.map { parameter -> (String, Substring) in
118+
guard let value = vaporRequest.parameters.get(parameter) else {
119+
throw VaporTransportError.missingRequiredPathParameter(parameter)
110120
}
111-
return (name, value)
121+
return (parameter, Substring(value))
112122
}
113123
let pathParameterDictionary = try Dictionary(keysAndValues, uniquingKeysWith: { _, _ in
114124
throw VaporTransportError.duplicatePathParameter(keysAndValues.map(\.0))
@@ -117,41 +127,61 @@ extension Dictionary where Key == String, Value == String {
117127
}
118128
}
119129

120-
extension Array where Element == URLQueryItem {
121-
init(from vaporRequest: Vapor.Request, queryItemNames: Set<String>) {
122-
let queryParameters = queryItemNames.sorted().compactMap { name -> URLQueryItem? in
123-
guard let value = try? vaporRequest.query.get(String.self, at: name) else {
124-
return nil
125-
}
126-
return .init(name: name, value: value)
127-
}
128-
self = queryParameters
129-
}
130-
}
131-
132130
extension Vapor.Response {
133-
convenience init(_ response: OpenAPIRuntime.Response) {
131+
convenience init(response: HTTPTypes.HTTPResponse, body: OpenAPIRuntime.HTTPBody?) {
134132
self.init(
135-
status: .init(statusCode: response.statusCode),
133+
status: .init(statusCode: response.status.code),
136134
headers: .init(response.headerFields),
137-
body: .init(data: response.body)
135+
body: .init(body)
138136
)
139137
}
140138
}
141139

142-
extension Array where Element == OpenAPIRuntime.HeaderField {
140+
extension Vapor.Response.Body {
141+
init(_ body: OpenAPIRuntime.HTTPBody?) {
142+
guard let body else {
143+
self = .empty
144+
return
145+
}
146+
let stream: @Sendable (any Vapor.BodyStreamWriter) -> () = { writer in
147+
_ = writer.eventLoop.makeFutureWithTask {
148+
do {
149+
for try await chunk in body {
150+
try await writer.write(.buffer(ByteBuffer(bytes: chunk))).get()
151+
}
152+
try await writer.write(.end).get()
153+
} catch {
154+
try await writer.write(.error(error)).get()
155+
}
156+
}
157+
}
158+
switch body.length {
159+
case let .known(count):
160+
self = .init(stream: stream, count: count)
161+
case .unknown:
162+
self = .init(stream: stream)
163+
}
164+
}
165+
}
166+
167+
extension HTTPTypes.HTTPFields {
143168
init(_ headers: NIOHTTP1.HTTPHeaders) {
144-
self = headers.map { .init(name: $0.name, value: $0.value) }
169+
self.init(headers.compactMap { name, value in
170+
guard let name = HTTPField.Name(name) else {
171+
return nil
172+
}
173+
return HTTPField(name: name, value: value)
174+
})
145175
}
146176
}
147177

148178
extension NIOHTTP1.HTTPHeaders {
149-
init(_ headers: [OpenAPIRuntime.HeaderField]) {
150-
self.init(headers.map { ($0.name, $0.value) })
179+
init(_ headers: HTTPTypes.HTTPFields) {
180+
self.init(headers.map { ($0.name.rawName, $0.value) })
151181
}
152182
}
153183

154-
extension OpenAPIRuntime.HTTPMethod {
184+
extension HTTPTypes.HTTPRequest.Method {
155185
init(_ method: NIOHTTP1.HTTPMethod) throws {
156186
switch method {
157187
case .GET: self = .get
@@ -168,7 +198,7 @@ extension OpenAPIRuntime.HTTPMethod {
168198
}
169199

170200
extension NIOHTTP1.HTTPMethod {
171-
init(_ method: OpenAPIRuntime.HTTPMethod) {
201+
init(_ method: HTTPTypes.HTTPRequest.Method) {
172202
switch method {
173203
case .get: self = .GET
174204
case .put: self = .PUT
@@ -178,7 +208,7 @@ extension NIOHTTP1.HTTPMethod {
178208
case .head: self = .HEAD
179209
case .patch: self = .PATCH
180210
case .trace: self = .TRACE
181-
default: self = .RAW(value: method.name)
211+
default: self = .RAW(value: method.rawValue)
182212
}
183213
}
184214
}

Tests/OpenAPIVaporTests/VaporTransportTests.swift

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import XCTVapor
1616
@testable import OpenAPIVapor
17+
import HTTPTypes
1718
import OpenAPIRuntime
1819

1920
final class VaporTransportTests: XCTestCase {
@@ -30,47 +31,47 @@ final class VaporTransportTests: XCTestCase {
3031

3132
func testRequestConversion() async throws {
3233
// POST /hello/{name}
33-
app.post("hello", ":name") { vaporRequest in
34+
app.post("hello", ":name", "world") { vaporRequest in
3435
// Hijack the request handler to test the request-conversion functions.
35-
let expectedRequest = Request(
36-
path: "/hello/Maria",
37-
query: "greeting=Howdy",
36+
let expectedRequest = HTTPTypes.HTTPRequest(
3837
method: .post,
38+
scheme: nil,
39+
authority: nil,
40+
path: "/hello/Maria/world?greeting=Howdy",
3941
headerFields: [
40-
.init(name: "X-Mumble", value: "mumble"),
41-
.init(name: "content-length", value: "4")
42-
],
43-
body: Data("👋".utf8)
42+
HTTPField.Name("X-Mumble")!: "mumble",
43+
HTTPField.Name("content-length")!: "4",
44+
]
4445
)
4546
let expectedRequestMetadata = ServerRequestMetadata(
46-
pathParameters: [ "name": "Maria" ],
47-
queryParameters: [ URLQueryItem(name: "greeting", value: "Howdy") ]
47+
pathParameters: ["name": "Maria"]
4848
)
49-
let request = try await Request(vaporRequest)
49+
let request = try HTTPTypes.HTTPRequest(vaporRequest)
50+
let body = OpenAPIRuntime.HTTPBody(vaporRequest)
51+
let collectedBody = try await [UInt8](collecting: body, upTo: .max)
5052
XCTAssertEqual(request, expectedRequest)
53+
XCTAssertEqual(collectedBody, [UInt8]("👋".utf8))
5154
XCTAssertEqual(
5255
try ServerRequestMetadata(
5356
from: vaporRequest,
54-
forPath: [.constant("hello"), .parameter("name")],
55-
extractingQueryItemNamed: ["greeting"]
57+
forPath: "/hello/{name}/world"
5658
),
5759
expectedRequestMetadata
5860
)
5961

6062
// Use the response-conversion to create the Vapor response for returning.
61-
let response = Response(
62-
statusCode: 201,
63+
let response = HTTPTypes.HTTPResponse(
64+
status: .created,
6365
headerFields: [
64-
.init(name: "X-Mumble", value: "mumble")
65-
],
66-
body: Data("👋".utf8)
66+
HTTPField.Name("X-Mumble")!: "mumble"
67+
]
6768
)
68-
return Vapor.Response(response)
69+
return Vapor.Response(response: response, body: .init([UInt8]("👋".utf8)))
6970
}
7071

7172
try app.test(
7273
.POST,
73-
"/hello/Maria?greeting=Howdy",
74+
"/hello/Maria/world?greeting=Howdy",
7475
headers: ["X-Mumble": "mumble"],
7576
body: ByteBuffer(string: "👋"),
7677
afterResponse: { response in
@@ -81,18 +82,19 @@ final class VaporTransportTests: XCTestCase {
8182

8283
func testHandlerRegistration() throws {
8384
let transport = VaporTransport(routesBuilder: app)
84-
try transport.register({ _, _ in OpenAPIRuntime.Response(statusCode: 201) },
85+
let response = HTTPTypes.HTTPResponse(status: .created)
86+
try transport.register(
87+
{ _, _, _ in (response, nil) },
8588
method: .post,
86-
path: [.constant("hello"), .parameter("name")],
87-
queryItemNames: ["greeting"]
89+
path: "/hello/{name}"
8890
)
8991
try app.test(
9092
.POST,
9193
"/hello/Maria?greeting=Howdy",
9294
headers: ["X-Mumble": "mumble"],
9395
body: ByteBuffer(string: "👋"),
9496
afterResponse: { response in
95-
XCTAssertEqual(response.status.code, 201)
97+
XCTAssertEqual(response.status, .created)
9698
}
9799
)
98100
}
@@ -108,7 +110,7 @@ final class VaporTransportTests: XCTestCase {
108110
(.patch, .PATCH),
109111
(.trace, .TRACE)
110112
])
111-
try XCTAssert(function: OpenAPIRuntime.HTTPMethod.init(_:), behavesAccordingTo: [
113+
try XCTAssert(function: HTTPTypes.HTTPRequest.Method.init(_:), behavesAccordingTo: [
112114
(.GET, .get),
113115
(.PUT, .put),
114116
(.POST, .post),

0 commit comments

Comments
 (0)