mirror of
https://github.com/swift-server/swift-openapi-vapor.git
synced 2026-05-03 07:12:27 +00:00
Initial code commit
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/.vscode
|
||||
/.devcontainer
|
||||
/Packages
|
||||
Package.resolved
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
@@ -0,0 +1,36 @@
|
||||
// swift-tools-version: 5.8
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "swift-openapi-vapor",
|
||||
platforms: [
|
||||
.macOS(.v13),
|
||||
.iOS(.v16),
|
||||
.tvOS(.v16),
|
||||
.watchOS(.v9),
|
||||
],
|
||||
products: [
|
||||
.library(name: "VaporOpenAPIRuntime", targets: ["VaporOpenAPIRuntime"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-openapi-runtime.git", branch: "main"),
|
||||
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "VaporOpenAPIRuntime",
|
||||
dependencies: [
|
||||
.product(name: "Vapor", package: "vapor"),
|
||||
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "VaporOpenAPIRuntimeTests",
|
||||
dependencies: [
|
||||
"VaporOpenAPIRuntime",
|
||||
.product(name: "XCTVapor", package: "vapor"),
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,169 @@
|
||||
import Foundation
|
||||
import OpenAPIRuntime
|
||||
import Vapor
|
||||
import NIOFoundationCompat
|
||||
|
||||
public final class VaporTransport {
|
||||
|
||||
/// A routes builder with which to register request handlers.
|
||||
internal var routesBuilder: Vapor.RoutesBuilder
|
||||
|
||||
/// Creates a new transport.
|
||||
/// - Parameter routesBuilder: A routes builder with which to register request handlers.
|
||||
public init(routesBuilder: Vapor.RoutesBuilder) {
|
||||
self.routesBuilder = routesBuilder
|
||||
}
|
||||
}
|
||||
|
||||
extension VaporTransport: ServerTransport {
|
||||
public func register(
|
||||
_ handler: @Sendable @escaping (OpenAPIRuntime.Request, OpenAPIRuntime.ServerRequestMetadata) async throws -> OpenAPIRuntime.Response,
|
||||
method: OpenAPIRuntime.HTTPMethod,
|
||||
path: [RouterPathComponent],
|
||||
queryItemNames: Set<String>
|
||||
) throws {
|
||||
self.routesBuilder.on(
|
||||
HTTPMethod(method),
|
||||
path.map(Vapor.PathComponent.init(_:))
|
||||
) { vaporRequest in
|
||||
let request = try await OpenAPIRuntime.Request(vaporRequest)
|
||||
let requestMetadata = try OpenAPIRuntime.ServerRequestMetadata(
|
||||
from: vaporRequest,
|
||||
forPath: path,
|
||||
extractingQueryItemNamed: queryItemNames
|
||||
)
|
||||
let response = try await handler(request, requestMetadata)
|
||||
return Vapor.Response(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum VaporTransportError: Error {
|
||||
case unsupportedHTTPMethod(String)
|
||||
case duplicatePathParameter([String])
|
||||
case missingRequiredPathParameter(String)
|
||||
}
|
||||
|
||||
extension Vapor.PathComponent {
|
||||
init(_ pathComponent: OpenAPIRuntime.RouterPathComponent) {
|
||||
switch pathComponent {
|
||||
case .constant(let value): self = .constant(value)
|
||||
case .parameter(let value): self = .parameter(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension OpenAPIRuntime.Request {
|
||||
init(_ vaporRequest: Vapor.Request) async throws {
|
||||
let headerFields: [OpenAPIRuntime.HeaderField] = .init(vaporRequest.headers)
|
||||
|
||||
let bodyData = Data(buffer: try await vaporRequest.body.collect(upTo: .max), byteTransferStrategy: .noCopy)
|
||||
|
||||
let method = try OpenAPIRuntime.HTTPMethod(vaporRequest.method)
|
||||
|
||||
self.init(
|
||||
path: vaporRequest.url.path,
|
||||
query: vaporRequest.url.query,
|
||||
method: method,
|
||||
headerFields: headerFields,
|
||||
body: bodyData
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension OpenAPIRuntime.ServerRequestMetadata {
|
||||
init(
|
||||
from vaporRequest: Vapor.Request,
|
||||
forPath path: [RouterPathComponent],
|
||||
extractingQueryItemNamed queryItemNames: Set<String>
|
||||
) throws {
|
||||
self.init(
|
||||
pathParameters: try .init(from: vaporRequest, forPath: path),
|
||||
queryParameters: .init(from: vaporRequest, queryItemNames: queryItemNames)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Dictionary where Key == String, Value == String {
|
||||
init(from vaporRequest: Vapor.Request, forPath path: [RouterPathComponent]) throws {
|
||||
let keysAndValues = try path.compactMap { item -> (String, String)? in
|
||||
guard case let .parameter(name) = item else {
|
||||
return nil
|
||||
}
|
||||
guard let value = vaporRequest.parameters.get(name) else {
|
||||
throw VaporTransportError.missingRequiredPathParameter(name)
|
||||
}
|
||||
return (name, value)
|
||||
}
|
||||
let pathParameterDictionary = try Dictionary(keysAndValues, uniquingKeysWith: { _, _ in
|
||||
throw VaporTransportError.duplicatePathParameter(keysAndValues.map(\.0))
|
||||
})
|
||||
self = pathParameterDictionary
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == URLQueryItem {
|
||||
init(from vaporRequest: Vapor.Request, queryItemNames: Set<String>) {
|
||||
let queryParameters = queryItemNames.sorted().compactMap { name -> URLQueryItem? in
|
||||
guard let value = try? vaporRequest.query.get(String.self, at: name) else {
|
||||
return nil
|
||||
}
|
||||
return .init(name: name, value: value)
|
||||
}
|
||||
self = queryParameters
|
||||
}
|
||||
}
|
||||
|
||||
extension Vapor.Response {
|
||||
convenience init(_ response: OpenAPIRuntime.Response) {
|
||||
self.init(
|
||||
status: .init(statusCode: response.statusCode),
|
||||
headers: .init(response.headerFields),
|
||||
body: .init(data: response.body)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == OpenAPIRuntime.HeaderField {
|
||||
init(_ headers: NIOHTTP1.HTTPHeaders) {
|
||||
self = headers.map { .init(name: $0.name, value: $0.value) }
|
||||
}
|
||||
}
|
||||
|
||||
extension NIOHTTP1.HTTPHeaders {
|
||||
init(_ headers: [OpenAPIRuntime.HeaderField]) {
|
||||
self.init(headers.map { ($0.name, $0.value) })
|
||||
}
|
||||
}
|
||||
|
||||
extension OpenAPIRuntime.HTTPMethod {
|
||||
init(_ method: NIOHTTP1.HTTPMethod) throws {
|
||||
switch method {
|
||||
case .GET: self = .get
|
||||
case .PUT: self = .put
|
||||
case .POST: self = .post
|
||||
case .DELETE: self = .delete
|
||||
case .OPTIONS: self = .options
|
||||
case .HEAD: self = .head
|
||||
case .PATCH: self = .patch
|
||||
case .TRACE: self = .trace
|
||||
default: throw VaporTransportError.unsupportedHTTPMethod(method.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NIOHTTP1.HTTPMethod {
|
||||
init(_ method: OpenAPIRuntime.HTTPMethod) {
|
||||
switch method {
|
||||
case .get: self = .GET
|
||||
case .put: self = .PUT
|
||||
case .post: self = .POST
|
||||
case .delete: self = .DELETE
|
||||
case .options: self = .OPTIONS
|
||||
case .head: self = .HEAD
|
||||
case .patch: self = .PATCH
|
||||
case .trace: self = .TRACE
|
||||
default: self = .RAW(value: method.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import XCTVapor
|
||||
@testable import VaporOpenAPIRuntime
|
||||
import OpenAPIRuntime
|
||||
|
||||
final class VaporTransportTests: XCTestCase {
|
||||
|
||||
var app: Application!
|
||||
|
||||
override func setUp() async throws {
|
||||
app = Application(.testing)
|
||||
}
|
||||
|
||||
override func tearDown() async throws {
|
||||
app.shutdown()
|
||||
}
|
||||
|
||||
func testRequestConversion() async throws {
|
||||
// POST /hello/{name}
|
||||
app.post("hello", ":name") { vaporRequest in
|
||||
// Hijack the request handler to test the request-conversion functions.
|
||||
let expectedRequest = Request(
|
||||
path: "/hello/Maria",
|
||||
query: "greeting=Howdy",
|
||||
method: .post,
|
||||
headerFields: [
|
||||
.init(name: "X-Mumble", value: "mumble"),
|
||||
.init(name: "content-length", value: "4"),
|
||||
],
|
||||
body: Data("👋".utf8)
|
||||
)
|
||||
let expectedRequestMetadata = ServerRequestMetadata(
|
||||
pathParameters: [ "name": "Maria" ],
|
||||
queryParameters: [ URLQueryItem(name: "greeting", value: "Howdy") ]
|
||||
)
|
||||
let request = try await Request(vaporRequest)
|
||||
XCTAssertEqual(request, expectedRequest)
|
||||
XCTAssertEqual(
|
||||
try ServerRequestMetadata(
|
||||
from: vaporRequest,
|
||||
forPath: [.constant("hello"), .parameter("name")],
|
||||
extractingQueryItemNamed: ["greeting"]
|
||||
),
|
||||
expectedRequestMetadata
|
||||
)
|
||||
|
||||
// Use the response-conversion to create the Vapor response for returning.
|
||||
let response = Response(
|
||||
statusCode: 201,
|
||||
headerFields: [
|
||||
.init(name: "X-Mumble", value: "mumble"),
|
||||
],
|
||||
body: Data("👋".utf8)
|
||||
)
|
||||
return Vapor.Response(response)
|
||||
}
|
||||
|
||||
try app.test(
|
||||
.POST,
|
||||
"/hello/Maria?greeting=Howdy",
|
||||
headers: ["X-Mumble": "mumble"],
|
||||
body: ByteBuffer(string: "👋"),
|
||||
afterResponse: { response in
|
||||
XCTAssertEqual(response.status.code, 201)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func testHandlerRegistration() throws {
|
||||
let transport = VaporTransport(routesBuilder: app)
|
||||
try transport.register(
|
||||
{ _, _ in OpenAPIRuntime.Response(statusCode: 201) },
|
||||
method: .post,
|
||||
path: [.constant("hello"), .parameter("name")],
|
||||
queryItemNames: ["greeting"]
|
||||
)
|
||||
try app.test(
|
||||
.POST,
|
||||
"/hello/Maria?greeting=Howdy",
|
||||
headers: ["X-Mumble": "mumble"],
|
||||
body: ByteBuffer(string: "👋"),
|
||||
afterResponse: { response in
|
||||
XCTAssertEqual(response.status.code, 201)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func testHTTPMethodConversion() throws {
|
||||
XCTAssert(function: NIOHTTP1.HTTPMethod.init(_:), behavesAccordingTo: [
|
||||
(.get, .GET),
|
||||
(.put, .PUT),
|
||||
(.post, .POST),
|
||||
(.delete, .DELETE),
|
||||
(.options, .OPTIONS),
|
||||
(.head, .HEAD),
|
||||
(.patch, .PATCH),
|
||||
(.trace, .TRACE),
|
||||
])
|
||||
try XCTAssert(function: OpenAPIRuntime.HTTPMethod.init(_:), behavesAccordingTo: [
|
||||
(.GET, .get),
|
||||
(.PUT, .put),
|
||||
(.POST, .post),
|
||||
(.DELETE, .delete),
|
||||
(.OPTIONS, .options),
|
||||
(.HEAD, .head),
|
||||
(.PATCH, .patch),
|
||||
(.TRACE, .trace),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func XCTAssert<Input, Output>(
|
||||
function: (Input) throws -> Output,
|
||||
behavesAccordingTo expectations: [(Input, Output)],
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) rethrows where Output: Equatable {
|
||||
for (input, output) in expectations {
|
||||
try XCTAssertEqual(function(input), output, file: file, line: line)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user