Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 7 additions & 17 deletions connectrpc/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,11 +344,11 @@ where
/// PreEncoded::<MyResponse>::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<Out>`, which can't be unified against the
/// `Encodable<Res>` 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<Out>`, which
/// can't be unified against the `Encodable<Res>` impls. Hand-written
/// `Router` registrations don't hit this unless they leave the message type
/// open.
pub fn streaming_handler_fn<F, Fut, Req, Res, B>(f: F) -> FnStreamingHandler<F>
where
F: Fn(RequestContext, Req) -> Fut + Send + Sync + 'static,
Expand Down Expand Up @@ -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::<FooResponse>(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, Fut, ReqView>(f: F) -> FnViewHandler<F>
where
F: Fn(RequestContext, OwnedView<ReqView>, CodecFormat) -> Fut + Send + Sync + 'static,
Expand Down
4 changes: 2 additions & 2 deletions connectrpc/src/interceptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,8 @@ pub(crate) trait UnaryTerminal: Send + Sync {
///
/// `ctx.spec` is `Some(..)` for generated `FooServiceServer<T>`
/// 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
Expand Down
6 changes: 3 additions & 3 deletions connectrpc/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<crate::spec::Spec> {
self.spec
}
Expand Down
102 changes: 36 additions & 66 deletions connectrpc/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,20 @@ impl From<Method> for RegisteredMethod {
/// The router maps service/method paths to their handlers and manages
/// request dispatching.
///
/// Most users should register generated services with the generated
/// `<Service>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<T>` 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<T>` dispatcher does.
///
/// [`RequestContext::spec`]: crate::RequestContext::spec
#[derive(Default)]
Expand All @@ -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<H, Req, Res>(self, service_name: &str, method_name: &str, handler: H) -> Self
where
H: Handler<Req, Res>,
Expand All @@ -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<H, Req, Res>(
self,
service_name: &str,
Expand Down Expand Up @@ -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<H, Req, Res>(
mut self,
service_name: &str,
Expand Down Expand Up @@ -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<H, Req, Res>(
mut self,
service_name: &str,
Expand All @@ -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<H, Req, Res>(
mut self,
service_name: &str,
Expand Down Expand Up @@ -305,6 +277,7 @@ impl Router {
// ====================================================================

/// Register a unary RPC handler that uses zero-copy request views.
#[doc(hidden)]
pub fn route_view<H, ReqView>(self, service_name: &str, method_name: &str, handler: H) -> Self
where
H: ViewHandler<ReqView>,
Expand All @@ -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<H, ReqView>(
self,
service_name: &str,
Expand Down Expand Up @@ -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<H, ReqView, Res>(
mut self,
service_name: &str,
Expand All @@ -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<H, ReqView>(
mut self,
service_name: &str,
Expand All @@ -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<H, ReqView, Res>(
mut self,
service_name: &str,
Expand All @@ -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<T>`. 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<T>`. 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);
Expand Down
29 changes: 13 additions & 16 deletions connectrpc/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
//! ```
//!
Expand All @@ -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))
Expand Down Expand Up @@ -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);
/// ```
///
Expand Down Expand Up @@ -1165,8 +1161,7 @@ impl<D: Dispatcher> ConnectRpcService<D> {
/// 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<T>` struct for monomorphic
/// dispatch with no HashMap lookup or trait-object indirection.
pub fn new(dispatcher: D) -> Self {
Expand Down Expand Up @@ -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))
Expand All @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ methods (new request-scoped metadata can then be added in minor releases):
| `ctx.time_remaining()` | Saturating `Option<Duration>` 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) |
Expand Down Expand Up @@ -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`.
Expand Down