#Bus Access Patterns Cookbook

Version: 0.43.0 | Updated: 2026-03-25 | Applies to: ranvier-core 0.36+ | Category: Cookbook


#Overview

Bus is Ranvier's type-safe resource container. Every Transition receives a &mut Bus and uses it to share state across pipeline steps. This cookbook provides practical patterns for common Bus usage scenarios.

For the foundational decision guide, see Bus Access Patterns.


#1. Access Method Decision Matrix

Method Return On Missing When to Use
insert::<T>(value) -- overwrites Write a value for downstream steps
read::<T>() Option<&T> None Optional data -- can proceed without it
read_mut::<T>() Option<&mut T> None Modify an existing value in-place
get::<T>() Result<&T, BusAccessError> Err Production code needing error messages
get_mut::<T>() Result<&mut T, BusAccessError> Err Mutable access with error reporting
require::<T>() &T panic Framework guarantees existence
try_require::<T>() Option<&T> None Semantic alias for read()
has::<T>() bool false Check existence without borrowing
get_cloned::<T>() Result<T, BusAccessError> Err v0.43 Owned clone — replaces read().cloned()
remove::<T>() Option<T> None Take ownership, removing from Bus
provide::<T>(value) -- inserts Alias for insert, semantic emphasis on "providing"

#2. Newtype Wrappers for Collision Safety

Plain types like String or u64 collide when you store multiple values of the same type on the Bus. Always use newtype wrappers to avoid this.

#Problem: Type Collision

// BAD: Both insert String, the second overwrites the first
bus.insert::<String>("user-123".into());     // UserId?
bus.insert::<String>("order-456".into());    // OrderId? Overwrites UserId!

#Solution: Newtype Wrappers

#[derive(Debug, Clone)]
struct UserId(pub String);

#[derive(Debug, Clone)]
struct OrderId(pub String);

#[derive(Debug, Clone)]
struct TenantId(pub String);

// Each type occupies its own slot in the Bus
bus.insert(UserId("user-123".into()));
bus.insert(OrderId("order-456".into()));
bus.insert(TenantId("tenant-789".into()));

// Read each independently
let user = bus.read::<UserId>();    // Some(UserId("user-123"))
let order = bus.read::<OrderId>();  // Some(OrderId("order-456"))

#Convention

Define Bus types near the module that produces or consumes them. Guard-specific Bus types (such as RequestOrigin, ClientIdentity, CorsHeaders) are defined in ranvier-guard.


#3. Database Pool Sharing

Share a connection pool across all Transitions by inserting it into the Bus before the circuit executes.

use sqlx::PgPool;

#[derive(Debug, Clone)]
struct DbPool(pub PgPool);

// Insert pool via bus_injector (runs once per request)
Ranvier::http()
    .bus_injector({
        let pool = PgPool::connect(&database_url).await?;
        move |_parts: &http::request::Parts, bus: &mut Bus| {
            bus.insert(DbPool(pool.clone()));
        }
    })
    .get("/api/users", list_users_circuit)
    .run(())
    .await?;

Inside any Transition, read the pool:

#[async_trait]
impl Transition<(), Vec<User>, > for ListUsers {
    type Error = String;
    type Resources = ();

    async fn run(
        &self,
        _input: (),
        _resources: &Self::Resources,
        bus: &mut Bus,
    ) -> Outcome<Vec<User>, Self::Error> {
        let pool = &bus.require::<DbPool>().0;
        let users = sqlx::query_as::<_, User>("SELECT * FROM users")
            .fetch_all(pool)
            .await
            .map_err(|e| e.to_string())?;
        Outcome::next(users)
    }
}

Alternative: If the pool is the same for all requests, use Resources instead of the Bus. Resources are shared across the entire Axon lifetime, while the Bus is per-request.


#4. Cross-Transition Data Passing

The Bus is the standard way to pass data between Transitions in a pipeline. Earlier steps call insert; later steps call read.

use ranvier_core::prelude::*;
use ranvier_runtime::Axon;

#[derive(Debug, Clone)]
struct ValidationResult {
    is_valid: bool,
    warnings: Vec<String>,
}

#[derive(Debug, Clone)]
struct EnrichedOrder {
    original: CreateOrder,
    tax_amount: f64,
    shipping_cost: f64,
}

let order_pipeline = Axon::typed::<CreateOrder, String>("order-pipeline")
    // Step 1: Validate and write result to Bus
    .then_fn("validate", |order, _res, bus| async move {
        let result = ValidationResult {
            is_valid: order.quantity > 0,
            warnings: vec![],
        };
        bus.insert(result.clone());
        if result.is_valid {
            Outcome::next(order)
        } else {
            Outcome::fault("Validation failed".to_string())
        }
    })
    // Step 2: Enrich and write enriched data to Bus
    .then_fn("enrich", |order, _res, bus| async move {
        let enriched = EnrichedOrder {
            tax_amount: order.quantity as f64 * 0.08,
            shipping_cost: 5.99,
            original: order,
        };
        bus.insert(enriched.clone());
        Outcome::next(enriched)
    })
    // Step 3: Read both previous results
    .then_fn("finalize", |enriched, _res, bus| async move {
        let validation = bus.require::<ValidationResult>();
        let total = enriched.tax_amount + enriched.shipping_cost;
        Outcome::next(format!(
            "Order confirmed: ${:.2} total, {} warnings",
            total,
            validation.warnings.len()
        ))
    });

