hummingbird-project / hummingbird-examples

Examples demonstrating various aspects of the Hummingbird swift server framework
Apache License 2.0
90 stars 19 forks source link

SwiftUI style syntax for `HummingbirdRouter` #118

Closed connor-ricks closed 2 months ago

connor-ricks commented 2 months ago

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 {
        Get(...)
        Post(...)
    }
}

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.

adam-fowler commented 2 months ago

I guess you could extend your controller to conform to MiddlewareProtocol and in the handle function pass it onto a version of endpoints created internally.

protocol Controller<Context> {
    var endpoints: RouterMiddleware<Context> { get }
}

extension Controller: MiddlewareProtocol {
    func handle(request:Input, context: Context, next...) async throw -> Output {
        try await endpoints.handle(request, context, next)
}

I'm not sure how performant that'd be as endpoints would be called every call to handle.

adam-fowler commented 2 months ago

We can probably add something to the result builder so we cache the endpoints variable.

connor-ricks commented 2 months ago

Would the caching part not also be a concern in the existing approach too?

Playing around with this a bit more, I'm stuck here trying to mimic View's body.

public protocol Controller: MiddlewareProtocol where Context: RouterRequestContext {
    associatedtype Body

    @RouteBuilder<Context>
    var body: Body { get }
}

extension Controller where Body: RouterMiddleware<Context>, Body.Input == Input, Body.Output == Output, Body.Context == Context {
    public 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 FooController: Controller {
    typealias Context = BasicRouterRequestContext
    var body: some RouterMiddleware<Context> {
        RouteGroup("foo") {
            Get("bar") { _, _ in
                return "Hello world!"
            }
        }
    }
}

Sadly this has a couple compile errors...

I feel like there's just a small piece I'm missing here to get these conformances to work together. The context generic definitely increases the complexity a bit more than what I had tackled with Vapor's approach.

connor-ricks commented 2 months ago

I also realize that I haven't 100% wrapped my head around the nuance and structure of Hummingbird yet, so my contributions to this conversation could be a little out of left field...

My aim was simply to try and tackle a solution that would be a bit more familiar for developers who have used result builders elsewhere in the Swift ecosystem. The composability is a major plus for organization, and the Body style syntax you see in things like SwiftUI's View and PointFree's Parser/Reducer feels much more ergonomic and natural.

adam-fowler commented 2 months ago

Would the caching part not also be a concern in the existing approach too?

No it wouldn't as the endpoint variable is only referenced when the full result builder is built.

adam-fowler commented 2 months ago

Regarding your code I can't tell as I'm on holiday and don't have a laptop with me.

adam-fowler commented 2 months ago

Although maybe requiring Body to conform to RouterMiddleware will help

connor-ricks commented 2 months ago

You're on holiday?! Go away and enjoy it! Let's talk again when you're back!

connor-ricks commented 2 months ago

I managed to get this to compile this morning... Caching is still a potential issue.

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)
}
connor-ricks commented 2 months ago

Closing this issue to migrate the conversation to the actual Hummingbird repo.

New issue here