Skip to content

Commit

Permalink
Add route controller for better composability. (#565)
Browse files Browse the repository at this point in the history
  • Loading branch information
connor-ricks committed Oct 11, 2024
1 parent 913e6a0 commit 6dc2eee
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
//
//===----------------------------------------------------------------------===//

// MARK: - Middleware2

public struct _Middleware2<M0: MiddlewareProtocol, M1: MiddlewareProtocol>: MiddlewareProtocol where M0.Input == M1.Input, M0.Context == M1.Context, M0.Output == M1.Output {
public typealias Input = M0.Input
public typealias Output = M0.Output
Expand All @@ -34,6 +36,10 @@ public struct _Middleware2<M0: MiddlewareProtocol, M1: MiddlewareProtocol>: Midd
}
}

extension _Middleware2: RouterMiddleware where M0.Input == Request, M0.Output == Response {}

// MARK: - MiddlewareFixedTypeBuilder

/// Middleware stack result builder
///
/// Generates a middleware stack from the elements inside the result builder. The input,
Expand Down
48 changes: 48 additions & 0 deletions Sources/HummingbirdRouter/RouterController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2023-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 Hummingbird

// MARK: - RouterController

/// A type that represents part of your app's middleware and routes
///
/// You create custom controllers by declaring types that conform to the `RouterController`
/// protocol. Implement the required ``RouterController/body-swift.property`` computed
/// property to provide the content for your custom controller.
///
/// struct MyController: RouterController {
/// typealias Context = BasicRouterRequestContext
///
/// var body: some RouterMiddleware<Context> {
/// Get("foo") { _,_ in "foo" }
/// }
/// }
///
/// Assemble the controller's body by combining one or more of the built-in controllers or middleware.
/// provided by Hummingbird, plus other custom controllers that you define, into a hierarchy of controllers.
public protocol RouterController<Context> {
associatedtype Context
associatedtype Body: RouterMiddleware<Context>
@MiddlewareFixedTypeBuilder<Request, Response, Context> var body: Body { get }
}


// MARK: MiddlewareFixedTypeBuilder + RouterController Builders

extension MiddlewareFixedTypeBuilder {
public static func buildExpression<C0: RouterController>(_ c0: C0) -> C0.Body where C0.Body.Input == Input, C0.Body.Output == Output, C0.Body.Context == Context {
return c0.body
}
}
152 changes: 152 additions & 0 deletions Tests/HummingbirdRouterTests/ControllerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//===----------------------------------------------------------------------===//
//
// 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 Hummingbird
import HummingbirdRouter
import HummingbirdTesting
import XCTest

final class ControllerTests: XCTestCase {
func testRouterControllerWithSingleRoute() async throws {
struct TestController: RouterController {
typealias Context = BasicRouterRequestContext
var body: some RouterMiddleware<Context> {
Get("foo") { _,_ in "foo" }
}
}

let router = RouterBuilder(context: BasicRouterRequestContext.self) {
TestController()
}

let app = Application(responder: router)
try await app.test(.router) { client in
try await client.execute(uri: "/foo", method: .get) {
XCTAssertEqual(String(buffer: $0.body), "foo")
}
}
}

func testRouterControllerWithMultipleRoutes() async throws {
struct TestController: RouterController {
typealias Context = BasicRouterRequestContext
var body: some RouterMiddleware<Context> {
Get("foo") { _,_ in "foo" }
Get("bar") { _,_ in "bar" }
}
}

let router = RouterBuilder(context: BasicRouterRequestContext.self) {
TestController()
}

let app = Application(responder: router)
try await app.test(.router) { client in
try await client.execute(uri: "/foo", method: .get) {
XCTAssertEqual(String(buffer: $0.body), "foo")
}

try await client.execute(uri: "/bar", method: .get) {
XCTAssertEqual(String(buffer: $0.body), "bar")
}
}
}

func testRouterControllerWithGenericChildren() async throws {
struct ChildController: RouterController {
typealias Context = BasicRouterRequestContext
let name: String
var body: some RouterMiddleware<Context> {
Get("child_\(name)") { _,_ in "child_\(name)" }
}
}

struct ParentController<Context: RouterRequestContext, Child: RouterMiddleware>: RouterController where Child.Context == Context {
var child: Child

init(@MiddlewareFixedTypeBuilder<Request, Response, Context> _ child: () -> Child) {
self.child = child()
}

var body: some RouterMiddleware<Context> {
RouteGroup("parent") {
child
}
}
}

let router = RouterBuilder(context: BasicRouterRequestContext.self) {
ParentController {
Get("child_a") { _,_ in "child_a"}
ChildController(name: "b")
ChildController(name: "c")
Get("child_d") { _,_ in "child_d"}
}
}

let app = Application(responder: router)
try await app.test(.router) { client in
for letter in "abcd" {
try await client.execute(uri: "/parent/child_\(letter)", method: .get) {
XCTAssertEqual(String(buffer: $0.body), "child_\(letter)")
}
}
}
}

func testRouterControllerWithMiddleware() async throws {
struct TestMiddleware<Context: RequestContext>: RouterMiddleware {
func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
var response = try await next(request, context)
response.headers[.middleware] = "TestMiddleware"
return response
}
}

struct ChildController: RouterController {
typealias Context = BasicRouterRequestContext
var body: some RouterMiddleware<Context> {
Get("foo") { _,_ in "foo" }
}
}

struct ParentController: RouterController {
typealias Context = BasicRouterRequestContext
var body: some RouterMiddleware<Context> {
RouteGroup("parent") {
TestMiddleware()
ChildController()
Get("bar") { _,_ in "bar" }
}
}
}

let router = RouterBuilder(context: BasicRouterRequestContext.self) {
ParentController()
}

let app = Application(responder: router)
try await app.test(.router) { client in
try await client.execute(uri: "/parent/foo", method: .get) {
XCTAssertEqual($0.headers[.middleware], "TestMiddleware")
XCTAssertEqual(String(buffer: $0.body), "foo")
}

try await client.execute(uri: "/parent/bar", method: .get) {
XCTAssertEqual($0.headers[.middleware], "TestMiddleware")
XCTAssertEqual(String(buffer: $0.body), "bar")
}
}
}
}

0 comments on commit 6dc2eee

Please sign in to comment.