The learning tree.
What can I do now?
Start at the top with Hello Clamp and go down one capability at a time. Click any node for its tutorial. Solid nodes are runnable examples today; dashed ones are planned.
Every step on the tree
- Hello ClampRun the smallest Clamp program, and generate the same program as a new project with clamp init.
- A typed capabilityInject a Clock capability into a Greeter and test it with a fixed clock.
- Modules resolved by KernelDeclare a Greeter that requires a Clock and a module that provides one, and let Kernel resolve the match.
- CLI commandsBuild a command tree from contributions declared by separate modules, and run its commands.
- Configuration and lifecycleSee how a Clamp process starts and stops resources in dependency order, and how rustclamp::config reads settings from .env and the environment.
- HTTP API with PostgreSQLCall one Users operation from a console command and from an HTTP server, with the same domain code.
- In-process eventsTurn an in-process domain event into a versioned message envelope and print it as JSON.
- A websiteCreate a web project with clamp init --web and run it, or serve a folder of static pages through Clamp's HTTP route target.
- A worker over JetStreamSend an email request from an API through a message queue to a Worker handler, in memory or through NATS JetStream.
- Orders that survive a crashCreate an order and its outbox message in one PostgreSQL transaction, publish it to NATS, and have a Worker apply it once.
- Scheduled jobsDeclare scheduled jobs, drive them with a controlled clock, and watch catch-up, skipped misfires and drain on shutdown.
- Split into processesDescribe one application with two processes, CLI and Worker, and see which modules each one includes.
- A device loopRun a fixed-step simulation loop with a fake device and check it replays identically.
- no_std CoreUse a Core module identity from a #![no_std] library with alloc, without Kernel or Runtime.
- Mount an existing Axum app (planned)Hand Clamp a whole axum::Router instead of re-declaring routes: the “start where you are” path for Axum services.
- SQLite and Redis (planned)More storage on the same boundaries as PostgreSQL.
- Metrics and tracing (planned)Correlated tracing exists across API and Worker; exported metrics and a full observability step come next.
- Several services (planned)Compose multiple services that share contracts, each deployed on its own.
Hello Clamp
Run the smallest Clamp program, and generate the same program as a new project with clamp init.
Requires nothing (Rust 1.96.1; clamp init fetches the rustclamp crate from GitHub)
Concepts
- Clamp::run
- The facade's synchronous entrypoint. It calls the closure once on the current thread and returns its result unchanged; errors and panics keep ordinary Rust behavior.
- Facade
- The rustclamp crate is the entry point an app depends on. By default it has no dependencies; db, web and config are optional features.
- clamp init
- The developer command that writes a new project. The default template is blank (Hello World); --app, --web, --vue, --react, --tui and --package choose others.
Walkthrough
- Run the example
From the rustclamp repository, run the Pico example. It prints one line.
cargo run --offline --example 00-pico # Hello Clamp - Read the code
Clamp::run calls the closure once on the current thread and returns its result unchanged. This path needs no Core, Kernel or Runtime.
use rustclamp::prelude::*; fn main() { Clamp::run(|| { println!("Hello Clamp"); }); } - Generate a project
clamp init creates a blank Hello World project by default (--blank). The name must start with a letter and use only letters, digits, - or _.
clamp init hello-clamp cd hello-clamp cargo run - Check the generated files
The generated src/main.rs prints "Hello from Clamp!". Cargo.toml depends on the rustclamp facade through its public Git repository and contains an empty [workspace] table so the project stays standalone.
use rustclamp::prelude::*; fn main() { Clamp::run(|| println!("Hello from Clamp!")); }
Clamp::run returns the closure's result unchanged, so change the closure to produce a value and print it outside: let n = Clamp::run(|| 2 + 2); println!("{n}"); should print 4.
let n = Clamp::run(|| 2 + 2);
println!("{n}");Verify
tests/pico.rs checks that the closure runs once on the calling thread, accepts owned non-Send captures, borrows local state, returns errors untranslated, and propagates panics while dropping captures once.
cargo test --offline --test picoexamples/00-pico
Ask ClampBot about this
A typed capability
Inject a Clock capability into a Greeter and test it with a fixed clock.
Requires nothing
Concepts
- Capability
- A named thing a module can provide or require. Core defines ClockCapability for the Clock trait.
- Clock
- A Core trait with one method, now(), returning SystemTime. Code that takes a Clock can be tested with a fixed time.
- Injection
- The Greeter receives its Clock as a constructor argument. There is no global lookup.
Walkthrough
- Run it
The example depends only on rustclamp-core, not the facade. It prints the current Unix second.
cargo run --offline --locked --manifest-path examples/a1-capability/Cargo.toml --example a1-capability # Hello, world! (unix second <now>) - Read the Greeter
Greeter takes any type that implements Core's Clock trait. It never calls the system clock itself.
pub struct Greeter<C> { clock: C, } impl<C: Clock> Greeter<C> { pub fn new(clock: C) -> Self { Self { clock } } pub fn greet(&self, name: &str) -> String { let seconds = self .clock .now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(); format!("Hello, {name}! (unix second {seconds})") } } - Supply the real clock
The binary implements Clock with SystemTime::now and hands it to the Greeter.
struct SystemClock; impl Clock for SystemClock { fn now(&self) -> SystemTime { SystemTime::now() } } fn main() { println!("{}", Greeter::new(SystemClock).greet("world")); } - Test with a fixed clock
The test passes a clock fixed at 42 seconds and gets an exact result.
cargo test --offline --locked --manifest-path examples/a1-capability/Cargo.toml # asserts: Hello, Ada! (unix second 42)
Make SystemClock return a fixed time instead of SystemTime::now(). The output becomes Hello, world! (unix second 42), the same as the test.
fn now(&self) -> SystemTime {
std::time::UNIX_EPOCH + std::time::Duration::from_secs(42)
}Verify
tests/greeter.rs builds a Greeter with a clock fixed at 42 seconds and asserts the exact string, and the package does not depend on the facade (tools/check.py asserts this).
cargo test --offline --locked --manifest-path examples/a1-capability/Cargo.tomlexamples/a1-capability
Ask ClampBot about this
Modules resolved by Kernel
Declare a Greeter that requires a Clock and a module that provides one, and let Kernel resolve the match.
Requires nothing
Concepts
- Module
- A type with a stable ModuleId (const ID). Modules are the units Kernel composes.
- Requires / Provides
- Requires<C> on a module declares that it needs capability C. Provides<C> declares that it can supply a value for C.
- Resolver
- Kernel's Resolver::resolve takes the requirement, the available provisions and the selected provider, and returns the provided value or a composition error.
Walkthrough
- Run it
This example adds rustclamp-kernel. The fixed clock is at 42 seconds, so the output is stable.
cargo run --offline --locked --manifest-path examples/a2-module/Cargo.toml --example a2-module # Hello, world! (unix second 42, principal Ada, request 7, tick 12) - Declare the requirement and the provider
A module has an ID. GreeterModule says it requires the Clock capability; FixedClockModule says it provides it.
pub struct FixedClockModule(pub SystemTime); impl Module for FixedClockModule { const ID: ModuleId = CLOCK_MODULE; } impl Provides<ClockCapability> for FixedClockModule { fn provided_value(&self) -> &(dyn Clock + 'static) { self } } pub struct GreeterModule; impl Module for GreeterModule { const ID: ModuleId = GREETER_MODULE; } impl Requires<ClockCapability> for GreeterModule {} - Resolve with Kernel
Resolver::resolve finds the provider for the requirement and returns the Clock. Resolution returns an error if no provider matches.
let provisions = [Provision::<ClockCapability>::from_module(&clock)]; let requirement = CapabilityRequirement::<ClockCapability>::from_module::<GreeterModule>(); let clock = Resolver::resolve::<ClockCapability>( requirement.required_by(), &provisions, requirement.selected_provider(), )?; let greeter = Greeter::new(clock); - Run the tests
The tests in tests/application.rs cover the example.
cargo test --offline --locked --manifest-path examples/a2-module/Cargo.toml
Give FixedClockModule a different time in main, for example 100 seconds. The output changes to unix second 100 and nothing else needs editing.
let clock = FixedClockModule(UNIX_EPOCH + std::time::Duration::from_secs(100));Verify
tests/application.rs checks that the application module uses Core and Kernel without the facade, that request state is per invocation and not a capability, and that a non-Send clock can be borrowed without shared locking.
cargo test --offline --locked --manifest-path examples/a2-module/Cargo.tomlexamples/a2-module
Ask ClampBot about this
CLI commands
Build a command tree from contributions declared by separate modules, and run its commands.
Requires nothing
Concepts
- Contribution
- A declaration a module hands to a target, here CommandDeclaration::new(name, function).
- Target
- The consumer of contributions. CliCommandTarget validates names and builds the CommandTree; Core and Kernel only carry the declarations.
- Qualifier
- A type-level label on a target. PublicCommands and AdminCommands are different types, so their declarations cannot be mixed.
Walkthrough
- Run it
Two modules contribute public commands, one contributes an admin command, and a second admin target is built empty.
cargo run --offline --locked --manifest-path examples/a3-contribution/Cargo.toml --example a3-contribution # hello: hello at 42 # goodbye: goodbye # admin command count: 1 # empty admin command count: 0 - Declare a command
A module contributes a named declaration for a typed target. PublicCommands and AdminCommands are different Rust types, so a public command cannot be passed to an admin target.
impl HelloModule { pub fn contribution() -> CommandDeclaration<PublicCommands> { CommandDeclaration::new("hello", hello) } } - Build the tree
TargetComposition passes the declarations to the CLI target, which validates duplicate names and sorts them. The result is a CommandTree you run by name; hello needs a Clock, goodbye does not.
let target = CliCommandTarget::<PublicCommands>::new(); let commands = TargetComposition::<_, PublicCommands>::new(vec![ (HelloModule::ID, HelloModule::contribution()), (GoodbyeModule::ID, GoodbyeModule::contribution()), ]) .build(Some(&target)) .expect("the CLI target accepts these declarations") .expect("a target produces a command tree"); commands.execute("hello", Some(&clock)).unwrap() - Run the tests
Tests cover duplicate command names, a required declaration with no target, an empty target, and a third-party target.
cargo test --offline --locked --manifest-path examples/a3-contribution/Cargo.toml
In examples/a3-contribution.rs, register HelloModule::contribution() under GoodbyeModule::ID as well as HelloModule::ID. The first expect panics with a CliBuildError that names the target, the command hello and both contributor modules.
(GoodbyeModule::ID, HelloModule::contribution()),Verify
tests/contribution.rs checks that the tree runs both commands with the declared Clock dependency, that duplicate names give a structured target error, that required contributions with no target are reported, that empty and qualified targets are isolated, and that a third-party target works through the public contracts.
cargo test --offline --locked --manifest-path examples/a3-contribution/Cargo.tomlexamples/a3-contribution
Ask ClampBot about this
Configuration and lifecycle
See how a Clamp process starts and stops resources in dependency order, and how rustclamp::config reads settings from .env and the environment.
Requires nothing (the Tokio adapter is optional: --features tokio-runtime)
Concepts
- Lifecycle phase
- One step a module can take part in: Initialize, Start, Ready, Drain, Cancel, TaskStop, Stop. A module lists only the phases it implements; modules that implement only Module have none.
- Dependency-derived order
- Providers initialize before their consumers and stop after them. The order is computed from the resolved requirements.
- Config
- rustclamp::config::Config holds key/value settings from .env plus the process environment. The environment wins; Debug prints keys only, never values.
Walkthrough
- Run the lifecycle example
The example uses fake resources and a fake clock. It prints the dependency edges, then each lifecycle phase with its time. The default build has no Tokio.
cargo run --offline --locked --manifest-path examples/a5-lifecycle/Cargo.toml --example a5-lifecycle # example.lifecycle.users requires example.lifecycle.database # example.lifecycle.worker requires example.lifecycle.users # Initialize: example.lifecycle.database (2 ms) # ... # Stop: example.lifecycle.database (13 ms) # state: Stopped; ready: false; elapsed: 14 ms - Read the order
Database starts before Users, and Users before Worker. Shutdown drains the Worker first and stops the Database last. The order comes from the resolved requirements, not from a hand-written list.
Initialize: example.lifecycle.database (2 ms) Initialize: example.lifecycle.users (3 ms) Start: example.lifecycle.users (4 ms) Start: example.lifecycle.worker (5 ms) ... Stop: example.lifecycle.worker (11 ms) Stop: example.lifecycle.users (12 ms) Stop: example.lifecycle.database (13 ms) - Read settings with rustclamp::config
The facade's config feature (included by web) parses .env text. Real environment variables win over .env. A key that is set but does not parse as the requested type panics at startup, naming the key.
use rustclamp::config::Config; let config = Config::parse("APP_NAME=Site\nMAIL_PER_DAY=100\n"); assert_eq!(config.get("APP_NAME"), Some("Site")); assert_eq!(config.get_or("MAIL_PER_DAY", 50), 100); assert_eq!(config.get_or("MISSING", 50), 50); - Run the tests
The twelve default-feature tests cover startup order, failure cleanup, deadlines and programmatic shutdown.
cargo test --offline --locked --manifest-path examples/a5-lifecycle/Cargo.toml
Add a typed read with a default. get_or parses the value as the default's type, so MAIL_PER_DAY=100 gives 100 and a missing key gives 50. A value that does not parse panics at startup, naming the key.
let config = Config::parse("MAIL_PER_DAY=abc\n");
let _ = config.get_or("MAIL_PER_DAY", 50); // panics: not a numberVerify
tests/lifecycle.rs covers dependency-derived order, freeze and validation before side effects, failure cleanup of only acquired modules, deadlines, bounded diagnostic flush, shared application resources with isolated process state, and the external-database path where hooks never run. The all-features run adds the Tokio adapter.
cargo test --offline --locked --manifest-path examples/a5-lifecycle/Cargo.toml
cargo test --offline --locked --manifest-path examples/a5-lifecycle/Cargo.toml --features tokio-runtimeexamples/a5-lifecycle
Ask ClampBot about this
HTTP API with PostgreSQL
Call one Users operation from a console command and from an HTTP server, with the same domain code.
Requires nothing for console and HTTP (in-memory repository); PostgreSQL only for the optional postgres feature (RUSTCLAMP_TEST_DATABASE_URL=postgres://...)
Concepts
- Port
- UserRepository is a trait owned by the domain. The memory adapter and the PostgreSQL adapter implement it; the Users operation does not know which one it has.
- Route contribution
- HttpRoute::<Public>::new(path, handler) is a declaration; HttpRoutes<Public> is the target that compiles all of them into one Axum router.
- Request context
- with_request_context adds the principal, tenant (x-tenant-id), correlation id and a deadline to every request. The domain checks authorization itself.
Walkthrough
- Run the console command
The console adapter uses the in-memory repository. It has no HTTP, PostgreSQL or Tokio dependency.
cargo run --offline --locked --manifest-path examples/01-users/Cargo.toml --bin users-console -- create Ada # created user 1: Ada - Start the HTTP server
The http feature adds the Axum-based rustclamp-http. The server listens on 127.0.0.1:3000 with an in-memory repository.
cargo run --offline --locked --manifest-path examples/01-users/Cargo.toml --features http --example 01-users-http - Call it
The demo identity accepts the header Authorization: Bearer demo. POST /users creates a user; GET /users/{id} reads it; /users/{id}/stream is an event stream; /health returns ok.
curl -i -X POST localhost:3000/users \ -H 'Authorization: Bearer demo' \ -H 'Content-Type: application/json' \ -d '{"name":"Ada"}' # 201 Created {"id":1,"name":"Ada"} curl localhost:3000/users/1 -H 'Authorization: Bearer demo' # {"id":1,"name":"Ada"} - See how routes are declared
Routes are contributions compiled by Kernel's TargetComposition into an HttpRoutes target. They compile before the listener binds.
let declarations = vec![ ( UsersHttp::ID, HttpRoute::<Public>::new("/users", post(create_user::<R>).with_state(state.clone())), ), ( UsersHttp::ID, HttpRoute::<Public>::new("/users/{id}", get(get_user::<R>).with_state(state.clone())), ), ]; let target = HttpRoutes::<Public>::new(); let compiled = TargetComposition::<HttpRoutes<Public>, Public>::new(declarations) .build(Some(&target)) - Run the tests
Default tests cover the console path. Add --features http --all-targets for the HTTP path. The PostgreSQL adapter test uses RUSTCLAMP_TEST_DATABASE_URL.
cargo test --offline --locked --manifest-path examples/01-users/Cargo.toml --features http --all-targets
Call the server without the Authorization header. A missing header means no principal, and the Users operation then returns an Unauthorized error (users.rs checks context.principal()). Any value other than Bearer demo is rejected by the demo resolver. Use curl -i to see the status line.
curl -i -X POST localhost:3000/users -H 'Content-Type: application/json' -d '{"name":"Ada"}'Verify
tests/users.rs checks that direct and console paths call the same operations, and that authorization, validation, tenant and deadline are domain rules. With --features http the HTTP tests also run; tests/postgres.rs needs a PostgreSQL service.
cargo test --offline --locked --manifest-path examples/01-users/Cargo.toml
cargo test --offline --locked --manifest-path examples/01-users/Cargo.toml --features http --all-targetsexamples/01-users
Ask ClampBot about this
In-process events
Turn an in-process domain event into a versioned message envelope and print it as JSON.
Requires nothing (rustclamp-messaging also has a bounded in-memory bus; no broker)
Concepts
- MessageEnvelope
- The versioned message record: id, name, schema_version, correlation_id, causation_id, deadline_unix_ms and a JSON payload.
- Boundary mapping
- The application maps a domain event to an envelope explicitly. The event type carries no serialization traits.
- In-memory bus
- rustclamp-messaging includes a bounded in-memory bus (InMemoryMessageBus) for tests and development without a broker.
Walkthrough
- Run it
The example needs only rustclamp-messaging and serde_json. The Cargo example is named 05-messaging.
cargo run --offline --locked --manifest-path examples/05-messaging/Cargo.toml --example 05-messaging - Read the output
The output is the serialized envelope on one line.
{"id":"message-42","name":"users.user-created","schema_version":1,"correlation_id":"request-7","causation_id":null,"payload":{"name":"Ada","tenant":"acme","user_id":42}} - Read the mapping
UserCreated has no serialization traits. The application maps it to a MessageEnvelope at the process boundary, so the message contract does not change the domain type.
fn map_user_created(event: UserCreated, context: PublishContext) -> MessageEnvelope { let payload: Value = json!({ "user_id": event.id, "tenant": event.tenant, "name": event.name, }); MessageEnvelope { id: context.message_id, name: "users.user-created".to_owned(), schema_version: 1, correlation_id: context.correlation_id, causation_id: None, deadline_unix_ms: None, payload, } }
Change the envelope name to users.user-renamed in examples/05-messaging/examples/mapping.rs and re-run. Only the name field of the printed JSON changes.
name: "users.user-renamed".to_owned(),Verify
This example has no tests/ directory. tools/check.py formats, lints, tests and runs it, and the run prints the envelope.
cargo test --offline --locked --manifest-path examples/05-messaging/Cargo.tomlexamples/05-messaging
Ask ClampBot about this
A website
Create a web project with clamp init --web and run it, or serve a folder of static pages through Clamp's HTTP route target.
Requires clamp CLI, Node/npm (for the Vite+ frontend). SQLite is compiled in; no external server. The static example needs nothing
Concepts
- Router
- rustclamp::web::Router. Routes are added with view, group, middleware and up; Router::package adds a reusable web package.
- View
- A Blade-style template in app/resources/views, rendered with router.view(path, name).
- Route target
- In the static example, HttpRoutes<Public> is the Clamp target that routes are contributed to, the same one used by 01-users.
Walkthrough
- Create the project
clamp init --web writes a Rust server with app/, routes/, public/, tests/ and a Vite+ and Tailwind 4 frontend. The frontend needs Node and npm. Then copy .env.example, generate APP_KEY and start both the Vite watcher and the server.
clamp init my-site --web cd my-site cp .env.example .env clamp key:generate clamp dev - Read the routes
Pages and JSON endpoints are separate files. The welcome page is a view; the API group is rate limited and has a health check.
// app/routes/web.rs pub fn routes(router: Router) -> Router { router.view("/", "welcome") } // app/routes/api.rs pub fn routes(router: Router, settings: &Settings) -> Router { let throttle = settings.per_minute(settings.api_per_minute); router.group(|api| api.middleware(throttle.middleware()).up("/api/health")) } - Open it
The server prints its address: the first free port from 8080, or PORT if set. /api/health answers {"status":"ok"} while the database answers. cargo dev starts only the Rust server, so run npm run build first.
- Run the generated tests
tests/routes.rs calls the app like a browser without a server.
cargo test - Alternative: serve static pages
examples/08-site-server serves a folder through the public HTTP route target on 127.0.0.1:8002. The folder needs an index.html and a 404.html. The README builds the folder with python3 rustclamp.com/build.py and passes rustclamp.com/public; any such folder works.
cargo run --offline --manifest-path examples/08-site-server/Cargo.toml \ --example 08-site-server -- <folder> # serving 127.0.0.1:8002 at http://127.0.0.1:8002
Add a view file under app/resources/views and a route for it in app/routes/web.rs. Restart or let clamp dev rebuild, then open the path. The welcome page is wired the same way.
router.view("/", "welcome").view("/about", "about")Verify
The generated tests/routes.rs calls the app through a test Client with no server and checks, among others, that /api/health returns JSON, an unknown path is 404, and every response carries security headers. 08-site-server has no tests; check.py builds it with fmt and clippy.
cargo test # inside the generated projectexamples/08-site-server
Ask ClampBot about this
A worker over JetStream
Send an email request from an API through a message queue to a Worker handler, in memory or through NATS JetStream.
Requires nothing for the in-memory run; NATS with JetStream (docker run nats:2-alpine -js) for email-worker and email-api
Concepts
- Handler declaration
- HandlerDeclaration::typed(name, version, closure) declares a Worker handler for one message name and schema version.
- Failure classification
- A handler returns HandlerFailure. Per the README, only retryable failures may be attempted again; an unknown outcome is dead-lettered; permanent is final.
- At-least-once delivery
- JetStream may redeliver after a crash or an expired lease, so the email operation has to be idempotent before real mail is sent. Retries are at 1, 5, 15 and 30 seconds, up to five deliveries, then email.dead.
Walkthrough
- Run it in one process
The API publishes to a bounded in-memory bus; a Worker handler receives and delivers it. No broker is needed.
cargo run --offline --locked --manifest-path examples/03-email-worker/Cargo.toml --example 03-email-worker # delivered 1 email(s) - Read the handler
A handler is declared for the message name email.send, version 1, and collected into a registry through TargetComposition. Here a delivery error is classified as a permanent failure.
let handler = HandlerDeclaration::typed(SEND_EMAIL, 1, move |payload: EmailPayload, _| { let gateway = worker_gateway.clone(); async move { send_email(payload.into(), &gateway) .map_err(|error| HandlerFailure::Permanent(Box::new(error))) } }); let registry = TargetComposition::<HandlerTarget, WorkerHandlers>::new(vec![(EMAIL_MODULE, handler)]) .build(Some(&HandlerTarget)) - Start JetStream for separate processes
To run the API and Worker as two processes, start a local JetStream server. Both read NATS_URL, which defaults to nats://127.0.0.1:4222.
docker run --rm --name rustclamp-nats -p 4222:4222 nats:2-alpine -js - Run Worker and API
Start the worker in one terminal and the API in another. Add --features tracing to either command for spans with correlation and message IDs.
cargo run --offline --locked --manifest-path examples/03-email-worker/Cargo.toml --bin email-worker cargo run --offline --locked --manifest-path examples/03-email-worker/Cargo.toml --bin email-api
In examples/run.rs call api.send(...) twice and receive and dispatch twice. The final line should read delivered 2 email(s). The bus was created with capacity 8.
Verify
This example has no tests/ directory. check.py formats, lints, tests and runs the in-memory example; the JetStream behavior in the README (acknowledge after success, dead letter, drain on Ctrl-C) is described but needs a NATS server to observe.
cargo test --offline --locked --manifest-path examples/03-email-worker/Cargo.tomlexamples/03-email-worker
Ask ClampBot about this
Orders that survive a crash
Create an order and its outbox message in one PostgreSQL transaction, publish it to NATS, and have a Worker apply it once.
Requires PostgreSQL and NATS with JetStream (docker commands in step 1); the phase7-inspect command needs neither
Concepts
- Transactional outbox
- The order, the inventory change and the outgoing message row are written in one database transaction. A separate publisher sends pending rows later.
- Inbox
- The Worker records each message id in a durable inbox table in the same transaction as its effect, so a duplicate delivery is skipped and acknowledged.
- Unknown outcome
- A payment timeout is not a decline: the provider may have charged, so the order keeps its inventory reservation and is not charged again automatically.
Walkthrough
- Start PostgreSQL and NATS
Run PostgreSQL and NATS with JetStream, then set DATABASE_URL. NATS_URL defaults to nats://127.0.0.1:4222.
docker run --rm --name rustclamp-orders-db -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=orders -p 5432:5432 postgres:17 docker run --rm --name rustclamp-orders-nats -p 4222:4222 nats:2-alpine -js export DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/orders - Migrate, then start the publisher and worker
Run these in separate terminals. The paths below are relative to the rustclamp repository; the README writes them with a rustclamp/ prefix for the combined checkout.
M=examples/06-create-order/Cargo.toml cargo run --offline --locked --manifest-path $M --bin orders-api -- migrate cargo run --offline --locked --manifest-path $M --bin outbox-publisher cargo run --offline --locked --manifest-path $M --bin orders-worker - Create an order
create decrements inventory, inserts the order and inserts the outbox message in one transaction. The payment command uses a local stub with the outcomes approve, decline, timeout and retryable.
cargo run --offline --locked --manifest-path $M --bin orders-api -- create widget 2 cargo run --offline --locked --manifest-path $M --bin orders-api -- payment 1 timeout - Read the transaction
If any step fails, nothing is committed, so an order never exists without its message.
let mut transaction = database.begin().await?; let remaining: Option<i32> = sqlx::query_scalar( "UPDATE inventory SET available = available - $1 WHERE sku = $2 AND available >= $1 RETURNING available", ) .bind(quantity).bind(sku) .fetch_optional(&mut *transaction).await?; // ... INSERT INTO orders ... RETURNING id sqlx::query("INSERT INTO outbox (message_id, payload) VALUES ($1, $2)") .bind(&message.id) .bind(serde_json::to_string(&message)?) .execute(&mut *transaction) .await?; transaction.commit().await?; - Inspect the processes without any service
phase7-inspect prints the API, publisher, worker and scheduler process projections from the Kernel blueprint. It needs neither PostgreSQL nor NATS.
cargo run --offline --locked --manifest-path $M --bin phase7-inspect
Run the payment command with each stub outcome: approve, decline, timeout, retryable. Per the README, decline restores inventory in a compensation transaction, timeout leaves the reservation, and retryable leaves the order unchanged.
cargo run --offline --locked --manifest-path $M --bin orders-api -- payment 1 declineVerify
There is no tests/ directory. tools/check.py builds, lints and tests the example package, and phase7-inspect checks the process boundaries offline. The outbox and inbox behavior can only be seen with PostgreSQL and NATS running, as in step 1.
cargo test --offline --locked --manifest-path examples/06-create-order/Cargo.toml
cargo run --offline --locked --manifest-path examples/06-create-order/Cargo.toml --bin phase7-inspectexamples/06-create-order
Ask ClampBot about this
Scheduled jobs
Declare scheduled jobs, drive them with a controlled clock, and watch catch-up, skipped misfires and drain on shutdown.
Requires nothing
Concepts
- MisfirePolicy
- What to do when a job is late. Skip drops the job if it is at least one full interval late. RunOnce runs one invocation for any number of missed intervals.
- Job declaration
- JobDeclaration::new(name, interval, policy, async closure) is a contribution to SchedulerTarget.
- Admission
- After shutdown starts the scheduler stops admitting new due jobs but lets the active one drain: later invocations: 0.
Walkthrough
- Run it
The example uses a manual clock and a standard-library executor, not Tokio or cron. It prints each job invocation and counts per tick.
cargo run --offline --locked --manifest-path examples/04-scheduler/Cargo.toml --example 04-scheduler # ... # invocations: 2, 2, 1; skipped misfires: 1 # shutdown drained 1; later invocations: 0 - Declare the jobs
Each job has a name, an interval and a MisfirePolicy. RunOnce catches up once after missed intervals; Skip drops them.
let catch_up_job = JobDeclaration::new( "cleanup-expired-sessions", Duration::from_secs(10), MisfirePolicy::RunOnce, || async { println!("cleanup operation invoked"); Ok(()) }, ); let skip_missed_job = JobDeclaration::new( "prune-old-sessions", Duration::from_secs(10), MisfirePolicy::Skip, || async { println!("prune operation invoked"); Ok(()) }, ); - Tick with a clock you control
TargetComposition builds a scheduler from the declarations. The example moves the clock by 10 s and then 40 s and calls tick each time. The third tick reports one skipped misfire.
let scheduler = TargetComposition::<SchedulerTarget, ScheduledJobs>::new(vec![ (CLEANUP_MODULE, catch_up_job), (PRUNE_MODULE, skip_missed_job), ]) .build(Some(&SchedulerTarget)) .expect("job declarations are valid") .expect("selected target produces a scheduler"); let first = futures_executor::block_on(scheduler.tick(&clock)); - Run the tests
Tests are in tests/scheduler.rs.
cargo test --offline --locked --manifest-path examples/04-scheduler/Cargo.toml
Change prune-old-sessions from MisfirePolicy::Skip to MisfirePolicy::RunOnce. The third tick then runs both jobs and the summary line becomes invocations: 2, 2, 2; skipped misfires: 0.
MisfirePolicy::RunOnce,Verify
tests/scheduler.rs checks that the controlled clock applies RunOnce and Skip, that stopped admission prevents later due jobs, and that failed jobs can run again while overlapping ticks skip the active job.
cargo test --offline --locked --manifest-path examples/04-scheduler/Cargo.tomlexamples/04-scheduler
Ask ClampBot about this
Split into processes
Describe one application with two processes, CLI and Worker, and see which modules each one includes.
Requires nothing
Concepts
- Process
- An application execution boundary selected by ID. Here it is not an operating-system process.
- Projection
- The resolved view of one process: the modules reachable from its roots, with an inclusion reason and path, plus selected providers and exclusions.
- Reachability
- Only modules reachable from a process's roots are validated. A dormant module with an unsatisfied requirement does not break the CLI process.
Walkthrough
- Run both processes
Each run prints the modules the selected process includes. The Worker reads WORKER_CONCURRENCY, which must be a positive integer; the CLI never reads it.
cargo run --offline --locked --manifest-path examples/a4-process/Cargo.toml --example a4-process -- cli WORKER_CONCURRENCY=4 cargo run --offline --locked --manifest-path examples/a4-process/Cargo.toml --example a4-process -- worker - Read the output
Both share the Clock and Epoch providers. Only the Worker includes the queue.
example.process.cli projection includes: example.process.cli-root, example.process.clock, example.process.epoch, example.process.greeting-command example.process.worker projection includes: example.process.clock, example.process.epoch, example.process.queue, example.process.worker-root; worker concurrency: 4 - Inspect a process
inspect prints the roots, the root-to-module path for each module, resolved providers, and excluded modules. The dormant module requires an undeclared database but is only listed as Unreachable, so it does not block the CLI.
cargo run --offline --locked --manifest-path examples/a4-process/Cargo.toml --example a4-process -- inspect cli # ... # excluded modules: # example.process.dormant (Unreachable) # example.process.queue (Unreachable) # example.process.worker-root (Unreachable) - Read the blueprint
One ApplicationBlueprint declares the modules, two executions with their roots, two processes, and the provider requirements.
application .add_execution(CLI_EXECUTION, CLI_ROOT) .add_execution(WORKER_EXECUTION, WORKER_ROOT) .add_process(CLI_PROCESS, vec![CLI_EXECUTION]) .add_process(WORKER_PROCESS, vec![WORKER_EXECUTION]) .require_provider(GREETING_COMMAND, ClockCapability::ID, None, CLOCK_PROVIDER) .require_provider(CLOCK_PROVIDER, EPOCH_CAPABILITY, None, EPOCH_PROVIDER) .require_provider(WORKER_ROOT, QUEUE_CAPABILITY, None, QUEUE_PROVIDER); - Run the tests
Tests start both projections together and check their state is independent.
cargo test --offline --locked --manifest-path examples/a4-process/Cargo.toml
Run the Worker with WORKER_CONCURRENCY=0, or without it. You get startup failed: WorkerSetting { ... setting: "WORKER_CONCURRENCY", problem: Invalid } or problem: Missing. The CLI run is unaffected.
WORKER_CONCURRENCY=0 cargo run --offline --locked --manifest-path examples/a4-process/Cargo.toml --example a4-process -- workerVerify
tests/process.rs checks that CLI reaches its command and the shared clock but not Worker infrastructure (and the reverse), that a dormant invalid module does not break CLI, that CLI ignores missing or invalid Worker config while Worker reports it, and that one host runs both projections with isolated state.
cargo test --offline --locked --manifest-path examples/a4-process/Cargo.tomlexamples/a4-process
Ask ClampBot about this
A device loop
Run a fixed-step simulation loop with a fake device and check it replays identically.
Requires nothing (the device is a fake sensor; no hardware)
Concepts
- Fixed-step replay
- The simulation advances in whole steps with integer units and a seeded random source, so the same seed and inputs give the same states.
- Controlled sources
- Clock, random and input are supplied by the caller, so a test can control them.
- CanTarget
- A target that compiles typed CAN decoders into a sorted table and rejects duplicate CAN IDs.
Walkthrough
- Run it
The loop applies three controlled inputs and prints the state after each step.
cargo run --offline --locked --manifest-path examples/07-device-loop/Cargo.toml --example 07-device-loop # step=1 position=2mm velocity=3mm/step # step=2 position=5mm velocity=2mm/step # step=3 position=7mm velocity=5mm/step - Compare with plain Rust
A second example, 07-device-loop-direct, writes the same loop with ordinary Rust and prints the same three lines.
cargo run --offline --locked --manifest-path examples/07-device-loop/Cargo.toml --example 07-device-loop-direct - Read the update order
Simulation::step owns the update order: position, then input, then the random sample. State uses integers so replay is exact.
pub fn step(state: &mut State, input: Input, random: &mut Seeded) { state.position_mm += state.velocity_mm_per_step; state.velocity_mm_per_step += i64::from(input.delta); state.noise = random.next_u64(); state.steps += 1; } - Run the tests
Tests run the fixture twice with the same seed and inputs and compare every state. The loop is stdlib-only and the default graph has no Tokio. Feature tokio-service selects the async runtime.
cargo test --offline --locked --manifest-path examples/07-device-loop/Cargo.toml
In examples/direct.rs, add a fourth delta, for example [1, -1, 3, 0]. The extra line is step=4 position=12mm velocity=5mm/step. direct.rs is the plain Rust twin of the example.
for (step, delta) in [1_i64, -1, 3, 0].into_iter().enumerate() {Verify
tests/device_loop.rs checks that replay is repeatable and the seed controls randomness, that the loop stops on cancellation, that raw sensor values are validated, that the sensor queue applies backpressure, that duplicate CAN frames are rejected, and that missing hardware capabilities produce Kernel diagnostics.
cargo test --offline --locked --manifest-path examples/07-device-loop/Cargo.tomlexamples/07-device-loop
Ask ClampBot about this
no_std Core
Use a Core module identity from a #![no_std] library with alloc, without Kernel or Runtime.
Requires nothing
Concepts
- no_std + alloc
- The library has no std dependency, only the alloc crate for Vec. The host binary still uses std.
- Incremental adoption
- Application state stays an ordinary Rust-owned Vec. Core is added only for a module identity.
- Portability boundary
- Core's Clock exposes std::time::SystemTime, so Core itself is not shown to build on bare metal.
Walkthrough
- Run it
A host std binary calls into the no_std library.
cargo run --offline --locked --manifest-path examples/a6-platform-neutral/Cargo.toml --example a6-platform-neutral # example.platform-neutral.thermometer: 21000 mC - Read the library
The crate is #![no_std] and uses alloc for Vec. State stays in an ordinary Rust-owned Vec; Core adds only a module identity.
#![no_std] #![forbid(unsafe_code)] #![deny(missing_docs)] extern crate alloc; use alloc::vec::Vec; use rustclamp_core::ModuleId; pub struct Thermometer; impl rustclamp_core::Module for Thermometer { const ID: ModuleId = ModuleId::new("example.platform-neutral.thermometer"); } - Know the limit
This shows source compatibility of the consumer on the host. It does not show that Core builds on a bare-metal target: Core's Clock still exposes std::time::SystemTime.
- Run the tests
Tests are in tests/consumer.rs.
cargo test --offline --locked --manifest-path examples/a6-platform-neutral/Cargo.toml
In examples/main.rs record a second value and print history.readings(). It returns the samples in insertion order.
history.record(22_000);
println!("{:?}", history.readings()); // [21000, 22000]Verify
tests/consumer.rs has one test: an ordinary Rust resource survives incremental Core adoption. check.py asserts the example has no Kernel or Runtime dependency and that the facade is absent.
cargo test --offline --locked --manifest-path examples/a6-platform-neutral/Cargo.tomlexamples/a6-platform-neutral
Ask ClampBot about this
Mount an existing Axum app
Hand Clamp a whole axum::Router instead of re-declaring routes: the “start where you are” path for Axum services.
SQLite and Redis
More storage on the same boundaries as PostgreSQL.
Track itMetrics and tracing
Correlated tracing exists across API and Worker; exported metrics and a full observability step come next.
Track itSeveral services
Compose multiple services that share contracts, each deployed on its own.
Track it