-
-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add route controller for better composability. (#565)
- Loading branch information
1 parent
913e6a0
commit 6dc2eee
Showing
3 changed files
with
206 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} | ||
} | ||
} |