From 6ae9f00b4d3716c2c103617ea2469f75d379845d Mon Sep 17 00:00:00 2001 From: Yong-yuan-X <2463436064@qq.com> Date: Thu, 11 Jun 2026 23:51:37 +0800 Subject: [PATCH] router: hide low-level route helpers from rustdoc --- connectrpc/src/handler.rs | 24 +++----- connectrpc/src/interceptor.rs | 4 +- connectrpc/src/response.rs | 6 +- connectrpc/src/router.rs | 102 ++++++++++++---------------------- connectrpc/src/service.rs | 29 +++++----- docs/guide.md | 4 +- 6 files changed, 63 insertions(+), 106 deletions(-) diff --git a/connectrpc/src/handler.rs b/connectrpc/src/handler.rs index d3e69aa..9c04960 100644 --- a/connectrpc/src/handler.rs +++ b/connectrpc/src/handler.rs @@ -344,11 +344,11 @@ where /// PreEncoded::::from_bytes_unchecked(bytes) /// ``` /// -/// The codegen-emitted route registrations (`route_view_server_stream::<_, -/// _, Out>`) always pin `Res` because the trait method's stream item is the -/// *opaque* `impl Encodable`, which can't be unified against the -/// `Encodable` impls. Hand-written `Router` registrations don't hit -/// this unless they leave the message type open. +/// Generated server-streaming registrations always pin `Res` because the +/// trait method's stream item is the *opaque* `impl Encodable`, which +/// can't be unified against the `Encodable` impls. Hand-written +/// `Router` registrations don't hit this unless they leave the message type +/// open. pub fn streaming_handler_fn(f: F) -> FnStreamingHandler where F: Fn(RequestContext, Req) -> Fut + Send + Sync + 'static, @@ -772,18 +772,8 @@ where /// /// The closure receives the negotiated [`CodecFormat`] and returns the /// response **already encoded**, so a body that borrows from `&svc` is -/// encoded before the borrow ends. For a hand-written router this looks -/// like: -/// -/// ```rust,ignore -/// router.route_view(SERVICE, "Foo", view_handler_fn({ -/// let svc = Arc::clone(&svc); -/// move |ctx, req, format| { -/// let svc = Arc::clone(&svc); -/// async move { svc.foo(ctx, req).await?.encode::(format) } -/// } -/// })) -/// ``` +/// encoded before the borrow ends. Generated service registration uses this +/// adapter for unary handlers that operate on borrowed request views. pub fn view_handler_fn(f: F) -> FnViewHandler where F: Fn(RequestContext, OwnedView, CodecFormat) -> Fut + Send + Sync + 'static, diff --git a/connectrpc/src/interceptor.rs b/connectrpc/src/interceptor.rs index e24c2e0..1b91d82 100644 --- a/connectrpc/src/interceptor.rs +++ b/connectrpc/src/interceptor.rs @@ -302,8 +302,8 @@ pub(crate) trait UnaryTerminal: Send + Sync { /// /// `ctx.spec` is `Some(..)` for generated `FooServiceServer` /// dispatchers and for [`Router`](crate::Router) routes registered -/// through the generated `register()`; it is `None` only for manual -/// `route_*` registrations without a +/// through the generated `register()`; it is `None` only for low-level +/// manual registrations without a /// [`Router::with_spec`](crate::Router::with_spec) call. /// /// `#[non_exhaustive]` so future fields can be added without a diff --git a/connectrpc/src/response.rs b/connectrpc/src/response.rs index 508a04d..f6126c3 100644 --- a/connectrpc/src/response.rs +++ b/connectrpc/src/response.rs @@ -193,9 +193,9 @@ impl RequestContext { /// by the dynamic [`Router`](crate::Router) when registered through /// the generated `register()` (which chains /// [`Router::with_spec`](crate::Router::with_spec) per route). - /// `None` only for routes registered through the manual `route_*` - /// builders without a `with_spec` call. See [`path`](Self::path) for - /// the always-present procedure path. + /// `None` only for low-level manual registrations that do not attach a + /// [`Spec`](crate::Spec). See [`path`](Self::path) for the always-present + /// procedure path. pub fn spec(&self) -> Option { self.spec } diff --git a/connectrpc/src/router.rs b/connectrpc/src/router.rs index 06a3c12..6b1eb26 100644 --- a/connectrpc/src/router.rs +++ b/connectrpc/src/router.rs @@ -102,13 +102,20 @@ impl From for RegisteredMethod { /// The router maps service/method paths to their handlers and manages /// request dispatching. /// +/// Most users should register generated services with the generated +/// `Ext::register` extension trait instead of manually calling the +/// low-level route registration helpers: +/// +/// ```rust,ignore +/// let router = Arc::new(MyService).register(Router::new()); +/// ``` +/// /// `Router` is the *dynamic* dispatch path: method paths are owned `String` /// keys. It can still carry a [`Spec`] when one is attached with -/// [`with_spec`](Self::with_spec) — the generated -/// `FooServiceExt::register` does this for every method, so a `Router` -/// built through codegen surfaces [`RequestContext::spec`] just like the -/// monomorphic `FooServiceServer` dispatcher does. Hand-written -/// `route_*` registrations without a `Spec` surface `None`. +/// [`with_spec`](Self::with_spec). Generated registration does this for every +/// method, so a `Router` built through codegen surfaces +/// [`RequestContext::spec`] just like the monomorphic +/// `FooServiceServer` dispatcher does. /// /// [`RequestContext::spec`]: crate::RequestContext::spec #[derive(Default)] @@ -125,20 +132,7 @@ impl Router { /// Register a unary RPC handler. /// - /// # Arguments - /// - /// * `service_name` - The fully qualified service name (e.g., "example.v1.GreetService") - /// * `method_name` - The method name (e.g., "Greet") - /// * `handler` - The handler function or closure - /// - /// # Example - /// - /// ```rust,ignore - /// let router = Router::new() - /// .route("example.v1.GreetService", "Greet", |ctx, req: GreetRequest| async move { - /// Ok(GreetResponse { message: format!("Hello, {}!", req.name) }) - /// }); - /// ``` + #[doc(hidden)] pub fn route(self, service_name: &str, method_name: &str, handler: H) -> Self where H: Handler, @@ -154,20 +148,7 @@ impl Router { /// This is typically used for methods marked with `idempotency_level = NO_SIDE_EFFECTS` /// in the protobuf definition. /// - /// # Arguments - /// - /// * `service_name` - The fully qualified service name (e.g., "example.v1.GreetService") - /// * `method_name` - The method name (e.g., "Greet") - /// * `handler` - The handler function or closure - /// - /// # Example - /// - /// ```rust,ignore - /// let router = Router::new() - /// .route_idempotent("example.v1.QueryService", "GetUser", |ctx, req: GetUserRequest| async move { - /// Ok(GetUserResponse { ... }) - /// }); - /// ``` + #[doc(hidden)] pub fn route_idempotent( self, service_name: &str, @@ -212,18 +193,7 @@ impl Router { /// /// A server streaming handler takes a single request and returns a stream of responses. /// - /// # Arguments - /// - /// * `service_name` - The fully qualified service name (e.g., "example.v1.GreetService") - /// * `method_name` - The method name (e.g., "GreetMany") - /// * `handler` - The streaming handler function or closure - /// - /// # Example - /// - /// ```rust,ignore - /// let router = Router::new() - /// .route_server_stream("example.v1.GreetService", "GreetMany", streaming_handler_fn(my_handler)); - /// ``` + #[doc(hidden)] pub fn route_server_stream( mut self, service_name: &str, @@ -251,6 +221,7 @@ impl Router { /// Register a client streaming RPC handler. /// /// A client streaming handler receives a stream of requests and returns a single response. + #[doc(hidden)] pub fn route_client_stream( mut self, service_name: &str, @@ -277,6 +248,7 @@ impl Router { /// Register a bidi streaming RPC handler. /// /// A bidi streaming handler receives a stream of requests and returns a stream of responses. + #[doc(hidden)] pub fn route_bidi_stream( mut self, service_name: &str, @@ -305,6 +277,7 @@ impl Router { // ==================================================================== /// Register a unary RPC handler that uses zero-copy request views. + #[doc(hidden)] pub fn route_view(self, service_name: &str, method_name: &str, handler: H) -> Self where H: ViewHandler, @@ -315,6 +288,7 @@ impl Router { } /// Register an idempotent unary RPC handler that uses zero-copy request views. + #[doc(hidden)] pub fn route_view_idempotent( self, service_name: &str, @@ -356,6 +330,7 @@ impl Router { } /// Register a server streaming RPC handler that uses zero-copy request views. + #[doc(hidden)] pub fn route_view_server_stream( mut self, service_name: &str, @@ -382,6 +357,7 @@ impl Router { } /// Register a client streaming RPC handler that uses zero-copy request views. + #[doc(hidden)] pub fn route_view_client_stream( mut self, service_name: &str, @@ -406,6 +382,7 @@ impl Router { } /// Register a bidi streaming RPC handler that uses zero-copy request views. + #[doc(hidden)] pub fn route_view_bidi_stream( mut self, service_name: &str, @@ -432,30 +409,23 @@ impl Router { /// Attach a [`Spec`] to the route registered at `spec.procedure`. /// - /// The route must already exist — [`Spec::procedure`] is the lookup - /// key (with the leading slash stripped, matching the `route_*` - /// methods' `format!("{service}/{method}")` keying). Generated - /// `FooServiceExt::register` chains this after each `route_view*` - /// call so the dynamic `Router` carries the same per-method - /// metadata as the monomorphic `FooServiceServer`. Hand-written - /// registrations may call it too when they have a `&'static` - /// procedure path: - /// - /// ```rust,ignore - /// const SAY_SPEC: Spec = Spec::server("/eliza.v1.Eliza/Say", StreamType::Unary); - /// let router = Router::new() - /// .route_view_idempotent("eliza.v1.Eliza", "Say", handler) - /// .with_spec(SAY_SPEC); - /// ``` + /// The route must already exist. [`Spec::procedure`] is used as the lookup + /// key after its leading slash is stripped. Generated + /// `FooServiceExt::register` calls this after registering each method so + /// the dynamic `Router` carries the same per-method metadata as the + /// monomorphic `FooServiceServer`. Most users do not need to call this + /// directly. /// /// # Panics /// - /// Debug builds panic if no route is registered at `spec.procedure`, - /// or if the route is unary and `spec.idempotency_level` disagrees - /// with the `route` / `route_idempotent` choice. Both indicate a - /// programming error in `register()` (a typo, or calling `with_spec` - /// before the matching `route_*`); release builds skip the check and - /// silently drop the `Spec`. + /// Debug builds panic if no route is registered at `spec.procedure`, or if + /// a unary route's Connect GET eligibility disagrees with + /// `spec.idempotency_level`. Both indicate a programming error in + /// generated registration or a call to `with_spec` before the matching + /// handler was registered. + /// + /// Release builds leave the router unchanged when no matching route + /// exists. The idempotency consistency check is debug-only. #[must_use] pub fn with_spec(mut self, spec: Spec) -> Self { let key = spec.procedure.strip_prefix('/').unwrap_or(spec.procedure); diff --git a/connectrpc/src/service.rs b/connectrpc/src/service.rs index 024b4d5..6c10fee 100644 --- a/connectrpc/src/service.rs +++ b/connectrpc/src/service.rs @@ -7,12 +7,10 @@ //! //! ```rust,ignore //! use connectrpc::{Router, ConnectRpcService}; -//! use hyper::service::service_fn; +//! use std::sync::Arc; //! -//! let router = Router::new() -//! .route("example.v1.GreetService", "Greet", greet_handler); -//! -//! let service = router.into_service(); +//! let router = Arc::new(MyGreetService).register(Router::new()); +//! let service = ConnectRpcService::new(router); //! // Use with hyper or any tower-compatible framework //! ``` //! @@ -21,9 +19,9 @@ //! ```rust,ignore //! use axum::{Router, routing::get}; //! use connectrpc::Router as ConnectRouter; +//! use std::sync::Arc; //! -//! let connect_router = ConnectRouter::new() -//! .route("example.v1.GreetService", "Greet", greet_handler); +//! let connect_router = Arc::new(MyGreetService).register(ConnectRouter::new()); //! //! let app = Router::new() //! .route("/health", get(health_handler)) @@ -1115,9 +1113,7 @@ fn create_grpc_web_envelope_stream( /// # Example /// /// ```rust,ignore -/// let router = Router::new() -/// .route("example.v1.GreetService", "Greet", greet_handler); -/// +/// let router = Arc::new(MyGreetService).register(Router::new()); /// let service = ConnectRpcService::new(router); /// ``` /// @@ -1165,8 +1161,7 @@ impl ConnectRpcService { /// Create a new ConnectRPC service from a dispatcher. /// /// The dispatcher can be either: - /// - a [`Router`] built with `.route()` / `.route_view()` calls (or via - /// generated `FooServiceExt::register`), or + /// - a [`Router`] built via generated `FooServiceExt::register`, or /// - a code-generated `FooServiceServer` struct for monomorphic /// dispatch with no HashMap lookup or trait-object indirection. pub fn new(dispatcher: D) -> Self { @@ -3108,13 +3103,14 @@ pub mod axum_integration { /// ```rust,ignore /// use axum::{Router, routing::get}; /// use connectrpc::Router as ConnectRouter; + /// use std::sync::Arc; /// /// async fn health() -> &'static str { /// "OK" /// } /// - /// let connect_router = ConnectRouter::new() - /// .route("example.v1.GreetService", "Greet", greet_handler); + /// let connect_router = + /// Arc::new(MyGreetService).register(ConnectRouter::new()); /// /// let app = Router::new() /// .route("/health", get(health)) @@ -3134,9 +3130,10 @@ pub mod axum_integration { /// ```rust,ignore /// use axum::{Router, routing::get}; /// use connectrpc::Router as ConnectRouter; + /// use std::sync::Arc; /// - /// let connect_router = ConnectRouter::new() - /// .route("example.v1.GreetService", "Greet", greet_handler); + /// let connect_router = + /// Arc::new(MyGreetService).register(ConnectRouter::new()); /// /// // Use as fallback for unmatched routes /// let app = Router::new() diff --git a/docs/guide.md b/docs/guide.md index 65ca6bf..0e95d40 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -314,7 +314,7 @@ methods (new request-scoped metadata can then be added in minor releases): | `ctx.time_remaining()` | Saturating `Option` until the deadline (`None` when no deadline is set) — budget downstream calls with this | | `ctx.extensions()` | `http::Extensions` carried from the underlying `http::Request` | | `ctx.path()` | Requested procedure path (`/package.Service/Method`) from the request URI | -| `ctx.spec()` | Static metadata for the dispatched RPC method ([`Spec`](#static-method-metadata-spec)); `None` only for `route_*` registrations without `with_spec` | +| `ctx.spec()` | Static metadata for the dispatched RPC method ([`Spec`](#static-method-metadata-spec)); `None` only for low-level manual registrations that do not attach one | | `ctx.protocol()` | The negotiated wire protocol for this request (`Connect` / `Grpc` / `GrpcWeb`) | | `ctx.peer_addr()` | Remote socket address (requires the `server` feature; `None` when the transport didn't insert it) | | `ctx.peer_certs()` | TLS client cert chain (requires the `server-tls` feature; `None` for plaintext or no client cert) | @@ -740,7 +740,7 @@ assert_eq!(GREET_SERVICE_GREET_SPEC.origin, SpecOrigin::Server); > (used by `FooServiceExt::register(Router)`) does too — the generated > `register()` chains `.with_spec(SPEC_CONST)` after each route. The > only handlers that see `ctx.spec() == None` are those registered -> through the manual `route_*` builders without a `with_spec` call. +> through low-level manual registration without attaching a `Spec`. > `ctx.path()` is populated unconditionally regardless of dispatch path > — use it when you only need the procedure name and want to be robust > to a missing `Spec`.