hummingbird-project / hummingbird

Lightweight, flexible HTTP server framework written in Swift
Apache License 2.0
1.16k stars 53 forks source link

Result builder `Body` style syntax. #565

Closed connor-ricks closed 6 days ago

connor-ricks commented 3 weeks ago

Copying this issue over from the example repo to move it to the more appropriate location and closing the example issue.

Hello,

I was experimenting with Hummingbird today and saw the HummingbirdRouter target was added for result builder style syntax.

While I managed to use the result builder syntax to get some routes created, I was wondering if at this point in time, it was possible to make the content feel more composable.

Most of the examples build route collections using a property called endpoints but then when they add them, they explicitly call .endpoints.

I was hoping to achieve a syntax more like this...

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

struct Auth: MiddlewareProtocol {
    var body: some MiddlewareProtocol {
        SSO()
        Get(...)
        Post(...)
    }
}

structure SSO: MiddlewareProtocol {
    var body: some MiddlewareProtocol {
        ...
    }
}

Just some rough syntax, but hopefully you get the idea.

Is this possible at the moment? Perusing the implementation, it seems maybe not quite, but perhaps it is pretty close.

I achieved a similar syntax with Vapor. You can take a peek here.

connor-ricks commented 3 weeks ago

I managed to get this to compile this morning... Caching is still a potential issue. I'm wondering if this is a worthwhile addition to the library or if I should simply just use this in my own project?

protocol Controller: RouterMiddleware {
    associatedtype Body: RouterMiddleware
    var body: Body { get }
}

extension Controller where Body.Input == Input, Body.Context == Context, Body.Output == Output {
    func handle(_ input: Input, context: Context, next: (Input, Context) async throws -> Output) async throws -> Output {
        try await body.handle(input, context: context, next: next)
    }
}

struct UserController: Controller {
    typealias Context = BasicRouterRequestContext

    var body: some RouterMiddleware<Context> {
        RouteGroup("user") {
            Get { _,_ in
                "User"
            }
        }
    }
}

func foo() {
    let router = RouterBuilder {
        UserController()
    }

    let app = Application(responder: router)
}
adam-fowler commented 3 weeks ago

It is a worthwhile addition. I think we'd want to integrate it into the result builder first though so the result builder is only evaluated at build time and not every time we call handle().

adam-fowler commented 3 weeks ago

I think you'd just have to add implementations of buildExpression, buildBlock etc to MiddlewareFixedTypeBuilder which take a Controller parameter and dereference the controller body parameter in those functions.

Also you'd need to remove the MiddlewareProtocol conformance from Controller along with its handle function.

It'd probably be nice to implement somethings similar for the trie router as well so getting the naming right might be difficult.

connor-ricks commented 3 weeks ago

Thanks for the input. I might try and play around with this a little bit this weekend. Go enjoy your vacation!

connor-ricks commented 2 weeks ago

Created a small PR for this. It doesn't include support for the "trie" router, but it's a start. The fact that the Controller has a body itself that is a builder makes me think that it belongs properly in the HummingbirdRouter instead of the main target, but you'll definitely have more context than me. Feel free to make changes! 😄

connor-ricks commented 2 weeks ago

Actively under development in #577

connor-ricks commented 6 days ago

577 has been merged!