# API Ergonomics
This document captures what we want the `ready-active-safe` API to feel like.
The goal is Rails level readability in Rust without drifting into a framework.
This is not a contract document.
For the authoritative API contract, see `docs/DESIGN.md`.
For the product identity, see `docs/VISION.md`.
## North Star
Within 15 minutes, a new user should be able to:
1. add the dependency
2. define `Mode`, `Event`, and `Command`
3. write one readable `match (mode, event)`
4. assert on the returned `Decision`
If that takes longer, we fix docs or we fix the API.
## Constraints We Do Not Break
We want great DX, but we keep the core boring and explicit:
- `Machine::on_event` stays pure
- decisions stay data
- effects stay in the runtime
- no proc macro DSL as the primary interface
- time stays out of the base trait
If an idea makes the crate feel like a generic FSM framework, it is probably out of scope.
## What Great Readability Looks Like
Great user experience is not about being clever.
It is about making the happy path obvious and keeping the mental model small.
In practice, that means:
- one import path for common usage
- names that match what people already call things
- a transition definition that you can scan in a few seconds
- a testing story where you assert on outcomes as data
## Ten Reference Projects With Great DX
These are not all direct competitors.
They are reference points for readability, documentation quality, and user flow.
### behave (Rust, testing DSL)
Repo: https://github.com/stateruntime/behave
behave is a good reminder that tests are part of DX.
The best interfaces make the readable thing the easy thing.
What we copy:
keep our own tests short and scenario shaped, and keep failure messages clear.
### statig (Rust, state machines)
Repo: https://github.com/mdeloof/statig
statig reads well because the happy path is simple and close to plain Rust.
Even when it uses macros, the concepts are stable and familiar.
What we copy:
keep state handlers readable and keep transitions explicit.
What we skip:
proc macros as the default in the core crate.
### smlang (Rust, transition tables)
Repo: https://github.com/korken89/smlang
smlang is scannable because the definition is a table, not a pile of control flow.
Guards and actions sit next to the transition they belong to.
What we copy:
make the important transitions easy to scan in one place.
What we skip:
a macro DSL as the primary interface.
### AASM (Ruby, state machines)
Repo: https://github.com/aasm/aasm
Ruby state machine libraries often win on language.
Method names read like English, and docs focus on the happy path first.
What we copy:
start docs with a small working example, then deepen.
### Statesman (Ruby, workflows)
Repo: https://github.com/gocardless/statesman
Statesman is opinionated about audit and history.
That focus makes the library feel like it was built for production, not toy examples.
What we copy:
make audit and replay a first class story, even if implementation ships later.
### Stateless (C#, state machines)
Repo: https://github.com/dotnet-state-machine/stateless
Stateless is widely liked because it reads as configuration.
Transitions are easy to inspect, and it is easy to test.
What we copy:
keep transition intent explicit, and keep the public surface small.
### transitions (Python, state machines)
Repo: https://github.com/pytransitions/transitions
transitions is popular because it is easy to adopt.
It has a short path from install to a working machine.
What we copy:
make the first success fast, and keep the docs copy and paste friendly.
### XState (JS and TS, statecharts)
Docs: https://stately.ai/docs/machines
XState nails inspectability.
Machines are data, so it is easy to log, visualize, and debug.
What we copy:
lean into decisions as inspectable data.
What we skip:
hierarchy and concurrency in the core kernel.
### looplab fsm (Go, state machines)
Repo: https://github.com/looplab/fsm
Go libraries often feel readable because they do not fight the language.
They keep the vocabulary small and keep configuration obvious.
What we copy:
keep our own vocabulary small and consistent across docs and code.
### Tinder StateMachine (Kotlin, state machines)
Repo: https://github.com/Tinder/StateMachine
This library is loved because its definitions read like a simple story.
It also makes side effects explicit via separate actions.
What we copy:
keep the decision boundary clean, and keep actions separate from transitions.
## What This Means For ready-active-safe
These are concrete DX rules that keep us aligned with the vision:
- keep `ready_active_safe::prelude::*` as the one import for typical usage
- keep `Machine::on_event` readable without helper macros
- keep the most common runtime checks easy to write, like `Decision::target_mode()`
- keep match arms sentence shaped with small helpers
- keep runtimes simple with move based helpers instead of clones
Concrete helpers we provide:
- `stay()` and `transition(...)` as wrappers around `Decision::stay()` and `Decision::transition(...)`
- `Decision::emit(...)` and `Decision::emit_all(...)` as readable command helpers
- `apply_decision(...)` to apply a decision without cloning
- `Policy::check(...)` to return a reason string for denied transitions
- `apply_decision_checked(...)` to combine policy checks and application
## What We Avoid
These patterns tend to make Rust code harder to read:
- proc macro DSLs as the default user path
- APIs that hide control flow
- runtimes that dictate sync vs async choices
- deep type level tricks that make error messages worse
## Backlog Ideas That Improve Readability
This list is intentionally practical and small.
Each item should make real code easier to read, not just shorter.
- Add more golden path examples that feel like production code
- Use `runtime::Runner` in docs/examples to show the reference loop (mode storage, policy checks, dispatch)
- Keep docs and examples free of hidden doctest lines