Skip to content

Commit

Permalink
Run router before any middleware (hummingbird-project#156)
Browse files Browse the repository at this point in the history
* Run router before any middleware

Split TrieRouter into two, the builder and the responder
EndpointResponder returns the responder for a method, instead of calling the responder
Collapsed the builder part of TrieRouter into HBRouterBuilder

* Add tests, fix formatting

* Make RouterBuilder a class

* Add changes to get websockets compiling

* Swift format
  • Loading branch information
adam-fowler committed Jan 16, 2023
1 parent d643afc commit 3db5d72
Show file tree
Hide file tree
Showing 12 changed files with 219 additions and 143 deletions.
14 changes: 7 additions & 7 deletions Sources/Hummingbird/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Copyright (c) 2021-2023 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand Down Expand Up @@ -44,10 +44,8 @@ public final class HBApplication: HBExtensible {
public let eventLoopGroup: EventLoopGroup
/// thread pool used by application
public let threadPool: NIOThreadPool
/// middleware applied to requests
public let middleware: HBMiddlewareGroup
/// routes requests to requestResponders based on URI
public var router: HBRouter
public var router: HBRouterBuilder
/// http server
public var server: HBHTTPServer
/// Configuration
Expand Down Expand Up @@ -76,8 +74,7 @@ public final class HBApplication: HBExtensible {
logger.logLevel = configuration.logLevel
self.logger = logger

self.router = TrieRouter()
self.middleware = HBMiddlewareGroup()
self.router = HBRouterBuilder()
self.configuration = configuration
self.extensions = HBExtensions()
self.encoder = NullEncoder()
Expand Down Expand Up @@ -160,9 +157,12 @@ public final class HBApplication: HBExtensible {
self.lifecycle.shutdown()
}

/// middleware applied to requests
public var middleware: HBMiddlewareGroup { return self.router.middlewares }

/// Construct the RequestResponder from the middleware group and router
public func constructResponder() -> HBResponder {
return self.middleware.constructResponder(finalResponder: self.router)
return self.router.buildRouter()
}

/// shutdown eventloop, threadpool and any extensions attached to the Application
Expand Down
4 changes: 2 additions & 2 deletions Sources/Hummingbird/AsyncAwaitSupport/Router+async.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Copyright (c) 2021-2023 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand Down Expand Up @@ -93,7 +93,7 @@ extension HBRouterMethods {
}

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension HBRouter {
extension HBRouterBuilder {
/// Add path for closure returning type conforming to ResponseFutureEncodable
@discardableResult public func on<Output: HBResponseGenerator>(
_ path: String,
Expand Down
8 changes: 6 additions & 2 deletions Sources/Hummingbird/Middleware/MiddlewareGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
public final class HBMiddlewareGroup {
var middlewares: [HBMiddleware]

init() {
/// Initialize `HBMiddlewareGroup`
///
/// Set middleware array to be empty
public init() {
// this is set by WebSocketRouterGroup so this needs to be kept public
self.middlewares = []
}

Expand All @@ -32,7 +36,7 @@ public final class HBMiddlewareGroup {
/// Construct responder chain from this middleware group
/// - Parameter finalResponder: The responder the last middleware calls
/// - Returns: Responder chain
func constructResponder(finalResponder: HBResponder) -> HBResponder {
public func constructResponder(finalResponder: HBResponder) -> HBResponder {
var currentResponser = finalResponder
for i in (0..<self.middlewares.count).reversed() {
let responder = MiddlewareResponder(middleware: middlewares[i], next: currentResponser)
Expand Down
13 changes: 5 additions & 8 deletions Sources/Hummingbird/Router/EndpointResponder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Copyright (c) 2021-2023 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand All @@ -15,18 +15,15 @@
import NIOCore
import NIOHTTP1

/// Responder that chooses the next responder to call based on the request method
final class HBEndpointResponder: HBResponder {
/// Stores endpoint responders for each HTTP method
final class HBEndpointResponders {
init(path: String) {
self.path = path
self.methods = [:]
}

public func respond(to request: HBRequest) -> EventLoopFuture<HBResponse> {
guard let responder = methods[request.method.rawValue] else {
return request.failure(HBHTTPError(.notFound))
}
return responder.respond(to: request)
public func getResponder(for method: HTTPMethod) -> HBResponder? {
return self.methods[method.rawValue]
}

func addResponder(for method: HTTPMethod, responder: HBResponder) {
Expand Down
88 changes: 22 additions & 66 deletions Sources/Hummingbird/Router/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Copyright (c) 2021-2023 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand All @@ -12,75 +12,31 @@
//
//===----------------------------------------------------------------------===//

import HummingbirdCore
import NIOCore
import NIOHTTP1

/// Directs Requests to handlers based on the request uri.
/// 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>`.
///
/// `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
/// ```
/// app.router.get("string") { _ -> String in
/// return "string"
/// }
/// app.router.post("status") { _ -> HTTPResponseStatus in
/// return .ok
/// }
/// app.router.data("data") { request -> ByteBuffer in
/// return request.allocator.buffer(string: "buffer")
/// }
/// ```
/// Routes can also return `EventLoopFuture`'s. So you can support returning values from
/// asynchronous processes.
///
/// 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
/// ```
/// app.router.get("user/*", use: anyUser)
/// app.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 protocol HBRouter: HBRouterMethods, HBResponder {
/// Add router entry
func add(_ path: String, method: HTTPMethod, responder: HBResponder)
}

extension HBRouter {
/// Add path for closure returning type conforming to ResponseFutureEncodable
@discardableResult public func on<Output: HBResponseGenerator>(
_ path: String,
method: HTTPMethod,
options: HBRouterMethodOptions = [],
use closure: @escaping (HBRequest) throws -> Output
) -> Self {
let responder = constructResponder(options: options, use: closure)
add(path, method: method, responder: responder)
return self
}

/// Add path for closure returning type conforming to ResponseFutureEncodable
@discardableResult public func on<Output: HBResponseGenerator>(
_ path: String,
method: HTTPMethod,
options: HBRouterMethodOptions = [],
use closure: @escaping (HBRequest) -> EventLoopFuture<Output>
) -> Self {
let responder = constructResponder(options: options, use: closure)
add(path, method: method, responder: responder)
return self
}
struct HBRouter: HBResponder {
let trie: RouterPathTrie<HBEndpointResponders>
let notFoundResponder: HBResponder

/// return new `RouterGroup`
/// - Parameter path: prefix to add to paths inside the group
public func group(_ path: String = "") -> HBRouterGroup {
return .init(path: path, router: self)
/// 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) -> EventLoopFuture<HBResponse> {
let path = request.uri.path
guard let result = trie.getValueAndParameters(path),
let responder = result.value.getResponder(for: request.method)
else {
return self.notFoundResponder.respond(to: request)
}
var request = request
if result.parameters.count > 0 {
request.parameters = result.parameters
}
// store endpoint path in request (mainly for metrics)
request.endpointPath = result.value.path
return responder.respond(to: request)
}
}
111 changes: 111 additions & 0 deletions Sources/Hummingbird/Router/RouterBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

import HummingbirdCore
import NIOCore
import NIOHTTP1

/// `HBRouterBuilder` 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
/// ```
/// app.router.get("string") { _ -> String in
/// return "string"
/// }
/// app.router.post("status") { _ -> HTTPResponseStatus in
/// return .ok
/// }
/// app.router.data("data") { request -> ByteBuffer in
/// return request.allocator.buffer(string: "buffer")
/// }
/// ```
/// Routes can also return `EventLoopFuture`'s. So you can support returning values from
/// asynchronous processes.
///
/// 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
/// ```
/// app.router.get("user/*", use: anyUser)
/// app.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 HBRouterBuilder: HBRouterMethods {
var trie: RouterPathTrie<HBEndpointResponders>
public let middlewares: HBMiddlewareGroup

public init() {
self.trie = RouterPathTrie()
self.middlewares = .init()
}

/// Add route to router
/// - Parameters:
/// - path: URI path
/// - method: http method
/// - responder: handler to call
public func add(_ path: String, method: HTTPMethod, responder: HBResponder) {
self.trie.addEntry(.init(path), value: HBEndpointResponders(path: path)) { node in
node.value!.addResponder(for: method, responder: middlewares.constructResponder(finalResponder: responder))
}
}

func endpoint(_ path: String) -> HBEndpointResponders? {
self.trie.getValueAndParameters(path)?.value
}

/// build router
public func buildRouter() -> HBResponder {
HBRouter(trie: self.trie, notFoundResponder: self.middlewares.constructResponder(finalResponder: NotFoundResponder()))
}

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

/// Add path for closure returning type conforming to ResponseFutureEncodable
@discardableResult public func on<Output: HBResponseGenerator>(
_ path: String,
method: HTTPMethod,
options: HBRouterMethodOptions = [],
use closure: @escaping (HBRequest) -> EventLoopFuture<Output>
) -> 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 {
return .init(path: path, router: self)
}
}

/// Responder that return a not found error
struct NotFoundResponder: HBResponder {
func respond(to request: HBRequest) -> NIOCore.EventLoopFuture<HBResponse> {
return request.eventLoop.makeFailedFuture(HBHTTPError(.notFound))
}
}
6 changes: 3 additions & 3 deletions Sources/Hummingbird/Router/RouterGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Copyright (c) 2021-2023 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand Down Expand Up @@ -32,10 +32,10 @@ import NIOHTTP1
/// ```
public struct HBRouterGroup: HBRouterMethods {
let path: String
let router: HBRouter
let router: HBRouterBuilder
let middlewares: HBMiddlewareGroup

init(path: String = "", middlewares: HBMiddlewareGroup = .init(), router: HBRouter) {
init(path: String = "", middlewares: HBMiddlewareGroup = .init(), router: HBRouterBuilder) {
self.path = path
self.router = router
self.middlewares = middlewares
Expand Down
Loading

0 comments on commit 3db5d72

Please sign in to comment.