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 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 built from the broker:

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

use crate::orders;

pub(crate) fn orders(broker: &MemoryBroker) -> impl RouterDef<MemoryBroker> + use<> {
    let replies = TypedPublisher::new(broker.publisher());
    Router::new()
        .include_publishing(orders::confirm, replies)
        .include(orders::handle)
}

Router middleware

The application's global middleware (added with RustStream::layer) wraps router handlers too: include_router applies the app stack around each handler when the router is mounted. A layer used this way must be a BlanketLayer - the router hides its handlers' concrete types, so the wrap happens through one generic method; every bundled layer qualifies.

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

A router can also carry its own stack: Router::layer wraps every handler in that router when it is mounted, inside the app's global stack (scopes nest, app outermost):

routes.rs
fn routes() -> impl RouterDef<MemoryBroker> {
    // wraps every handler on this router when it is mounted
    Router::new()
        .layer(LogLayer)
        .include(orders) //    wrapped by LogLayer
        .include(shipments) // wrapped by LogLayer
}

#[ruststream::app]
fn app() -> RustStream {
    RustStream::new(AppInfo::new("router-scope", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
        b.include_router(routes());
        b.include(audit); // directly on the scope: outside the router's stack
    })
}

See Middleware for what a layer is and how to write one, and examples/logging_middleware.rs for the app-scope side in a running service.

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):

// 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.