# Qubit State Machine (`rs-state-machine`)
[](https://circleci.com/gh/qubit-ltd/rs-state-machine)
[](https://coveralls.io/github/qubit-ltd/rs-state-machine?branch=main)
[](https://crates.io/crates/qubit-state-machine)
[](https://docs.rs/qubit-state-machine)
[](https://www.rust-lang.org)
[](LICENSE)
[](README.zh_CN.md)
Documentation: [API Reference](https://docs.rs/qubit-state-machine)
`qubit-state-machine` is a small Rust finite state machine crate for lifecycle,
workflow, and task-state tracking code.
It provides immutable transition rules, validation at build time, and a
CAS-backed `AtomicRef` for applying events to shared state.
## Why Use It
Use `qubit-state-machine` when you need:
- explicit finite state machine rules built from enum-like state and event types
- immutable transition tables that can be shared across threads
- build-time validation for unknown states and conflicting transitions
- event-driven state updates through `trigger` and `try_trigger`
- success callbacks that observe the old and new state after an update
- simple state tracking for services, jobs, devices, or UI logic
## Installation
```toml
[dependencies]
qubit-state-machine = "0.2"
```
## Quick Start: Job Processing
```rust
use qubit_state_machine::{AtomicRef, StateMachine};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
enum JobState {
Queued,
Running,
Succeeded,
Failed,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
enum JobEvent {
Start,
Complete,
Fail,
}
fn create_job_machine() -> Result<StateMachine<JobState, JobEvent>, Box<dyn std::error::Error>> {
Ok(StateMachine::builder()
.add_states(&[
JobState::Queued,
JobState::Running,
JobState::Succeeded,
JobState::Failed,
])
.set_initial_state(JobState::Queued)
.set_final_states(&[JobState::Succeeded, JobState::Failed])
.add_transition(JobState::Queued, JobEvent::Start, JobState::Running)
.add_transition(JobState::Running, JobEvent::Complete, JobState::Succeeded)
.add_transition(JobState::Running, JobEvent::Fail, JobState::Failed)
.build()?)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let machine = create_job_machine()?;
assert!(machine.contains_state(JobState::Running));
assert!(machine.is_initial_state(JobState::Queued));
assert!(machine.is_final_state(JobState::Succeeded));
assert_eq!(
machine.transition_target(JobState::Queued, JobEvent::Start),
Some(JobState::Running),
);
let state = AtomicRef::from_value(JobState::Queued);
let running = machine.trigger(&state, JobEvent::Start)?;
assert_eq!(running, JobState::Running);
assert_eq!(*state.load(), JobState::Running);
let mut audit_log = Vec::new();
let finished = machine.trigger_with(&state, JobEvent::Complete, |old_state, new_state| {
audit_log.push((old_state, new_state));
})?;
assert_eq!(finished, JobState::Succeeded);
assert_eq!(audit_log, vec![(JobState::Running, JobState::Succeeded)]);
assert!(!machine.try_trigger(&state, JobEvent::Fail));
assert_eq!(*state.load(), JobState::Succeeded);
Ok(())
}
```
## Build-Time Validation
Invalid rules are rejected before a `StateMachine` is created.
```rust
use qubit_state_machine::{StateMachine, StateMachineBuildError};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
enum JobState {
Queued,
Running,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
enum JobEvent {
Start,
}
let error = StateMachine::builder()
.add_state(JobState::Queued)
.add_transition(JobState::Queued, JobEvent::Start, JobState::Running)
.build()
.expect_err("transition target must be registered");
assert_eq!(
error,
StateMachineBuildError::TransitionTargetNotRegistered {
source: JobState::Queued,
event: JobEvent::Start,
target: JobState::Running,
},
);
```
## Applying Events Without Error Handling
Use `try_trigger` or `try_trigger_with` when an invalid transition should be a
simple `false` result.
```rust
use qubit_state_machine::{AtomicRef, StateMachine};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
enum DoorState {
Open,
Closed,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
enum DoorEvent {
Close,
Reopen,
}
let machine = StateMachine::builder()
.add_states(&[DoorState::Open, DoorState::Closed])
.add_transition(DoorState::Open, DoorEvent::Close, DoorState::Closed)
.build()
.expect("rules should build");
let state = AtomicRef::from_value(DoorState::Open);
assert!(machine.try_trigger(&state, DoorEvent::Close));
```
## Common Next Steps
| Define states and transitions | `StateMachine::builder`, `StateMachineBuilder` |
| Add one or more states | `add_state`, `add_states` |
| Mark initial and final states | `set_initial_state`, `set_initial_states`, `set_final_state`, `set_final_states` |
| Add transition rules | `add_transition`, `add_transition_value`, `Transition` |
| Query transition targets without changing state | `transition_target` |
| Apply events and get detailed errors | `trigger`, `trigger_with`, `StateMachineError` |
| Apply events without handling errors | `try_trigger`, `try_trigger_with` |
| Store shared mutable state | `AtomicRef` |
## Core API At A Glance
| `Transition` | Immutable value describing `source --event--> target`. |
| `StateMachineBuilder` | Mutable builder for states, initial states, final states, and transitions. |
| `StateMachine` | Immutable, validated transition table used to query and trigger events. |
| `AtomicRef` | Re-exported atomic reference used for CAS-backed current state. |
| `StateMachineBuildError` | Validation error returned while building invalid rule sets. |
| `StateMachineError` | Runtime error returned when an event cannot be applied. |
## Project Scope
- `qubit-state-machine` is intended for simple finite state machines, not a full
workflow engine.
- State and event types should be small enum-like values implementing
`Copy + Eq + Hash + Debug`.
- Rule definitions become immutable after `StateMachineBuilder::build`.
- `trigger` accepts `AtomicRef<S>` directly.
- Event-driven transitions are installed through `qubit-cas`.
- Callbacks run after the CAS update has succeeded.
## Contributing
Issues and pull requests are welcome.
Please keep contributions focused and easy to review:
- open an issue for bug reports, design questions, or larger feature proposals
- keep pull requests scoped to one behavior change, fix, or documentation update
- run `./ci-check.sh` before submitting changes
- include tests when changing runtime behavior
- update the README when public API behavior changes
By contributing to this project, you agree that your contribution will be
licensed under the same license as the project.
## License
Licensed under the [Apache License, Version 2.0](LICENSE).