Skip to content

Router: add_service / merge for top-down multi-service registration #88

@iainmcgin

Description

@iainmcgin

Registering multiple services with a Router currently reads inside-out:

let router = Arc::new(svc_b).register(Arc::new(svc_a).register(Router::new()));

or, more readably but with intermediate bindings:

let mut router = Router::new();
router = Arc::new(svc_a).register(router);
router = Arc::new(svc_b).register(router);

The register extension trait on Arc<Self> is a deliberate design — it avoids the visitor-pattern wrapper that tonic generates (GreeterServer::new(impl) / GreeterServer::from_arc(impl)) — but the call shape inverts the data flow compared to every other framework in the ecosystem:

// tonic
Server::builder()
    .add_service(GreeterServer::new(greeter))
    .add_service(EchoServer::new(echo))
    .serve(addr).await?;

// connect-go (each service mounts independently on the mux)
mux.Handle(elizav1connect.NewElizaServiceHandler(eliza))
mux.Handle(greetv1connect.NewGreetServiceHandler(greeter))

A user arriving from tonic types Router::new().add_service(...) first, gets a method-not-found, and has to read the codegen output to discover the register extension trait.

Proposed direction

Add a forwarding builder method on Router so registration reads top-down:

impl Router {
    /// Register a service. Equivalent to `Arc::new(svc).register(self)`.
    pub fn add_service<S, E>(self, svc: Arc<S>) -> Self
    where
        S: ?Sized,
        Arc<S>: ServiceRegister,   // a small trait the codegen `*Ext` traits could implement
    {
        svc.register(self)
    }
}

so the call site is:

let router = Router::new()
    .add_service(Arc::new(eliza))
    .add_service(Arc::new(greeter));

This is purely additive — register(self: Arc<Self>, router) stays as the underlying mechanism — but it gives the discoverable spelling. While here, also consider promoting the merge_routers free function to an inherent Router::merge(self, other) -> Self (and an in-place Router::extend(&mut self, other) sibling) for the same discoverability reason.

Scope

Small: one pub trait for the bound, one inherent method, doc update, example update.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions