#Bus Access Patterns - Decision Guide

Version: 0.51.0 Updated: 2026-07-16 Applies to: ranvier-core Category: Guides


#Overview

Bus is Ranvier's per-execution, type-indexed runtime context. The requested Rust type is checked by the compiler, but presence and authorization are runtime properties. Mandatory application dependencies should use Transition::Resources; an adapter may inject request context such as auth or tenant identity into a fresh Bus. This guide selects the appropriate runtime access method.


#Decision Tree

graph TD
    Q{"Must the resource\nexist in the Bus?"}
    Q -->|"YES: guaranteed\nby prior step"| REQ["require::<T>()\nReturns: &T\nOn missing: panic"]
    Q -->|"MAYBE: can\nproceed without"| READ["read::<T>()\nReturns: Option<&T>\nOn missing: None"]
    Q -->|"YES: must return\nerror if missing"| GET["get::<T>()\nReturns: Result<&T, BusAccessError>\nDefault for production"]

#Method Comparison

Method Return Type On Missing Use When
require::<T>() &T panic A tested framework invariant guarantees insertion and policy access
read::<T>() Option<&T> None Optional resource -- can proceed without it
get::<T>() Result<&T, BusAccessError> Err(NotFound) Production code that needs an error message
try_require::<T>() Option<&T> None Semantic alias for read()

#Mutable Variants

Method Return Type On Missing
read_mut::<T>() Option<&mut T> None
get_mut::<T>() Result<&mut T, BusAccessError> Err(NotFound)

#Usage Examples

#`require()` - Guaranteed by Framework

#[transition]
async fn use_runtime_clock(
    state: Job,
    _resources: &(),
    bus: &mut Bus,
) -> Outcome<Request, AppError> {
    // The application harness inserts Clock in every execution and tests that
    // invariant. User configuration and request auth should use get() instead.
    let clock = bus.require::<Clock>();
    clock.record_start();
    Outcome::Next(state)
}

#`read()` - Optional Enhancement

#[transition]
async fn filter_by_tenant(
    state: Vec<Item>,
    _resources: &(),
    bus: &mut Bus,
) -> Outcome<Vec<Item>, AppError> {
    // TenantId may or may not be present
    // If absent, return all items (no filtering)
    let filtered = match bus.get_cloned::<TenantId>() {
        Ok(tenant) => state.into_iter()
            .filter(|item| item.tenant_id == tenant.0)
            .collect(),
        Err(_) => state,
    };

    Outcome::Next(filtered)
}

#`get()` - Production Default

#[transition]
async fn load_config(
    state: Request,
    _resources: &(),
    bus: &mut Bus,
) -> Outcome<Request, AppError> {
    // AppConfig should be in the Bus, but we want a clear error if missing
    let config = match bus.get::<AppConfig>() {
        Ok(config) => config,
        Err(error) => {
            return Outcome::Fault(AppError::Internal(format!(
                "Config unavailable: {error}"
            )));
        }
    };

    // Use config...
    Outcome::Next(state)
}

#Inserting Resources

Resources are typically inserted during application setup:

let mut bus = Bus::new();

// Database pool
bus.insert(db_pool);

// Application config
bus.insert(app_config);

// Request context (insert only into this execution Bus)
bus.insert(auth_context);

#Helper Methods

Method Description
insert::<T>(value) Insert or replace a resource by type
provide::<T>(value) Semantic alias for insert
remove::<T>() Remove and return a resource (Option<T>)
has::<T>() Check if a resource exists (bool)

#Choosing the Right Pattern

#Simple Rule

  1. Framework guarantees it? Use require()
  2. Optional feature? Use read()
  3. Everything else? Use get()

#Anti-Patterns

Pattern Problem Fix
require() for user-provided config Panics in production if config missing Use get() with proper error message
read() silently ignoring missing deps Bugs go unnoticed Use get() and handle the error
Multiple require() calls for same type Redundant lookups Read once, store in a local variable

#Bus Access Policy (Advanced)

bus_allow / bus_deny attributes declare which types a Transition may access. The macro validates the declared type list at compile time. Actual Bus lookup is runtime-enforced because the macro does not inspect every expression in the transition body.

#[transition(bus_allow = [AuthContext, AppConfig])]
async fn handle(
    input: Request,
    bus: &mut Bus,
) -> Outcome<Response, AppError> {
    // get() returns Unauthorized when the active runtime policy rejects T.
    // ...
}

get() and get_mut() reject policy violations with BusAccessError::Unauthorized. Convenience methods such as read() log the violation and collapse it to None; require() panics. Use get() in production paths that must preserve missing-versus-unauthorized diagnostics.

Bus::new() remains unrestricted for 0.51.x compatibility. M419 may require an explicit policy when a production profile is selected, without changing the development constructor silently.


  • examples/bus-capability-demo -- Bus access policy demonstration
  • examples/outcome-variants-demo -- Outcome control flow patterns
  • ranvier-core/src/bus.rs -- Bus implementation
  • Outcome and Bus Typed-Boundary Decision