Skip to content

Commit

Permalink
Add RouterBuilder options and implement lowercased
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-fowler committed Jun 22, 2024
1 parent c7d0a6f commit d547315
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 24 deletions.
21 changes: 21 additions & 0 deletions Sources/Hummingbird/Router/RouterPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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 {
Expand Down
8 changes: 7 additions & 1 deletion Sources/HummingbirdRouter/Route.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ public struct Route<Handler: _RouteHandlerProtocol, Context: RouterRequestContex
/// - routerPath: Route path, relative to group route is defined in
/// - handler: Route handler
init(_ method: HTTPRequest.Method, _ routerPath: RouterPath = "", handler: Handler) {
let serviceContext = ServiceContext.current ?? ServiceContext.topLevel
let options = serviceContext.routerBuildState?.options ?? []
var routerPath = routerPath
if options.contains(.caseInsensitive) {
routerPath = routerPath.lowercased()
}
self.method = method
self.routerPath = routerPath
self.handler = handler
Expand Down Expand Up @@ -89,7 +95,7 @@ public struct Route<Handler: _RouteHandlerProtocol, Context: RouterRequestContex

/// Return full path of route, using Task local stored `routeGroupPath`.
static func getFullPath(from path: RouterPath) -> String {
let parentGroupPath = ServiceContext.current?.routeGroupPath ?? ""
let parentGroupPath = ServiceContext.current?.routerBuildState?.routeGroupPath ?? ""
if path.count > 0 || parentGroupPath.count == 0 {
return "\(parentGroupPath)/\(path)"
} else {
Expand Down
35 changes: 15 additions & 20 deletions Sources/HummingbirdRouter/RouteGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Context: RouterRequestContext, Handler: MiddlewareProtocol>: RouterMiddleware where Handler.Input == Request, Handler.Output == Response, Handler.Context == Context {
public typealias Input = Request
Expand All @@ -52,13 +35,25 @@ public struct RouteGroup<Context: RouterRequestContext, Handler: MiddlewareProto
_ routerPath: RouterPath,
@MiddlewareFixedTypeBuilder<Request, Response, Context> 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
Expand Down
33 changes: 30 additions & 3 deletions Sources/HummingbirdRouter/RouterBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Context: RouterRequestContext, Handler: MiddlewareProtocol>: MiddlewareProtocol where Handler.Input == Request, Handler.Output == Response, Handler.Context == Context
Expand All @@ -21,13 +34,23 @@ public struct RouterBuilder<Context: RouterRequestContext, Handler: MiddlewarePr
public typealias Output = Response

let handler: Handler
let options: RouterBuilderOptions

/// Initialize RouterBuilder with contents of result builder
/// - Parameters:
/// - context: Request context used by router
/// - builder: Result builder for router
public init(context: Context.Type = Context.self, @MiddlewareFixedTypeBuilder<Input, Output, Context> builder: () -> Handler) {
self.handler = builder()
public init(
context: Context.Type = Context.self,
options: RouterBuilderOptions = [],
@MiddlewareFixedTypeBuilder<Input, Output, Context> 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
Expand All @@ -38,7 +61,11 @@ public struct RouterBuilder<Context: RouterRequestContext, Handler: MiddlewarePr
/// - Returns: HTTP Response
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: "/")[...]
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)
}
}
Expand Down
38 changes: 38 additions & 0 deletions Sources/HummingbirdRouter/RouterBuilderState.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
29 changes: 29 additions & 0 deletions Tests/HummingbirdRouterTests/RouterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit d547315

Please sign in to comment.