Skip to main content

Crate modrun

Crate modrun 

Source
Expand description

modrun — a modular application composer for Rust.

Build large Tokio services as domain-oriented Modules with constructor injection, explicit module boundaries, and coordinated lifecycle — wired once at the composition root.

Register constructors, pull the graph with invokers, and manage start/stop via Lifecycle.

§Quick start

use modrun::{Hook, Lifecycle, Modrun};

#[derive(Clone)]
struct Config { port: u16 }

#[derive(Clone)]
struct Server { cfg: Config }

impl Hook for Server {
    async fn on_start(&mut self) -> modrun::Result<()> {
        println!("listening on {}", self.cfg.port);
        Ok(())
    }

    async fn on_stop(&mut self) -> modrun::Result<()> {
        Ok(())
    }
}

fn new_config() -> Config { Config { port: 8080 } }
fn new_server(cfg: Config) -> Server { Server { cfg } }

fn boot(lc: Lifecycle, server: Server) -> modrun::Result<()> {
    lc.append(server)
}

#[tokio::main]
async fn main() -> modrun::Result<()> {
    Modrun::builder()
        .provide(new_config)
        .provide(new_server)
        .invoke(boot)
        .run()
        .await
}

Dependencies must be Clone to inject by value. Singletons are cached by type and cloned on inject. Prefer Arc<T> when a type is large, shared across many constructors, or injected more than once — Arc<T> is also stored, so you can inject Arc<T> even when T itself is not Clone. Put shared mutable state behind Arc inside your type, e.g. #[derive(Clone)] struct Db(Arc<DbInner>).

Because the cache key is the type, a type can only be provided once. To wire two of the same thing — a primary and a replica pool, say — give each its own newtype wrapper.

§Async constructors

Anything that needs to await before it exists (connection pools, clients that handshake) can be provided directly:

use modrun::Modrun;

#[derive(Clone)]
struct Pool;

async fn connect() -> Result<Pool, std::io::Error> {
    Ok(Pool)
}

#[tokio::main]
async fn main() -> modrun::Result<()> {
    Modrun::builder()
        .provide_result_async(connect)
        .invoke(|_pool: Pool| {})
        .start()
        .await?
        .stop()
        .await
}

§Domain modules

A Module groups related wiring under a name and gives it a private scope. provide_private keeps a type invisible outside the module, so two modules can each hold their own Repo without colliding.

use modrun::{Modrun, Module};

#[derive(Clone)]
struct UserRepo;
#[derive(Clone)]
struct UserService;

fn new_user_repo() -> UserRepo { UserRepo }
fn new_user_service(_repo: UserRepo) -> UserService { UserService }
fn boot_user(_svc: UserService) {}

fn user_domain() -> Module {
    Module::builder("user")
        .provide_private(new_user_repo)
        .provide(new_user_service)
        .invoke(boot_user)
}

#[tokio::main]
async fn main() -> modrun::Result<()> {
    Modrun::builder()
        .module(user_domain())
        .start()
        .await?
        .stop()
        .await
}

§Groups

Several modules can each contribute one T; consumers inject Group<T> (not Vec<T>). Register members with ModrunBuilder::provide_group. An empty group needs ModrunBuilder::init_group (or ModrunBuilder::require_group, which also registers the group and then rejects emptiness). Members do not occupy the singleton slot for T. Group members and injected groups require T: Clone; prefer Arc<Group<T>> or Group<Arc<T>> when many consumers or heavy values would otherwise clone.

§Dependency graph

Export the wiring graph with ModrunBuilder::render_dot or .dot_graph(path) (DOT is written after validation, before constructors run). Built-in Lifecycle and Shutdowner are omitted from the exported graph.

§Startup banner

ModrunBuilder::run and ModrunBuilder::start print a modrun ASCII banner to stderr before wiring begins (Spring Boot style), and only when stderr is a TTY. Override with .banner(text) (always printed to stderr) or disable with .no_banner(). Tests that run in a terminal should call .no_banner() so captured stderr stays quiet.

§Logging

Framework events (provide / supply / invoke / construct / lifecycle) are emitted through tracing with target modrun, using uber/fx-style console lines such as [modrun] PROVIDE .... Events also carry structured fields (constructor, module, elapsed, …) for JSON subscribers. Call modrun::logging::init() from examples and local binaries (requires the logging feature); it is a no-op if a subscriber is already installed, writes to stderr, and enables ANSI only when stderr is a terminal. Production processes should install their own subscriber and filter with RUST_LOG=modrun=info. Without a subscriber the events are cheap no-ops. Debug builds also print to stderr if a RunningApp is dropped without RunningApp::stop.

modrun::logging::init();

§Stability

The public model is type-keyed singletons, Group<T> value groups, lazy provide, invoke as the graph root, and start/stop via Lifecycle. Domain wiring uses Module::builder (symmetric with Modrun::builder). There are no string qualifiers and no service locator after build. Hook methods will only grow with defaults. Constructors and invokers accept at most eight parameters; pack extra deps in a struct. MSRV is 1.85 (edition 2024). Application-facing API is stable since 1.0.0; see CONTRIBUTING.md. Startup overhead is mostly your constructors; the container is dropped after build. Prefer Arc<T> and Arc<Group<T>> for heavy deps — see README「Performance」. Examples may need a newer compiler via their own dependencies (the MSRV job does not compile --all-targets).

Application code should use ModrunBuilder::provide / ModrunBuilder::invoke. Wrapper libraries use the hidden [__wiring] module and ModrunBuilder::provide_dyn / invoke_dyn (also #[doc(hidden)]).

§Crate features

  • signal (enabled by default) — OS signal listeners in ModrunBuilder::run on Unix (Ctrl-C / SIGTERM) and Windows (Ctrl-C / Ctrl-Break / Ctrl-Close / Ctrl-Shutdown). On other targets the feature is a no-op for OS signals; use Shutdowner. Disable with default-features = false when you only call ModrunBuilder::start or wait on Shutdowner yourself.
  • logging — modrun::logging::init / try_init for fx-style console output. Skip it in production and install your own subscriber.

Modules§

logging
Fx-style console logging for modrun framework events.

Structs§

Group
Many values of the same type, aggregated from provide_group registrations across modules.
HookFn
Ad-hoc start/stop callbacks. Prefer implementing Hook when the two phases share state.
Lifecycle
Application lifecycle registry.
Modrun
Entry point for configuring an application: Modrun::builder.
ModrunBuilder
Fluent builder for a modrun application.
Module
A named domain module.
MultipleStopError
Several OnStop hook failures aggregated into one error.
PreparedTask
Await prepare during OnStart, then spawn run in the background.
RunningApp
A started application that can be stopped explicitly.
Shutdowner
Triggers graceful shutdown from inside the app.
Stopped
Completes when the matching Task / PreparedTask begins OnStop (or is dropped).
Task
Long-running work spawned in OnStart and joined in OnStop.

Enums§

Error
Errors produced while wiring, starting, or stopping an application.

Constants§

DEFAULT_BANNER
Default ASCII banner printed at startup unless crate::ModrunBuilder::no_banner is called.
DEFAULT_TIMEOUT
Default build/start/stop timeout (15s).

Traits§

Hook
Start/stop callbacks for a component.

Functions§

hook
Build an ad-hoc Hook from closures.
task
Spawn run as a background task from OnStart, signal it on OnStop, then join.
task_with
Like task, but prepare is awaited during OnStart.

Type Aliases§

BoxError
Owned user error retained as std::error::Error::source.
Result
Result alias used throughout the crate and by hooks / fallible constructors.