Skip to content

Commit

Permalink
RouterBuilder fixes, name changes
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-fowler committed Dec 5, 2023
1 parent 0921e19 commit e069eb4
Show file tree
Hide file tree
Showing 11 changed files with 804 additions and 144 deletions.
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ let package = Package(
.byName(name: "HummingbirdJobs"),
.byName(name: "HummingbirdXCT"),
]),
.testTarget(name: "HummingbirdRouterTests", dependencies: [
.byName(name: "HummingbirdRouter"),
.byName(name: "HummingbirdXCT"),
]),
.testTarget(
name: "HummingbirdCoreTests",
dependencies:
Expand Down
102 changes: 77 additions & 25 deletions Sources/Hummingbird/Router/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,88 @@
//
//===----------------------------------------------------------------------===//

/// Directs requests to handlers based on the request uri and method.
import HTTPTypes
import HummingbirdCore
import NIOCore

/// Create rules for routing requests and then create `HBResponder` that will follow these rules.
///
/// Conforms to `HBResponder` so need to provide its own implementation of
/// `func apply(to request: Request) -> EventLoopFuture<Response>`.
/// `HBRouter` requires an implementation of the `on(path:method:use)` functions but because it
/// also conforms to `HBRouterMethods` it is also possible to call the method specific functions `get`, `put`,
/// `head`, `post` and `patch`. The route handler closures all return objects conforming to
/// `HBResponseGenerator`. This allows us to support routes which return a multitude of types eg
/// ```
/// router.get("string") { _ -> String in
/// return "string"
/// }
/// router.post("status") { _ -> HTTPResponseStatus in
/// return .ok
/// }
/// router.data("data") { request -> ByteBuffer in
/// return context.allocator.buffer(string: "buffer")
/// }
/// ```
/// Routes can also return `EventLoopFuture`'s. So you can support returning values from
/// asynchronous processes.
///
struct HBRouterResponder<Context: HBBaseRequestContext>: HBResponder {
let trie: RouterPathTrie<HBEndpointResponders<Context>>
let notFoundResponder: any HBResponder<Context>
/// The default `Router` setup in `HBApplication` is the `TrieRouter` . This uses a
/// trie to partition all the routes for faster access. It also supports wildcards and parameter extraction
/// ```
/// router.get("user/*", use: anyUser)
/// router.get("user/:id", use: userWithId)
/// ```
/// Both of these match routes which start with "/user" and the next path segment being anything.
/// The second version extracts the path segment out and adds it to `HBRequest.parameters` with the
/// key "id".
public final class HBRouter<Context: HBBaseRequestContext>: HBRouterMethods {
var trie: RouterPathTrieBuilder<HBEndpointResponders<Context>>
public let middlewares: HBMiddlewareGroup<Context>

init(context: Context.Type, trie: RouterPathTrie<HBEndpointResponders<Context>>, notFoundResponder: any HBResponder<Context>) {
self.trie = trie
self.notFoundResponder = notFoundResponder
public init(context: Context.Type = HBBasicRequestContext.self) {
self.trie = RouterPathTrieBuilder()
self.middlewares = .init()
}

/// Respond to request by calling correct handler
/// - Parameter request: HTTP request
/// - Returns: EventLoopFuture that will be fulfilled with the Response
public func respond(to request: HBRequest, context: Context) async throws -> HBResponse {
let path = request.uri.path
guard let result = trie.getValueAndParameters(path),
let responder = result.value.getResponder(for: request.method)
else {
return try await self.notFoundResponder.respond(to: request, context: context)
}
var context = context
if let parameters = result.parameters {
context.coreContext.parameters = parameters
/// Add route to router
/// - Parameters:
/// - path: URI path
/// - method: http method
/// - responder: handler to call
public func add(_ path: String, method: HTTPRequest.Method, responder: any HBResponder<Context>) {
// ensure path starts with a "/" and doesn't end with a "/"
let path = "/\(path.dropSuffix("/").dropPrefix("/"))"
self.trie.addEntry(.init(path), value: HBEndpointResponders(path: path)) { node in
node.value!.addResponder(for: method, responder: self.middlewares.constructResponder(finalResponder: responder))
}
// store endpoint path in request (mainly for metrics)
context.coreContext.endpointPath.value = result.value.path
return try await responder.respond(to: request, context: context)
}

/// build router
public func buildResponder() -> some HBResponder<Context> {
HBRouterResponder(context: Context.self, trie: self.trie.build(), notFoundResponder: self.middlewares.constructResponder(finalResponder: NotFoundResponder<Context>()))
}

/// Add path for closure returning type conforming to ResponseFutureEncodable
@discardableResult public func on(
_ path: String,
method: HTTPRequest.Method,
options: HBRouterMethodOptions = [],
use closure: @escaping @Sendable (HBRequest, Context) async throws -> some HBResponseGenerator
) -> Self {
let responder = constructResponder(options: options, use: closure)
self.add(path, method: method, responder: responder)
return self
}

/// return new `RouterGroup`
/// - Parameter path: prefix to add to paths inside the group
public func group(_ path: String = "") -> HBRouterGroup<Context> {
return .init(path: path, router: self)
}
}

/// Responder that return a not found error
struct NotFoundResponder<Context: HBBaseRequestContext>: HBResponder {
func respond(to request: HBRequest, context: Context) throws -> HBResponse {
throw HBHTTPError(.notFound)
}
}
99 changes: 0 additions & 99 deletions Sources/Hummingbird/Router/RouterBuilder.swift

This file was deleted.

31 changes: 29 additions & 2 deletions Sources/Hummingbird/Router/RouterPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
//===----------------------------------------------------------------------===//

/// Split router path into components
public struct RouterPath: Sendable, ExpressibleByStringLiteral {
public enum Element: Equatable, Sendable {
public struct RouterPath: Sendable, ExpressibleByStringLiteral, CustomStringConvertible {
public enum Element: Equatable, Sendable, CustomStringConvertible {
case path(Substring)
case capture(Substring)
case prefixCapture(suffix: Substring, parameter: Substring) // *.jpg
Expand All @@ -25,6 +25,29 @@ public struct RouterPath: Sendable, ExpressibleByStringLiteral {
case recursiveWildcard
case null

public var description: String {
switch self {
case .path(let path):
return String(path)
case .capture(let parameter):
return "${\(parameter)}"
case .prefixCapture(let suffix, let parameter):
return "${\(parameter)}\(suffix)"
case .suffixCapture(let prefix, let parameter):
return "\(prefix)${\(parameter)}"
case .wildcard:
return "*"
case .prefixWildcard(let suffix):
return "*\(suffix)"
case .suffixWildcard(let prefix):
return "\(prefix)*"
case .recursiveWildcard:
return "**"
case .null:
return "!"
}
}

static func ~= (lhs: Element, rhs: some StringProtocol) -> Bool {
switch lhs {
case .path(let lhs):
Expand Down Expand Up @@ -100,6 +123,10 @@ public struct RouterPath: Sendable, ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self.init(value)
}

public var description: String {
self.components.map(\.description).joined(separator: "/")
}
}

extension RouterPath: Collection {
Expand Down
47 changes: 47 additions & 0 deletions Sources/Hummingbird/Router/RouterResponder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-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
//
//===----------------------------------------------------------------------===//

/// Directs requests to handlers based on the request uri and method.
///
/// Conforms to `HBResponder` so need to provide its own implementation of
/// `func apply(to request: Request) -> EventLoopFuture<Response>`.
///
struct HBRouterResponder<Context: HBBaseRequestContext>: HBResponder {
let trie: RouterPathTrie<HBEndpointResponders<Context>>
let notFoundResponder: any HBResponder<Context>

init(context: Context.Type, trie: RouterPathTrie<HBEndpointResponders<Context>>, notFoundResponder: any HBResponder<Context>) {
self.trie = trie
self.notFoundResponder = notFoundResponder
}

/// Respond to request by calling correct handler
/// - Parameter request: HTTP request
/// - Returns: EventLoopFuture that will be fulfilled with the Response
public func respond(to request: HBRequest, context: Context) async throws -> HBResponse {
let path = request.uri.path
guard let result = trie.getValueAndParameters(path),
let responder = result.value.getResponder(for: request.method)
else {
return try await self.notFoundResponder.respond(to: request, context: context)
}
var context = context
if let parameters = result.parameters {
context.coreContext.parameters = parameters
}
// store endpoint path in request (mainly for metrics)
context.coreContext.endpointPath.value = result.value.path
return try await responder.respond(to: request, context: context)
}
}
Loading

0 comments on commit e069eb4

Please sign in to comment.