From 0921e197879c79d07abaa673a878b4fe3511a22a Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Tue, 5 Dec 2023 09:13:29 +0000 Subject: [PATCH] Result builder router in separate module --- Package.swift | 5 + Sources/Hummingbird/Router/RouterPath.swift | 22 +- .../Hummingbird/Server/RequestContext.swift | 8 +- .../MiddlewareModule/MiddlewareStack.swift | 73 ++++++ Sources/HummingbirdRouter/Route.swift | 227 ++++++++++++++++++ Sources/HummingbirdRouter/RouteBuilder.swift | 78 ++++++ Sources/HummingbirdRouter/RouteGroup.swift | 67 ++++++ Sources/HummingbirdRouter/RouteHandler.swift | 53 ++++ Sources/HummingbirdRouter/Router.swift | 62 +++++ Sources/HummingbirdRouter/RouterPath.swift | 92 +++++++ 10 files changed, 671 insertions(+), 16 deletions(-) create mode 100644 Sources/HummingbirdRouter/MiddlewareModule/MiddlewareStack.swift create mode 100644 Sources/HummingbirdRouter/Route.swift create mode 100644 Sources/HummingbirdRouter/RouteBuilder.swift create mode 100644 Sources/HummingbirdRouter/RouteGroup.swift create mode 100644 Sources/HummingbirdRouter/RouteHandler.swift create mode 100644 Sources/HummingbirdRouter/Router.swift create mode 100644 Sources/HummingbirdRouter/RouterPath.swift diff --git a/Package.swift b/Package.swift index e5eb66a6e..845128940 100644 --- a/Package.swift +++ b/Package.swift @@ -13,6 +13,7 @@ let package = Package( .library(name: "HummingbirdTLS", targets: ["HummingbirdTLS"]), .library(name: "HummingbirdFoundation", targets: ["HummingbirdFoundation"]), .library(name: "HummingbirdJobs", targets: ["HummingbirdJobs"]), + .library(name: "HummingbirdRouter", targets: ["HummingbirdRouter"]), .library(name: "HummingbirdXCT", targets: ["HummingbirdXCT"]), .executable(name: "PerformanceTest", targets: ["PerformanceTest"]), ], @@ -65,6 +66,10 @@ let package = Package( .byName(name: "Hummingbird"), .product(name: "Logging", package: "swift-log"), ]), + .target(name: "HummingbirdRouter", dependencies: [ + .byName(name: "Hummingbird"), + .product(name: "Logging", package: "swift-log"), + ]), .target(name: "HummingbirdXCT", dependencies: [ .byName(name: "Hummingbird"), .byName(name: "HummingbirdCoreXCT"), diff --git a/Sources/Hummingbird/Router/RouterPath.swift b/Sources/Hummingbird/Router/RouterPath.swift index ec3e42560..4c3d26ff5 100644 --- a/Sources/Hummingbird/Router/RouterPath.swift +++ b/Sources/Hummingbird/Router/RouterPath.swift @@ -13,8 +13,8 @@ //===----------------------------------------------------------------------===// /// Split router path into components -struct RouterPath: ExpressibleByStringLiteral { - enum Element: Equatable { +public struct RouterPath: Sendable, ExpressibleByStringLiteral { + public enum Element: Equatable, Sendable { case path(Substring) case capture(Substring) case prefixCapture(suffix: Substring, parameter: Substring) // *.jpg @@ -25,7 +25,7 @@ struct RouterPath: ExpressibleByStringLiteral { case recursiveWildcard case null - static func ~= (lhs: Element, rhs: S) -> Bool { + static func ~= (lhs: Element, rhs: some StringProtocol) -> Bool { switch lhs { case .path(let lhs): return lhs == rhs @@ -48,7 +48,7 @@ struct RouterPath: ExpressibleByStringLiteral { } } - static func == (lhs: Element, rhs: S) -> Bool { + static func == (lhs: Element, rhs: some StringProtocol) -> Bool { switch lhs { case .path(let lhs): return lhs == rhs @@ -58,9 +58,9 @@ struct RouterPath: ExpressibleByStringLiteral { } } - let components: [Element] + public let components: [Element] - init(_ value: String) { + public init(_ value: String) { let split = value.split(separator: "/", omittingEmptySubsequences: true) self.components = split.map { component in if component.first == ":" { @@ -97,20 +97,20 @@ struct RouterPath: ExpressibleByStringLiteral { } } - init(stringLiteral value: String) { + public init(stringLiteral value: String) { self.init(value) } } extension RouterPath: Collection { - func index(after i: Int) -> Int { + public func index(after i: Int) -> Int { return self.components.index(after: i) } - subscript(_ index: Int) -> RouterPath.Element { + public subscript(_ index: Int) -> RouterPath.Element { return self.components[index] } - var startIndex: Int { self.components.startIndex } - var endIndex: Int { self.components.endIndex } + public var startIndex: Int { self.components.startIndex } + public var endIndex: Int { self.components.endIndex } } diff --git a/Sources/Hummingbird/Server/RequestContext.swift b/Sources/Hummingbird/Server/RequestContext.swift index 9b3bf1274..1f694a306 100644 --- a/Sources/Hummingbird/Server/RequestContext.swift +++ b/Sources/Hummingbird/Server/RequestContext.swift @@ -26,7 +26,7 @@ public struct EndpointPath: Sendable { } /// Endpoint path - public internal(set) var value: String? { + public var value: String? { get { self._value.withLockedValue { $0 } } nonmutating set { self._value.withLockedValue { $0 = newValue } } } @@ -55,11 +55,9 @@ public struct HBCoreRequestContext: Sendable { @usableFromInline var logger: Logger /// Endpoint path - @usableFromInline - var endpointPath: EndpointPath + public var endpointPath: EndpointPath /// Parameters extracted from URI - @usableFromInline - var parameters: HBParameters + public var parameters: HBParameters @inlinable public init( diff --git a/Sources/HummingbirdRouter/MiddlewareModule/MiddlewareStack.swift b/Sources/HummingbirdRouter/MiddlewareModule/MiddlewareStack.swift new file mode 100644 index 000000000..89ad6130c --- /dev/null +++ b/Sources/HummingbirdRouter/MiddlewareModule/MiddlewareStack.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the swift-middleware open source project +// +// Copyright (c) 2023 Apple Inc. and the swift-middleware project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of swift-middleware project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import Hummingbird + +public struct MiddlewareClosure: MiddlewareProtocol { + @usableFromInline + var closure: Middleware + + @inlinable + public init(_ middleware: @escaping Middleware) { + self.closure = middleware + } + + @inlinable + public func handle(_ input: Input, context: Context, next: (Input, Context) async throws -> Output) async throws -> Output { + try await self.closure(input, context, next) + } +} + +public struct _Middleware2: MiddlewareProtocol where M0.Input == M1.Input, M0.Context == M1.Context, M0.Output == M1.Output { + public typealias Input = M0.Input + public typealias Output = M0.Output + public typealias Context = M0.Context + + @usableFromInline let m0: M0 + @usableFromInline let m1: M1 + + @inlinable + public init(_ m0: M0, _ m1: M1) { + self.m0 = m0 + self.m1 = m1 + } + + @inlinable + public func handle(_ input: M0.Input, context: M0.Context, next: (M0.Input, M0.Context) async throws -> M0.Output) async throws -> M0.Output { + try await self.m0.handle(input, context: context) { input, context in + try await self.m1.handle(input, context: context, next: next) + } + } +} + +@resultBuilder +public enum MiddlewareFixedTypeBuilder { + public static func buildExpression(_ m0: M0) -> M0 where M0.Input == Input, M0.Output == Output, M0.Context == Context { + return m0 + } + + public static func buildBlock(_ m0: M0) -> M0 { + return m0 + } + + public static func buildPartialBlock(first: M0) -> M0 { + first + } + + public static func buildPartialBlock( + accumulated m0: M0, + next m1: M1 + ) -> _Middleware2 where M0.Input == M1.Input, M0.Output == M1.Output, M0.Context == M1.Context { + _Middleware2(m0, m1) + } +} diff --git a/Sources/HummingbirdRouter/Route.swift b/Sources/HummingbirdRouter/Route.swift new file mode 100644 index 000000000..ccf2fe57b --- /dev/null +++ b/Sources/HummingbirdRouter/Route.swift @@ -0,0 +1,227 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2023 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import HTTPTypes +import Hummingbird +import ServiceContextModule + +/// Route definition +public struct Route: HBMiddlewareProtocol where Handler.Context == Context { + public let fullPath: String + public let routerPath: RouterPath + public let method: HTTPRequest.Method + public let handler: Handler + + /// Initialize Route + /// - Parameters: + /// - method: Route method + /// - routerPath: Route path, relative to Group route is defined in + /// - handler: Route handler + init(_ method: HTTPRequest.Method, _ routerPath: RouterPath = "", handler: Handler) { + self.method = method + self.routerPath = routerPath + self.handler = handler + self.fullPath = Self.getFullPath(from: routerPath) + } + + /// Initialize Route with a closure + /// - Parameters: + /// - method: Route method + /// - routerPath: Route path, relative to Group route is defined in + /// - handler: Router handler closure + public init( + _ method: HTTPRequest.Method, + _ routerPath: RouterPath = "", + handler: @escaping @Sendable (Input, Context) async throws -> RouteOutput + ) where Handler == RouteHandlerClosure { + self.init( + method, + routerPath, + handler: RouteHandlerClosure(closure: handler) + ) + } + + /// Initialize Route with a MiddlewareProtocol + /// - Parameters: + /// - method: Route method + /// - routerPath: Route path, relative to Group route is defined in + /// - builder: Result builder used to build Route middleware + public init( + _ method: HTTPRequest.Method, + _ routerPath: RouterPath = "", + @RouteBuilder builder: () -> M0 + ) where Handler == RouteHandlerMiddleware, M0.Input == HBRequest, M0.Output == HBResponse, M0.Context == Context { + self.init( + method, + routerPath, + handler: RouteHandlerMiddleware(middleware: builder()) + ) + } + + /// Handle route middleware + /// - Parameters: + /// - input: Request + /// - context: Context for handler + /// - next: Next middleware to call if route method and path is not matched + /// - Returns: Response + public func handle(_ input: HBRequest, context: Context, next: (HBRequest, Context) async throws -> HBResponse) async throws -> HBResponse { + if input.method == self.method, let context = self.routerPath.matchAll(context) { + context.coreContext.endpointPath.value = self.fullPath + return try await self.handler.handle(input, context: context) + } + return try await next(input, context) + } + + /// Return full path of route, using Task local stored `routeGroupPath`. + static func getFullPath(from path: RouterPath) -> String { + let parentGroupPath = ServiceContext.current?.routeGroupPath ?? "" + if path.count > 0 || parentGroupPath.count == 0 { + return "\(parentGroupPath)/\(path)" + } else { + return parentGroupPath + } + } +} + +/// Create a GET Route with a closure +/// - Parameters: +/// - routerPath: Route path, relative to Group route is defined in +/// - handler: Router handler closure +public func Get( + _ routerPath: RouterPath = "", + handler: @escaping @Sendable (HBRequest, Context) async throws -> RouteOutput +) -> Route, Context> { + .init(.get, routerPath, handler: handler) +} + +/// Create a GET Route with a MiddlewareProtocol +/// - Parameters: +/// - routerPath: Route path, relative to Group route is defined in +/// - builder: Result builder used to build Route middleware +public func Get( + _ routerPath: RouterPath = "", + @RouteBuilder builder: () -> M0 +) -> Route, Context> where M0.Input == HBRequest, M0.Output == HBResponse, M0.Context == Context { + .init(.get, routerPath, builder: builder) +} + +/// Create a HEAD Route with a closure +/// - Parameters: +/// - routerPath: Route path, relative to Group route is defined in +/// - handler: Router handler closure +public func Head( + _ routerPath: RouterPath = "", + handler: @escaping @Sendable (HBRequest, Context) async throws -> RouteOutput +) -> Route, Context> { + .init(.head, routerPath, handler: handler) +} + +/// Create a HEAD Route with a MiddlewareProtocol +/// - Parameters: +/// - routerPath: Route path, relative to Group route is defined in +/// - builder: Result builder used to build Route middleware +public func Head( + _ routerPath: RouterPath = "", + @RouteBuilder builder: () -> M0 +) -> Route, Context> where M0.Input == HBRequest, M0.Output == HBResponse, M0.Context == Context { + .init(.head, routerPath, builder: builder) +} + +/// Create a PUT Route with a closure +/// - Parameters: +/// - routerPath: Route path, relative to Group route is defined in +/// - handler: Router handler closure +public func Put( + _ routerPath: RouterPath = "", + handler: @escaping @Sendable (HBRequest, Context) async throws -> RouteOutput +) -> Route, Context> { + .init(.put, routerPath, handler: handler) +} + +/// Create a PUT Route with a MiddlewareProtocol +/// - Parameters: +/// - routerPath: Route path, relative to Group route is defined in +/// - builder: Result builder used to build Route middleware +public func Put( + _ routerPath: RouterPath = "", + @RouteBuilder builder: () -> M0 +) -> Route, Context> where M0.Input == HBRequest, M0.Output == HBResponse, M0.Context == Context { + .init(.put, routerPath, builder: builder) +} + +/// Create a POST Route with a closure +/// - Parameters: +/// - routerPath: Route path, relative to Group route is defined in +/// - handler: Router handler closure +public func Post( + _ routerPath: RouterPath = "", + handler: @escaping @Sendable (HBRequest, Context) async throws -> RouteOutput +) -> Route, Context> { + .init(.post, routerPath, handler: handler) +} + +/// Create a POST Route with a MiddlewareProtocol +/// - Parameters: +/// - routerPath: Route path, relative to Group route is defined in +/// - builder: Result builder used to build Route middleware +public func Post( + _ routerPath: RouterPath = "", + @RouteBuilder builder: () -> M0 +) -> Route, Context> where M0.Input == HBRequest, M0.Output == HBResponse, M0.Context == Context { + .init(.post, routerPath, builder: builder) +} + +/// Create a PATCH Route with a closure +/// - Parameters: +/// - routerPath: Route path, relative to Group route is defined in +/// - handler: Router handler closure +public func Patch( + _ routerPath: RouterPath = "", + handler: @escaping @Sendable (HBRequest, Context) async throws -> RouteOutput +) -> Route, Context> { + .init(.patch, routerPath, handler: handler) +} + +/// Create a PATCH Route with a MiddlewareProtocol +/// - Parameters: +/// - routerPath: Route path, relative to Group route is defined in +/// - builder: Result builder used to build Route middleware +public func Patch( + _ routerPath: RouterPath = "", + @RouteBuilder builder: () -> M0 +) -> Route, Context> where M0.Input == HBRequest, M0.Output == HBResponse, M0.Context == Context { + .init(.patch, routerPath, builder: builder) +} + +/// Create a DELETE Route with a closure +/// - Parameters: +/// - routerPath: Route path, relative to Group route is defined in +/// - handler: Router handler closure +public func Delete( + _ routerPath: RouterPath = "", + handler: @escaping @Sendable (HBRequest, Context) async throws -> RouteOutput +) -> Route, Context> { + .init(.delete, routerPath, handler: handler) +} + +/// Create a DELETE Route with a MiddlewareProtocol +/// - Parameters: +/// - routerPath: Route path, relative to Group route is defined in +/// - builder: Result builder used to build Route middleware +public func Delete( + _ routerPath: RouterPath = "", + @RouteBuilder builder: () -> M0 +) -> Route, Context> where M0.Input == HBRequest, M0.Output == HBResponse, M0.Context == Context { + .init(.delete, routerPath, builder: builder) +} diff --git a/Sources/HummingbirdRouter/RouteBuilder.swift b/Sources/HummingbirdRouter/RouteBuilder.swift new file mode 100644 index 000000000..c2bd70be1 --- /dev/null +++ b/Sources/HummingbirdRouter/RouteBuilder.swift @@ -0,0 +1,78 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2023 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Hummingbird + +/// Route Handler Middleware. +/// +/// Requires that the return value of handler conforms to ``HBResponseGenerator`` so +/// that the `handle` function can return an `HBResponse` +public struct Handle: Sendable, MiddlewareProtocol { + public typealias Input = HBRequest + public typealias Output = HBResponse + public typealias Handler = @Sendable (Input, Context) async throws -> HandlerOutput + + let handler: Handler + + init(_ handler: @escaping Handler) { + self.handler = handler + } + + public func handle(_ input: Input, context: Context, next: (Input, Context) async throws -> Output) async throws -> Output { + return try await self.handler(input, context).response(from: input, context: context) + } +} + +/// Result builder for a Route. +/// +/// This is very similar to the ``MiddlewareStack`` reult builder except it requires the +/// last entry of the builder to be a ``Handle`` so we are guaranteed a Response. It also +/// adds the ability to pass in a closure instead of ``Handle`` type. +@resultBuilder +public enum RouteBuilder { + /// Provide generic requirements for MiddlewareProtocol + public static func buildExpression(_ m0: M0) -> M0 where M0.Input == HBRequest, M0.Output == HBResponse, M0.Context == Context { + return m0 + } + + /// Build a ``Handle`` from a closure + public static func buildExpression(_ handler: @escaping @Sendable (HBRequest, Context) async throws -> HandlerOutput) -> Handle { + return .init(handler) + } + + public static func buildBlock(_ m0: Handle) -> Handle { + m0 + } + + public static func buildPartialBlock(first: M0) -> M0 { + first + } + + public static func buildPartialBlock( + accumulated m0: M0, + next m1: M1 + ) -> _Middleware2 where M0.Input == M1.Input, M0.Output == M1.Output, M0.Context == M1.Context { + _Middleware2(m0, m1) + } + + /// Build the final result where the input is a single ``Handle`` middleware + public static func buildFinalResult(_ m0: Handle) -> Handle { + m0 + } + + /// Build the final result where input is multiple middleware with the final middleware being a ``Handle`` middleware. + public static func buildFinalResult(_ m0: _Middleware2>) -> _Middleware2> { + m0 + } +} diff --git a/Sources/HummingbirdRouter/RouteGroup.swift b/Sources/HummingbirdRouter/RouteGroup.swift new file mode 100644 index 000000000..814399d63 --- /dev/null +++ b/Sources/HummingbirdRouter/RouteGroup.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2023 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Hummingbird +import ServiceContextModule + +extension ServiceContext { + enum RouteGroupPathKey: ServiceContextKey { + typealias Value = String + } + + /// Current RouteGroup path. This is used to propagate the route path down + /// through the Router result builder + public internal(set) var routeGroupPath: String? { + get { + self[RouteGroupPathKey.self] + } + set { + self[RouteGroupPathKey.self] = newValue + } + } +} + +public struct RouteGroup: HBMiddlewareProtocol where Handler.Input == HBRequest, Handler.Output == HBResponse, Handler.Context == Context { + public typealias Input = HBRequest + public typealias Output = HBResponse + + @usableFromInline + var routerPath: RouterPath + @usableFromInline + var handler: Handler + + public init( + _ routerPath: RouterPath = "", + context: Context.Type = Context.self, + @MiddlewareFixedTypeBuilder builder: () -> Handler + ) { + self.routerPath = routerPath + var serviceContext = ServiceContext.current ?? ServiceContext.topLevel + let parentGroupPath = serviceContext.routeGroupPath ?? "" + serviceContext.routeGroupPath = "\(parentGroupPath)/\(self.routerPath)" + self.handler = ServiceContext.$current.withValue(serviceContext) { + builder() + } + } + + @inlinable + public func handle(_ input: Input, context: Context, next: (Input, Context) async throws -> Output) async throws -> Output { + if let updatedContext = self.routerPath.matchPrefix(context) { + return try await self.handler.handle(input, context: updatedContext) { input, _ in + try await next(input, context) + } + } + return try await next(input, context) + } +} diff --git a/Sources/HummingbirdRouter/RouteHandler.swift b/Sources/HummingbirdRouter/RouteHandler.swift new file mode 100644 index 000000000..3beb6c4f4 --- /dev/null +++ b/Sources/HummingbirdRouter/RouteHandler.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2023 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Hummingbird + +/// Protocol for route handler object. +/// +/// Requires a function that returns a response from a request and context +public protocol RouteHandlerProtocol: Sendable { + associatedtype Context: HBRouterRequestContext + func handle(_ request: HBRequest, context: Context) async throws -> HBResponse +} + +/// Implementatinon of RouteHandleProtocol that uses a closure to produce a response +public struct RouteHandlerClosure: RouteHandlerProtocol { + @usableFromInline + let closure: @Sendable (HBRequest, Context) async throws -> RouteOutput + + @inlinable + public func handle(_ request: HBRequest, context: Context) async throws -> HBResponse { + try await self.closure(request, context).response(from: request, context: context) + } +} + +/// Implementatinon of RouteHandleProtocol that uses a MiddlewareStack to produce a resposne +public struct RouteHandlerMiddleware: RouteHandlerProtocol where M0.Input == HBRequest, M0.Output == HBResponse, M0.Context: HBRouterRequestContext { + public typealias Context = M0.Context + + /// Dummy function passed to middleware handle + @usableFromInline + static func notFound(_: HBRequest, _: Context) -> HBResponse { + .init(status: .notFound) + } + + @usableFromInline + let middleware: M0 + + @inlinable + public func handle(_ request: HBRequest, context: Context) async throws -> HBResponse { + try await self.middleware.handle(request, context: context, next: Self.notFound) + } +} diff --git a/Sources/HummingbirdRouter/Router.swift b/Sources/HummingbirdRouter/Router.swift new file mode 100644 index 000000000..c39b83f97 --- /dev/null +++ b/Sources/HummingbirdRouter/Router.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2023 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Hummingbird + +public struct HBRouterContext { + /// remaining path components to match + @usableFromInline + var remainingPathComponents: ArraySlice + + init() { + self.remainingPathComponents = [] + } +} + +/// Protocol that all request contexts used with HBRouterBuilder should conform to. +public protocol HBRouterRequestContext: HBBaseRequestContext { + var routerContext: HBRouterContext { get set } +} + +/// Router +public struct HBRouter: MiddlewareProtocol where Handler.Input == HBRequest, Handler.Output == HBResponse, Handler.Context == Context +{ + public typealias Input = HBRequest + public typealias Output = HBResponse + + let handler: Handler + + public init(handler: Handler) { + self.handler = handler + } + + public init(context: Context.Type = Context.self, @MiddlewareFixedTypeBuilder builder: () -> Handler) { + self.handler = builder() + } + + public func handle(_ input: Input, context: Context, next: (Input, Context) async throws -> Output) async throws -> Output { + var context = context + context.routerContext.remainingPathComponents = input.uri.path.split(separator: "/")[...] + return try await self.handler.handle(input, context: context, next: next) + } +} + +/// extend Router to conform to HBResponder so we can use it to process `HBRequest`` +extension HBRouter: HBResponder { + public func respond(to request: Input, context: Context) async throws -> Output { + try await self.handle(request, context: context) { _, _ in + throw HBHTTPError(.notFound) + } + } +} diff --git a/Sources/HummingbirdRouter/RouterPath.swift b/Sources/HummingbirdRouter/RouterPath.swift new file mode 100644 index 000000000..06e336553 --- /dev/null +++ b/Sources/HummingbirdRouter/RouterPath.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2021 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@_spi(HBInternal) import Hummingbird + +extension RouterPath { + func matchAll(_ context: Context) -> Context? { + if self.components.count != context.routerContext.remainingPathComponents.count { + if case .recursiveWildcard = self.components.last { + if self.components.count > context.routerContext.remainingPathComponents.count + 1 { + return nil + } + } else { + return nil + } + } + return self.match(context) + } + + @usableFromInline + func matchPrefix(_ context: Context) -> Context? { + if self.components.count > context.routerContext.remainingPathComponents.count { + return nil + } + return self.match(context) + } + + private func match(_ context: Context) -> Context? { + var pathIterator = context.routerContext.remainingPathComponents.makeIterator() + var context = context + for component in self.components { + switch component { + case .path(let lhs): + if lhs != pathIterator.next()! { + return nil + } + case .capture(let key): + context.coreContext.parameters[key] = pathIterator.next()! + + case .prefixCapture(let suffix, let key): + let pathComponent = pathIterator.next()! + if pathComponent.hasSuffix(suffix) { + context.coreContext.parameters[key] = pathComponent.dropLast(suffix.count) + } else { + return nil + } + case .suffixCapture(let prefix, let key): + let pathComponent = pathIterator.next()! + if pathComponent.hasPrefix(prefix) { + context.coreContext.parameters[key] = pathComponent.dropFirst(prefix.count) + } else { + return nil + } + case .wildcard: + break + case .prefixWildcard(let suffix): + if pathIterator.next()!.hasSuffix(suffix) { + } else { + return nil + } + case .suffixWildcard(let prefix): + if pathIterator.next()!.hasPrefix(prefix) { + } else { + return nil + } + case .recursiveWildcard: + var paths = pathIterator.next().map { [$0] } ?? [] + while let pathComponent = pathIterator.next() { + paths.append(pathComponent) + } + context.coreContext.parameters.setCatchAll(paths.joined(separator: "/")[...]) + context.routerContext.remainingPathComponents = [] + return context + case .null: + return nil + } + } + context.routerContext.remainingPathComponents = context.routerContext.remainingPathComponents.dropFirst(self.components.count) + return context + } +}