Appearance
fsm-rust
The rust consumer of fsm (docs/plans/fsm-v1.md, C-2). The crate is private and is a test bed: it is not published. It shows how the generated rust code of a fsm definition is used, and it checks that code against the conformance vectors. The generated library uses the standard library only; the orm crates are needed by the persistence example alone.
Generate into your project
Generate the library into a directory of your crate. The generated lib.rs is the crate root that declares one module per machine and fsm, so your Cargo.toml names it as the library path:
sh
fsm-gen gen --project /path/to/order-machines --kind rust --out ./srctext
[fsm-gen] gen: oktoml
[package]
name = "app"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/lib.rs"Run the command again after every change of the definition. The vectors come from fsm-gen vectors --kind rust --out <file>, written into a test file of your own (the test bed writes tests/conformance/vectors.rs).
Generated files
| File | Content |
|---|---|
src/lib.rs | the crate root: pub mod order; and pub mod fsm; (one line per machine) |
src/fsm.rs | the module fsm: the enum Code (Conflict, InvalidTransition, UnknownCode, ReservedCode, with as_str() giving E_CONFLICT and so on) and the struct Error |
src/order.rs | one module per machine: the enums State (#[repr(i32)], the value is the code) and Event (the position from 1), the struct Step, and the functions |
Each file starts with // Code generated by fsm-gen from <machine>; DO NOT EDIT..
API
The functions of the module order (src/order.rs):
| Function | Result |
|---|---|
initial() -> State | the initial state |
code(state: State) -> i32 | the code of a state |
key(state: State) -> &'static str | the key of a state, such as draft |
from_code(value: i64) -> Result<State, Error> | the state of a code; E_UNKNOWN_CODE for a value that is no state, E_RESERVED_CODE for a reserved code |
is_final(state: State) -> bool | whether a state is final |
events_from(state: State) -> &'static [Event] | the events that have an edge from a state, in declaration order |
target(expected: State, event: Event) -> Result<State, Error> | the state that the event leads to; E_INVALID_TRANSITION when it has no edge |
apply(expected: State, event: Event, actual: State) -> Result<Step, Error> | the result of one attempt (see the root README, Transitions) |
Step has the fields to: State and changed: bool. from_code takes an i64, so a value that a database returns is passed without narrowing it.
A complete apply call, copied from packages/rust/tests/conformance/vectors.rs (the first case of the apply feature, which is a test function that checks the result):
rust
#[test]
fn test_apply__order_apply_s0_e0_s0() {
crate::check_result(order::apply(order::State::Draft, order::Event::Next, order::State::Draft), Ok(order::Step { to: order::State::Paid, changed: true }));
}The same program outside the test bed, in a crate of the layout above with use app::{fsm::Error, order};, printed these lines when it was run against the generated files of this checkout (the first line is apply(Draft, Next, Draft), the second is the conflict of apply(Draft, Next, Shipped)):
text
2 true
E_CONFLICT Some(1) Some(3) Some(1)Errors
The functions return Result<_, fsm::Error>. Error (src/fsm.rs) is a struct with these fields:
| Field | Type | Meaning |
|---|---|---|
code | Code | the code of the error; code.as_str() gives "E_CONFLICT" and so on |
message | String | the message, which names the machine |
expected | Option<i32> | the expected state code, or None |
actual | Option<i32> | the stored state code, or None |
event | Option<i32> | the event position (from 1), or None |
Error implements Display (E_CONFLICT: the stored state ...) and std::error::Error.
Which fields a code sets is the rule of spec B-6 (docs/spec/fsm.md), and the vectors of tests/conformance/vectors.rs assert it:
| Code | expected | actual | event |
|---|---|---|---|
E_CONFLICT | Some | Some | Some |
E_INVALID_TRANSITION | Some | None | Some |
E_UNKNOWN_CODE, E_RESERVED_CODE | None | None | None |
Persistence
examples/persist.rs is the pattern of docs/spec/fsm.md, B-4 with polyspec orm on SQLite, PostgreSQL and MySQL (the DSN given to open selects the dialect). The stored state is a code in a row. transition runs the transaction of one transition: it reads the row under its lock (for_update), passes the stored code to the decide closure of the caller, and writes the row only when the closure reports a change. A deadlock runs the transaction again (orm transaction, three retries by default; the closure is pure, so a rerun is safe). A machine error is returned without a write, and a database error other than a deadlock is returned after one attempt:
rust
// packages/rust/examples/persist.rs, transition_with_fault (transition calls it with no fault)
pub async fn transition_with_fault(
db: &Db,
id: i64,
decide: impl Fn(i64) -> Result<(i32, bool), Error>,
fault: impl Fn() -> polyspec_orm::Result<()>,
) -> Result<(i32, bool), Failure> {
let outcome = db
.transaction(async || {
fault()?;
let row = Item::new().connect(db).id(id).for_update().get().await?;
let step = decide(i64::from(row.get_status()?));
if let Ok((to, true)) = step {
row.set_status(to).update(false).await?;
}
Ok(step)
})
.await
.map_err(Failure::Database)?;
outcome.map_err(Failure::Machine)
}The caller's closure is the apply use. It is copied from packages/rust/tests/conformance/vectors.rs (the persist case of the draft state with the next event):
rust
crate::check_persist(
order::code(order::State::Draft),
|stored| {
let actual = order::from_code(stored)?;
let step = order::apply(order::State::Draft, order::Event::Next, actual)?;
Ok((order::code(step.to), step.changed))
},
Ok((order::code(order::State::Paid), true)),
order::code(order::State::Paid),
)open, seed and fsm_of in examples/persist.rs create the tables, write a row and read it. The models come from contracts/schema/persist.dbs through build.rs, which writes them into OUT_DIR; a consumer that uses this pattern needs the build script of this package and the polyspec-orm crates.
Tests
From the checkout root:
sh
make -C packages/rust install # cargo fetch --locked: the crates of Cargo.lock (the orm release v0.0.18 by its git tag)
make -C packages/rust test # cargo test through kit's run-tests
make test-rust # the same from the root
make rust-persist-test-postgres # the persist cases on PostgreSQL (FSM_TEST_PG_DSN; a missing DSN exits 2)
make rust-persist-test-mysql # the persist cases on MySQL (FSM_TEST_MYSQL_DSN)The run passes 89 of 89 cases in about 100 seconds, which includes the compilation of the crate. tests/conformance/vectors.rs has one #[test] per vector case, named by its case name <feature>__<id> (vectors::<feature>__<id>), and each calls a check function of tests/conformance/main.rs; the persist cases are #[tokio::test]. The toolchain is rust-toolchain.toml at the root (Rust 1.99.0). The root make gen-check checks that the generated files are the ones fsm-gen writes today.