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.
Registering multiple services with a
Routercurrently reads inside-out:or, more readably but with intermediate bindings:
The
registerextension trait onArc<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: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 theregisterextension trait.Proposed direction
Add a forwarding builder method on
Routerso registration reads top-down:so the call site is:
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 themerge_routersfree function to an inherentRouter::merge(self, other) -> Self(and an in-placeRouter::extend(&mut self, other)sibling) for the same discoverability reason.Scope
Small: one
pub traitfor the bound, one inherent method, doc update, example update.