flowstate
Flowstate is library for modelling multi-step processes as self-executing state machines. It is heavily inspired by the typestate pattern but with the goal of making it easier to model self-executing workflows as finite state machines.
Typestate APIs make invalid state transitions impossible. However, the caller is typically responsible for driving the state transitions. In contrast, each state in a Flowstate workflow is responsible for transitioning to the next state. This allows workflows to be self-executing.
Basic Usage
The following is an example of a very basic workflow.
/**
* ╔═[BasicWorkflow]════════════════════════════╗
* ║ [StateA] ──> [StateB] ──> [WorkflowResult] ║
* ╚════════════════════════════════════════════╝
*/
use *;
;
;
;
Getting started
Add flowstate to your Cargo.toml.
[]
= "0.3"
The prelude brings all the essential types into scope.
use *;
Next, derive the Workflow trait. Flowstate can be used without procedural
macros, but it requires a little more boilerplate.
The #[flowstate(result = MyWorkflowResult)] attribute defines the result type.
This is the type that is returned on completion of the workflow. If your
workflow has multiple terminal states, this should be an enum representing each
of those terminal states.
The #[state] attribute lets Typestate know which field stores your state.
You can also define one or more context field, such as ctx in the example
above. These will be automatically propogated each time your workflow
transitions to a new state.
Next, define your states.
;
Implementing MyWorkflowState allows you to define the transition logic for
your state. This trait is generated by #[derive(Workflow)]. Note that
MyWorkflowState is implemented on MyWorkflow<MyState>, not on MyState.
This allows you to access context fields.
In the above example, we return self.transition(MyNextState). This is a
helper function generated by #[derive(Workflow)]. You can manually instantiate
the Transition<MyWorkflowResult> type, but the transition function removes
some of the boilerplate.
You can also return self.finish(result) to terminate the workflow with a
result.
On occasion, you may need move out of the previous state, or access the context
when constructing the next state, or result. In such cases, you may encounter
borrow checker errors. To avoid these, you can use self.transition_with(|state| ...)
or self.finish_with(|workflow| ...).
Finally, you can construct and run your workflow.
let workflow = new;
let result = workflow.run;
Concepts
States
A state is any type that implements [State]. The #[derive(State)] macro
implements it automatically.
;
;
;
Workflows
A workflow is a struct generic over a State type parameter. Workflows must
implement the Workflow trait. The #[derive(Workflow)] macro implements this
automatically, and generates some other utility methods and traits.
One field must be annotated with #[state]; all other fields are context shared
across every state. The #[derive(Workflow)] macro also requires a
#[flowstate(result = T)] attribute, specifying the type the workflow produces
when it terminates.
The macro generates:
- A
new(state, ...context_fields)constructor. - An implementation of [
Workflow]. - A
MyWorkflow::transition(next_state)method, which moves to the next state, carrying context through. - A
MyWorkflow::transition_with(map_fn)method, which transitions by mapping the current state to the next. - A
{WorkflowName}Statetrait (e.g.MyWorkflowState) that should be implemented for each state.
Transitions
[Transition<R>] is an alias for ControlFlow<R, Box<dyn WorkflowState<R>>>.
Each state's next method returns one of:
self.transition(next_state)continues to another state.self.transition_with(|state| next_state)continues to another state by mapping the previous state to the new state.self.finish(result)terminates with a result value.self.finish_with(|workflow| result)terminates by mapping the whole workflow to a result.
These map to ControlFlow::Continue for the transition and transition_with
methods, and ControlFlow::Break for the finish and finish_with methods.