From d5473153ed43fef4f50567d512be2e01e6135559 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Sat, 22 Jun 2024 14:50:37 +0100 Subject: [PATCH] Add RouterBuilder options and implement lowercased --- Sources/Hummingbird/Router/RouterPath.swift | 21 ++++++++++ Sources/HummingbirdRouter/Route.swift | 8 +++- Sources/HummingbirdRouter/RouteGroup.swift | 35 ++++++++--------- Sources/HummingbirdRouter/RouterBuilder.swift | 33 ++++++++++++++-- .../RouterBuilderState.swift | 38 +++++++++++++++++++ .../HummingbirdRouterTests/RouterTests.swift | 29 ++++++++++++++ 6 files changed, 140 insertions(+), 24 deletions(-) create mode 100644 Sources/HummingbirdRouter/RouterBuilderState.swift diff --git a/Sources/Hummingbird/Router/RouterPath.swift b/Sources/Hummingbird/Router/RouterPath.swift index af52a4196..0d7fa6cb8 100644 --- a/Sources/Hummingbird/Router/RouterPath.swift +++ b/Sources/Hummingbird/Router/RouterPath.swift @@ -79,6 +79,19 @@ public struct RouterPath: Sendable, ExpressibleByStringLiteral, CustomStringConv return false } } + + public func lowercased() -> Self { + switch self { + case .path(let path): + .path(path.lowercased()[...]) + case .prefixCapture(let suffix, let parameter): + .prefixCapture(suffix: suffix.lowercased()[...], parameter: parameter) + case .suffixCapture(let prefix, let parameter): + .suffixCapture(prefix: prefix.lowercased()[...], parameter: parameter) + default: + self + } + } } public let components: [Element] @@ -125,9 +138,17 @@ public struct RouterPath: Sendable, ExpressibleByStringLiteral, CustomStringConv self.init(value) } + internal init(components: [Element]) { + self.components = components + } + public var description: String { self.components.map(\.description).joined(separator: "/") } + + public func lowercased() -> Self { + .init(components: self.map { $0.lowercased() }) + } } extension RouterPath: Collection { diff --git a/Sources/HummingbirdRouter/Route.swift b/Sources/HummingbirdRouter/Route.swift index c3ad8fd31..5ce63cb97 100644 --- a/Sources/HummingbirdRouter/Route.swift +++ b/Sources/HummingbirdRouter/Route.swift @@ -33,6 +33,12 @@ public struct Route String { - let parentGroupPath = ServiceContext.current?.routeGroupPath ?? "" + let parentGroupPath = ServiceContext.current?.routerBuildState?.routeGroupPath ?? "" if path.count > 0 || parentGroupPath.count == 0 { return "\(parentGroupPath)/\(path)" } else { diff --git a/Sources/HummingbirdRouter/RouteGroup.swift b/Sources/HummingbirdRouter/RouteGroup.swift index af8b2d94f..3b5d322bb 100644 --- a/Sources/HummingbirdRouter/RouteGroup.swift +++ b/Sources/HummingbirdRouter/RouteGroup.swift @@ -15,23 +15,6 @@ 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 - } - } -} - /// Router middleware that applies a middleware chain to URIs with a specified prefix public struct RouteGroup: RouterMiddleware where Handler.Input == Request, Handler.Output == Response, Handler.Context == Context { public typealias Input = Request @@ -52,13 +35,25 @@ public struct RouteGroup builder: () -> Handler ) { - self.routerPath = routerPath + var routerPath = routerPath + // Get builder state from service context var serviceContext = ServiceContext.current ?? ServiceContext.topLevel - let parentGroupPath = serviceContext.routeGroupPath ?? "" - serviceContext.routeGroupPath = "\(parentGroupPath)/\(self.routerPath)" + var routerBuildState: RouterBuilderState + if let state = serviceContext.routerBuildState { + routerBuildState = state + } else { + routerBuildState = .init(options: []) + } + if routerBuildState.options.contains(.caseInsensitive) { + routerPath = routerPath.lowercased() + } + let parentGroupPath = routerBuildState.routeGroupPath + routerBuildState.routeGroupPath = "\(parentGroupPath)/\(routerPath)" + serviceContext.routerBuildState = routerBuildState self.handler = ServiceContext.$current.withValue(serviceContext) { builder() } + self.routerPath = routerPath } /// Process HTTP request and return an HTTP response diff --git a/Sources/HummingbirdRouter/RouterBuilder.swift b/Sources/HummingbirdRouter/RouterBuilder.swift index 6860d59b3..4f31405c6 100644 --- a/Sources/HummingbirdRouter/RouterBuilder.swift +++ b/Sources/HummingbirdRouter/RouterBuilder.swift @@ -13,6 +13,19 @@ //===----------------------------------------------------------------------===// import Hummingbird +import ServiceContextModule + +/// Router Options +public struct RouterBuilderOptions: OptionSet, Sendable { + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + /// Router path comparisons will be case insensitive + public static var caseInsensitive: Self { .init(rawValue: 1 << 0) } +} /// Router built using a result builder public struct RouterBuilder: MiddlewareProtocol where Handler.Input == Request, Handler.Output == Response, Handler.Context == Context @@ -21,13 +34,23 @@ public struct RouterBuilder builder: () -> Handler) { - self.handler = builder() + public init( + context: Context.Type = Context.self, + options: RouterBuilderOptions = [], + @MiddlewareFixedTypeBuilder builder: () -> Handler + ) { + var serviceContext = ServiceContext.current ?? ServiceContext.topLevel + serviceContext.routerBuildState = .init(options: options) + self.options = options + self.handler = ServiceContext.$current.withValue(serviceContext) { + builder() + } } /// Process HTTP request and return an HTTP response @@ -38,7 +61,11 @@ public struct RouterBuilder Output) async throws -> Output { var context = context - context.routerContext.remainingPathComponents = input.uri.path.split(separator: "/")[...] + var path = input.uri.path + if self.options.contains(.caseInsensitive) { + path = path.lowercased() + } + context.routerContext.remainingPathComponents = path.split(separator: "/")[...] return try await self.handler.handle(input, context: context, next: next) } } diff --git a/Sources/HummingbirdRouter/RouterBuilderState.swift b/Sources/HummingbirdRouter/RouterBuilderState.swift new file mode 100644 index 000000000..1f408ba3e --- /dev/null +++ b/Sources/HummingbirdRouter/RouterBuilderState.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2024 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 ServiceContextModule + +/// Router builder state used when building Router +internal struct RouterBuilderState { + var routeGroupPath: String = "" + let options: RouterBuilderOptions +} + +extension ServiceContext { + enum RouterBuilderStateKey: ServiceContextKey { + typealias Value = RouterBuilderState + } + + /// Current RouteGroup path. This is used to propagate the route path down + /// through the Router result builder + internal var routerBuildState: RouterBuilderState? { + get { + self[RouterBuilderStateKey.self] + } + set { + self[RouterBuilderStateKey.self] = newValue + } + } +} diff --git a/Tests/HummingbirdRouterTests/RouterTests.swift b/Tests/HummingbirdRouterTests/RouterTests.swift index 1b998c6b5..4d3c15ed0 100644 --- a/Tests/HummingbirdRouterTests/RouterTests.swift +++ b/Tests/HummingbirdRouterTests/RouterTests.swift @@ -443,6 +443,35 @@ final class RouterTests: XCTestCase { } } } + + // Test case insensitive router works + func testCaseInsensitive() async throws { + let router = RouterBuilder(context: BasicRouterRequestContext.self, options: .caseInsensitive) { + Get("Uppercased") { _, _ in + return HTTPResponse.Status.ok + } + Get("lowercased") { _, _ in + return HTTPResponse.Status.ok + } + RouteGroup("group") { + Get("Uppercased") { _, _ in + return HTTPResponse.Status.ok + } + } + } + let app = Application(responder: router.buildResponder()) + try await app.test(.router) { client in + try await client.execute(uri: "/uppercased", method: .get) { response in + XCTAssertEqual(response.status, .ok) + } + try await client.execute(uri: "/LOWERCASED", method: .get) { response in + XCTAssertEqual(response.status, .ok) + } + try await client.execute(uri: "/Group/uppercased", method: .get) { response in + XCTAssertEqual(response.status, .ok) + } + } + } } public struct TestRouterContext2: RouterRequestContext, RequestContext {