#5. Guard Bus Types

Guards use specific Bus types for HTTP-level communication. When writing custom Bus injectors or reading Guard output, reference these types directly.

#Reading Guard Output in Your Circuit

use ranvier_guard::prelude::*;

let circuit = Axon::simple::<String>("user-info")
    .then_fn("read-auth", |_input, _res, bus| async move {
        // AuthGuard inserts IamIdentity after successful authentication
        let identity = bus.get_cloned::<ranvier_core::iam::IamIdentity>();
        let subject = identity.map(|id| id.subject)
            .unwrap_or_else(|_| "anonymous".into());

        // RequestIdGuard inserts RequestId (UUID v4)
        let request_id = bus.get_cloned::<RequestId>()
            .map(|r| r.0)
            .unwrap_or_default();

        // CompressionGuard inserts CompressionConfig
        let encoding = bus.read::<CompressionConfig>()
            .map(|c| c.encoding.as_str())
            .unwrap_or("identity");

        Outcome::next(format!(
            "user={}, request={}, encoding={}",
            subject, request_id, encoding
        ))
    });

#6. Bus Access Policy

Restrict which types a Transition can read from or write to the Bus. This is useful for enforcing architectural boundaries.

use ranvier_core::bus::{Bus, BusAccessPolicy, BusTypeRef};

// Allow only specific types
let policy = BusAccessPolicy::allow_only(vec![
    BusTypeRef::of::<UserId>(),
    BusTypeRef::of::<DbPool>(),
]);
bus.set_access_policy(policy);

// Or deny specific sensitive types
let policy = BusAccessPolicy::deny_only(vec![
    BusTypeRef::of::<AuthorizationHeader>(),
]);
bus.set_access_policy(policy);

// Remove policy when done
bus.clear_access_policy();

When an access policy is active, attempts to read disallowed types return None and emit a tracing::warn! log entry.


#7. Testing with Bus

Use Bus::new() directly in unit tests to verify Transition behavior:

#[tokio::test]
async fn test_order_enrichment() {
    let mut bus = Bus::new();
    bus.insert(DbPool(test_pool().await));

    let transition = EnrichOrder;
    let input = CreateOrder {
        product_id: "prod-1".into(),
        quantity: 2,
        shipping_address: "123 Main St".into(),
    };

    let result = transition.run(input, &(), &mut bus).await;
    assert!(matches!(result, Outcome::Next(_)));

    // Verify Bus side effects
    let enriched = bus.get_cloned::<EnrichedOrder>().expect("should be in Bus");
    assert!(enriched.tax_amount > 0.0);
}

#9. `Bus::get_cloned()` — Owned Clone (v0.43)

get_cloned() returns an owned clone in a single call, replacing the common bus.read::<T>().cloned() pattern:

// BEFORE (v0.42):
let pool = bus.read::<PgPool>().cloned()
    .ok_or_else(|| "PgPool missing".to_string())?;

// AFTER (v0.43):
let pool = bus.get_cloned::<PgPool>().expect("PgPool");

// With try_outcome! for Outcome context:
use ranvier_core::try_outcome;
let pool = try_outcome!(bus.get_cloned::<PgPool>(), "PgPool not in Bus");

When to use: Any time you need an owned T from Bus and the type implements Clone. Replaces the 3-step read() → cloned() → unwrap/expect chain with a single method.


#10. `BusHttpExt` Trait — HTTP Parameter Extraction (v0.43)

The BusHttpExt trait (from ranvier_http) adds convenience methods for extracting path and query parameters from Bus in HTTP transitions:

use ranvier_http::BusHttpExt;  // or via prelude

// Path parameter (auto-parsed): /users/:id
let id: u64 = bus.path_param("id")?;  // Result<T: FromStr, String>

// Query parameter (optional): /items?page=2
let page: Option<i64> = bus.query_param("page");

// Query with default: /items?per_page=10
let per_page: i64 = bus.query_param_or("per_page", 10);

Before (manual PathParams extraction — 7 lines):

let id: u64 = match bus.read::<PathParams>().and_then(|p| p.get("id")) {
    Some(raw) => match raw.parse() {
        Ok(id) => id,
        Err(_) => return Outcome::Fault("Invalid ID".into()),
    },
    None => return Outcome::Fault("Missing ID".into()),
};

After (BusHttpExt — 4 lines):

let id: u64 = match bus.path_param("id") {
    Ok(id) => id,
    Err(e) => return Outcome::Fault(e),
};

#See Also

  • Bus Access Patterns -- Decision Guide -- foundational reference
  • Guard Patterns Cookbook -- Guard Bus wiring
  • Saga Compensation Cookbook -- Bus in saga contexts
  • Outcome Patterns Cookbook -- try_outcome!, combinators
  • JSON Outcomes Cookbook -- typed JSON at route boundary