Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions Sources/Hummingbird/Router/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,6 @@ public final class Router<Context: RequestContext>: RouterMethods, HTTPResponder
method: HTTPRequest.Method,
responder: Responder
) -> Self where Responder.Context == Context {
var path = path
if self.options.contains(.caseInsensitive) {
path = path.lowercased()
}
self.trie.addEntry(path, value: EndpointResponders(path: path)) { node in
node.value!.addResponder(for: method, responder: self.middlewares.constructResponder(finalResponder: responder))
}
Expand Down
34 changes: 34 additions & 0 deletions Sources/Hummingbird/Router/RouterPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
//
//===----------------------------------------------------------------------===//

internal import Foundation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to use FoundationEssentials, caseInsensitiveCompare should be available


/// Split router path into components
public struct RouterPath: Sendable, ExpressibleByStringLiteral, ExpressibleByStringInterpolation, CustomStringConvertible, Equatable {
public struct Element: Equatable, Sendable, CustomStringConvertible {
Expand Down Expand Up @@ -151,6 +153,38 @@ public struct RouterPath: Sendable, ExpressibleByStringLiteral, ExpressibleByStr
}
}

public func caseInsensitiveEquals(_ rhs: some StringProtocol) -> Bool {
switch value {
case .path(let lhs):
return lhs.caseInsensitiveCompare(rhs) == .orderedSame
default:
return false
}
}

public func caseInsensitiveMatch(_ rhs: some StringProtocol) -> Bool {
switch value {
case .path(let lhs):
return lhs.caseInsensitiveCompare(rhs) == .orderedSame
case .capture:
return true
case .prefixCapture(let suffix, _):
return rhs.hasSuffix(suffix)
case .suffixCapture(let prefix, _):
return rhs.hasPrefix(prefix)
case .wildcard:
return true
case .prefixWildcard(let suffix):
return rhs.hasSuffix(suffix)
case .suffixWildcard(let prefix):
return rhs.hasPrefix(prefix)
case .recursiveWildcard:
return true
case .null:
return false
}
}

/// Return lowercased version of RouterPath component
public func lowercased() -> Self {
switch self.value {
Expand Down
9 changes: 2 additions & 7 deletions Sources/Hummingbird/Router/RouterResponder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public struct RouterResponder<Context: RequestContext>: HTTPResponder {
options: RouterOptions,
notFoundResponder: any HTTPResponder<Context>
) {
self.trie = RouterTrie(base: trie)
self.trie = RouterTrie(base: trie, options: options)
self.options = options
self.notFoundResponder = notFoundResponder
}
Expand All @@ -43,12 +43,7 @@ public struct RouterResponder<Context: RequestContext>: HTTPResponder {
@inlinable
public func respond(to request: Request, context: Context) async throws -> Response {
do {
let path: String
if self.options.contains(.caseInsensitive) {
path = request.uri.path.lowercased()
} else {
path = request.uri.path
}
let path = request.uri.path
guard
let (responderChain, parameters) = trie.resolve(path),
let responder = responderChain.getResponder(for: request.method)
Expand Down
6 changes: 5 additions & 1 deletion Sources/Hummingbird/Router/Trie/RouterTrie.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ public final class RouterTrie<Value: Sendable>: Sendable {
@usableFromInline
let values: [Value?]

@_spi(Internal) public init(base: RouterPathTrieBuilder<Value>) {
@usableFromInline
let options: RouterOptions

@_spi(Internal) public init(base: RouterPathTrieBuilder<Value>, options: RouterOptions = []) {
var trie = Trie()
var values: [Value?] = []

Expand All @@ -84,5 +87,6 @@ public final class RouterTrie<Value: Sendable>: Sendable {

self.trie = trie
self.values = values
self.options = options
}
}
56 changes: 48 additions & 8 deletions Sources/Hummingbird/Router/Trie/Trie+resolve.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@
//
//===----------------------------------------------------------------------===//

internal import Foundation
import NIOCore

extension RouterTrie {
/// Resolve a path to a `Value` if available
@inlinable
public func resolve(_ path: String) -> (value: Value, parameters: Parameters)? {
var context = ResolveContext(path: path, trie: trie, values: values)
var context = ResolveContext(
path: path,
trie: trie,
values: values,
caseInsensitive: self.options.contains(.caseInsensitive)
)
return context.resolve()
}

Expand All @@ -29,12 +35,19 @@ extension RouterTrie {
@usableFromInline let trie: Trie
@usableFromInline let values: [Value?]
@usableFromInline var parameters = Parameters()

@usableFromInline init(path: String, trie: Trie, values: [Value?]) {
@usableFromInline let caseInsensitive: Bool

@usableFromInline init(
path: String,
trie: Trie,
values: [Value?],
caseInsensitive: Bool
) {
self.path = path
self.trie = trie
self.pathComponents = path.split(separator: "/", omittingEmptySubsequences: true)
self.values = values
self.caseInsensitive = caseInsensitive
}

@usableFromInline func nextPathComponent(advancingIndex index: inout Int) -> Substring? {
Expand Down Expand Up @@ -154,12 +167,39 @@ extension RouterTrie {
case match, mismatch, ignore, deadEnd
}

@usableFromInline
func equals(_ lhs: Substring, _ rhs: Substring) -> Bool {
if self.caseInsensitive {
return lhs.caseInsensitiveCompare(rhs) == .orderedSame
} else {
return lhs == rhs
}
}

@usableFromInline
func hasPrefix(_ lhs: Substring, _ rhs: Substring) -> Bool {
if self.caseInsensitive {
return lhs.prefix(rhs.count).caseInsensitiveCompare(rhs) == .orderedSame
} else {
return lhs.hasPrefix(rhs)
}
}

@usableFromInline
func hasSuffix(_ lhs: Substring, _ rhs: Substring) -> Bool {
if self.caseInsensitive {
return lhs.suffix(rhs.count).caseInsensitiveCompare(rhs) == .orderedSame
} else {
return lhs.hasSuffix(rhs)
}
}

@inlinable
mutating func matchComponent(_ component: Substring, node: TrieNode) -> MatchResult {
switch node.token {
case .path(let constant):
// The current node is a constant
if self.trie.stringValues[Int(constant)] == component {
if equals(self.trie.stringValues[Int(constant)], component) {
return .match
}

Expand All @@ -170,15 +210,15 @@ extension RouterTrie {
case .prefixCapture(let parameter, let suffix):
let suffix = self.trie.stringValues[Int(suffix)]

if component.hasSuffix(suffix) {
if hasSuffix(component, suffix) {
self.parameters[self.trie.stringValues[Int(parameter)]] = component.dropLast(suffix.count)
return .match
}

return .mismatch
case .suffixCapture(let prefix, let parameter):
let prefix = self.trie.stringValues[Int(prefix)]
if component.hasPrefix(prefix) {
if hasPrefix(component, prefix) {
self.parameters[self.trie.stringValues[Int(parameter)]] = component.dropFirst(prefix.count)
return .match
}
Expand All @@ -188,13 +228,13 @@ extension RouterTrie {
// Always matches, descend
return .match
case .prefixWildcard(let suffix):
if component.hasSuffix(self.trie.stringValues[Int(suffix)]) {
if hasSuffix(component, self.trie.stringValues[Int(suffix)]) {
return .match
}

return .mismatch
case .suffixWildcard(let prefix):
if component.hasPrefix(self.trie.stringValues[Int(prefix)]) {
if hasPrefix(component, self.trie.stringValues[Int(prefix)]) {
return .match
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Hummingbird/Router/TrieRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ import HummingbirdCore
if let child = self.children.first(where: { $0.key == key }) {
return child
}
return self.children.first { $0.key ~= key }
return self.children.first { $0.key.caseInsensitiveMatch(key) }
}

func forEach(_ process: (Node) throws -> Void) rethrows {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
//===----------------------------------------------------------------------===//

import AsyncHTTPClient
import Foundation
import HTTPTypes
import Hummingbird
import HummingbirdCore
Expand Down Expand Up @@ -220,7 +221,7 @@ extension HTTPFields {
self.reserveCapacity(count)
var firstHost = true
for field in oldHeaders {
if firstHost, field.name.lowercased() == "host" {
if firstHost, field.name.caseInsensitiveCompare("host") == .orderedSame {
firstHost = false
continue
}
Expand Down
Loading