Skip to content

Routing

As a service grows, handlers move out of main.rs into their own modules. A Router collects a module's handlers into one mountable group; include_router mounts the whole group on a broker scope.

Building a router

A Router mirrors the broker scope: alongside include / include_on and include_publishing / include_publishing_on (which take the reply wiring positionally - a TypedPublisher stack over a publish policy, since a consuming builder cannot chain a postfix .publisher(..)) it has with_codec (switches the chain's decode codec, see Codecs) and the manual handle / subscribe registrations. Every call consumes the router and returns a new one, so registrations chain:

routes.rs
use ruststream::runtime::Router;

fn orders() -> Router<MemoryBroker, impl RouterDef<MemoryBroker>> {
    Router::new().include(accept)
}

fn shipping() -> Router<MemoryBroker, impl RouterDef<MemoryBroker>> {
    Router::new().include(dispatch)
}
main.rs
RustStream::new(info).with_broker(broker, |b| {
    b.include_router(routes::orders());
});

Handlers that publish a reply register on the router the same way as on the scope, with a TypedPublisher stack over a publish policy - a pure declaration, so the router needs no broker:

routes.rs
use ruststream::memory::{MemoryBroker, MemoryPublish};
use ruststream::runtime::{Router, RouterDef, TypedPublisher};

use crate::orders;

// The reply wiring is a publish policy: pure declaration, so the router needs no broker at all.
pub(crate) fn orders() -> impl RouterDef<MemoryBroker> {
    let replies = TypedPublisher::new(MemoryPublish);
    Router::new()
        .include_publishing(orders::confirm, replies)
        .include(orders::handle)
}

Router middleware

A router can carry its own layer stack: Router::layer wraps every handler in that router when it is mounted. The application's global stack (added with RustStream::layer) wraps around it at include_router - scopes nest, app outermost:

main.rs
fn routes() -> impl RouterDef<MemoryBroker> {
    Router::new().include(confirm).include(reject)
}

Because a router hides its handlers' concrete types, a layer reaching them must be a BlanketLayer. Both scopes, the BlanketLayer requirement, and writing your own layer are covered in Middleware.

Composing and mounting

Build routers per module, then combine them however suits the service:

// Mount several routers on one broker - include_router can be called more than once.
RustStream::new(info).with_broker(broker, |b| {
    b.include_router(routes::orders());
    b.include_router(routes::shipping());
});

Or merge groups into one router before mounting (the whole program is examples/routing.rs):

examples/routing.rs
// Merge groups into one router, then mount the result.
let all = orders().merge(shipping());
b.include_router(all);

merge appends another router's registrations in order. Each router keeps its own codec and layer stack; when the result is mounted, the outer router's layers (and the app's global stack) wrap around the merged router's own.

Next

  • The handler contract and the #[subscriber] macro: Subscribers.
  • How the decode codec is resolved for include: Codecs.