Finite state machines in rust; bendns fork to add types.
| -rw-r--r-- | .github/workflows/publish.yml | 0 | ||||
| -rw-r--r-- | CHANGELOG.md | 12 | ||||
| -rw-r--r-- | README.md | 77 | ||||
| -rw-r--r-- | rust-fsm-dsl/src/lib.rs | 120 | ||||
| -rw-r--r-- | rust-fsm/src/lib.rs | 35 | ||||
| -rw-r--r-- | rust-fsm/tests/circuit_breaker_dsl.rs | 24 | ||||
| -rw-r--r-- | rust-fsm/tests/simple.rs | 10 |
7 files changed, 134 insertions, 144 deletions
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index e69de29..0000000 --- a/.github/workflows/publish.yml +++ /dev/null diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c59496..676cb73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ The format is based on [Keep a Changelog][keepachangelog], and this project adheres to [Semantic Versioning][semver]. ## [Unreleased] +### Changed +* All types generated by the `state_machine` macro are now confined in a module. + The name of the module is passed to the macro as the name of the state + machine. All generated types now have uniforms names: `Impl`, `Input`, + `State`, `Output`. +* If no outputs is specified in the `state_machine` macro, an empty `Output` + enum is generated instead of using `()` for the sake of uniformity. No + attributes (e.g. `derive` and `repr`) are applied on an empty `Output`, + because many of them are simply not designed to work this way. +## Added +* A type alias `StateMachine` for `rust_fsm::StateMachine<Impl>` is now + generated inside the said module. ## [0.6.2] - 2024-05-11 ### Changed @@ -7,8 +7,8 @@ The `rust-fsm` crate provides a simple and universal framework for building state machines in Rust with minimum effort. The essential part of this crate is the `StateMachineImpl` trait. This trait -allows a developer to provide a strict state machine definition, e.g. -specify its: +allows a developer to provide a strict state machine definition, e.g. specify +its: * An input alphabet - a set of entities that the state machine takes as inputs and performs state transitions based on them. @@ -30,7 +30,7 @@ of state machines: * A Moore machine by providing an output function that do not depend on the provided inputs. -## Features +## Usage in `no_std` environments This library has the feature named `std` which is enabled by default. You may want to import this library as @@ -46,7 +46,7 @@ also enabled by default. Initially this library was designed to build an easy to use DSL for defining state machines on top of it. Using the DSL will require to connect an additional crate `rust-fsm-dsl` (this is due to limitation of the procedural -macros system). +macros system). ### Using the DSL for defining state machines @@ -57,24 +57,21 @@ use rust_fsm::*; state_machine! { derive(Debug) - repr_c(true) - CircuitBreaker(Closed) + circuit_breaker(Closed) Closed(Unsuccessful) => Open [SetupTimer], Open(TimerTriggered) => HalfOpen, HalfOpen => { Successful => Closed, - Unsuccessful => Open [SetupTimer], + Unsuccessful => Open [SetupTimer] } } ``` This code sample: -* Defines a state machine called `CircuitBreaker`; +* Defines a state machine called `circuit_breaker`; * Derives the `Debug` trait for it (the `derive` section is optional); -* Adds repr(C) support to generated code for better FFI compatability - (the `repr_c` section is optional and defaults to false); * Sets the initial state of this state machine to `Closed`; * Defines state transitions. For example: on receiving the `Successful` input when in the `HalfOpen` state, the machine must move to the `Closed` @@ -84,56 +81,40 @@ This code sample: This state machine can be used as follows: -```rust +```rust,ignore // Initialize the state machine. The state is `Closed` now. -let mut machine: StateMachine<CircuitBreaker> = StateMachine::new(); +let mut machine = circuit_breaker::StateMachine::new(); // Consume the `Successful` input. No state transition is performed. -let _ = machine.consume(&CircuitBreakerInput::Successful); +let _ = machine.consume(&circuit_breaker::Input::Successful); // Consume the `Unsuccesful` input. The machine is moved to the `Open` // state. The output is `SetupTimer`. -let output = machine.consume(&CircuitBreakerInput::Unsuccessful).unwrap(); +let output = machine.consume(&circuit_breaker::Input::Unsuccessful).unwrap(); // Check the output -if let Some(CircuitBreakerOutput::SetupTimer) = output { +if let Some(circuit_breaker::Output::SetupTimer) = output { // Set up the timer... } // Check the state -if let CircuitBreakerState::Open = machine.state() { +if let circuit_breaker::State::Open = machine.state() { // Do something... } ``` -As you can see, the following entities are generated: - -* An empty structure `CircuitBreaker` that implements the `StateMachineImpl` - trait. -* Enums `CircuitBreakerState`, `CircuitBreakerInput` and - `CircuitBreakerOutput` that represent the state, the input alphabet and - the output alphabet respectively. - -Note that if there is no outputs in the specification, the output alphabet -is set to `()`. The set of states and the input alphabet must be non-empty -sets. - -#### Visibility +The following entities are generated: -You can specify visibility like this: - -```rust -state_machine! { - pub CircuitBreaker(Closed) +* An empty structure `circuit_breaker::Impl` that implements the + `StateMachineImpl` trait. +* Enums `circuit_breaker::State`, `circuit_breaker::Input` and + `circuit_breaker::Output` that represent the state, the input alphabet and the + output alphabet respectively. +* Type alias `circuit_breaker::StateMachine` that expands to +`StateMachine<circuit_breaker::Impl>`. - Closed(Unsuccessful) => Open [SetupTimer], - Open(TimerTriggered) => HalfOpen, - HalfOpen => { - Successful => Closed, - Unsuccessful => Open [SetupTimer], - } -} -``` +Note that if there is no outputs in the specification, the output alphabet is an +empty enum and due to technical limitations of many Rust attributes, no +attributes (e.g. `derive`, `repr`) are applied to it. -Note that the default visibility is private just like for any structure. The -specified visibility will apply to all structures and enums generated by the -macro. +Within the `state_machine` macro you must define at least one state +transition. ### Without DSL @@ -148,8 +129,4 @@ wrappers (for now there is only `StateMachine`). You can see an example of the Circuit Breaker state machine in the [project repository][repo]. -[repo]: https://github.com/eugene-babichenko/rust-fsm/blob/master/rust-fsm/tests/circuit_breaker.rs -[docs-badge]: https://docs.rs/rust-fsm/badge.svg -[docs-link]: https://docs.rs/rust-fsm -[crate-badge]: https://img.shields.io/crates/v/rust-fsm.svg -[crate-link]: https://crates.io/crates/rust-fsm +[repo]: https://github.com/eugene-babichenko/rust-fsm/blob/master/tests/circuit_breaker.rs diff --git a/rust-fsm-dsl/src/lib.rs b/rust-fsm-dsl/src/lib.rs index ba4ef46..3d82364 100644 --- a/rust-fsm-dsl/src/lib.rs +++ b/rust-fsm-dsl/src/lib.rs @@ -45,7 +45,7 @@ pub fn state_machine(tokens: TokenStream) -> TokenStream { return output.into(); } - let struct_name = input.name; + let fsm_name = input.name; let visibility = input.visibility; let transitions: Vec<_> = input @@ -76,94 +76,88 @@ pub fn state_machine(tokens: TokenStream) -> TokenStream { } } - let states_enum_name = Ident::new(&format!("{}State", struct_name), struct_name.span()); let initial_state_name = &input.initial_state; - let inputs_enum_name = Ident::new(&format!("{}Input", struct_name), struct_name.span()); - let mut transition_cases = vec![]; for transition in transitions.iter() { let initial_state = &transition.initial_state; let input_value = &transition.input_value; let final_state = &transition.final_state; transition_cases.push(quote! { - (#states_enum_name::#initial_state, #inputs_enum_name::#input_value) => { - Some(#states_enum_name::#final_state) + (State::#initial_state, Input::#input_value) => { + Some(State::#final_state) } }); } - let (outputs_repr, outputs_type, output_impl) = if !outputs.is_empty() { - let outputs_type_name = Ident::new(&format!("{}Output", struct_name), struct_name.span()); - let outputs_repr = quote! { - #derives - #type_repr - #visibility enum #outputs_type_name { - #(#outputs),* - } - }; - - let outputs_type = quote! { #outputs_type_name }; - - let mut output_cases = vec![]; - for transition in transitions.iter() { - if let Some(output_value) = &transition.output { - let initial_state = &transition.initial_state; - let input_value = &transition.input_value; - output_cases.push(quote! { - (#states_enum_name::#initial_state, #inputs_enum_name::#input_value) => { - Some(#outputs_type_name::#output_value) - } - }); - } + let mut output_cases = vec![]; + for transition in transitions.iter() { + if let Some(output_value) = &transition.output { + let initial_state = &transition.initial_state; + let input_value = &transition.input_value; + output_cases.push(quote! { + (State::#initial_state, Input::#input_value) => { + Some(Output::#output_value) + } + }); } + } - let output_impl = quote! { - match (state, input) { - #(#output_cases)* - _ => None, - } - }; + let attrs = quote! { + #derives + #type_repr + }; - (outputs_repr, outputs_type, output_impl) + // Many attrs and derives may work incorrectly (or simply not work) for + // empty enums, so we just skip them altogether if the output alphabet is + // empty. + let output_attrs = if outputs.is_empty() { + quote!() } else { - (quote! {}, quote! { () }, quote! {None}) + attrs.clone() }; let output = quote! { - #derives - #type_repr - #visibility struct #struct_name; + #visibility mod #fsm_name { + #attrs + pub struct Impl; - #derives - #type_repr - #visibility enum #states_enum_name { - #(#states),* - } + pub type StateMachine = rust_fsm::StateMachine<Impl>; - #derives - #type_repr - #visibility enum #inputs_enum_name { - #(#inputs),* - } + #attrs + pub enum Input { + #(#inputs),* + } - #outputs_repr + #attrs + pub enum State { + #(#states),* + } + + #output_attrs + pub enum Output { + #(#outputs),* + } - impl rust_fsm::StateMachineImpl for #struct_name { - type Input = #inputs_enum_name; - type State = #states_enum_name; - type Output = #outputs_type; - const INITIAL_STATE: Self::State = #states_enum_name::#initial_state_name; + impl rust_fsm::StateMachineImpl for Impl { + type Input = Input; + type State = State; + type Output = Output; + const INITIAL_STATE: Self::State = State::#initial_state_name; - fn transition(state: &Self::State, input: &Self::Input) -> Option<Self::State> { - match (state, input) { - #(#transition_cases)* - _ => None, + fn transition(state: &Self::State, input: &Self::Input) -> Option<Self::State> { + match (state, input) { + #(#transition_cases)* + _ => None, + } } - } - fn output(state: &Self::State, input: &Self::Input) -> Option<Self::Output> { - #output_impl + fn output(state: &Self::State, input: &Self::Input) -> Option<Self::Output> { + match (state, input) { + #(#output_cases)* + _ => None, + } + } } } }; diff --git a/rust-fsm/src/lib.rs b/rust-fsm/src/lib.rs index 34b59cf..445fa2b 100644 --- a/rust-fsm/src/lib.rs +++ b/rust-fsm/src/lib.rs @@ -49,12 +49,12 @@ //! //! The DSL is parsed by the `state_machine` macro. Here is a little example. //! -//! ```rust,ignore +//! ```rust //! use rust_fsm::*; //! //! state_machine! { //! derive(Debug) -//! CircuitBreaker(Closed) +//! circuit_breaker(Closed) //! //! Closed(Unsuccessful) => Open [SetupTimer], //! Open(TimerTriggered) => HalfOpen, @@ -67,7 +67,7 @@ //! //! This code sample: //! -//! * Defines a state machine called `CircuitBreaker`; +//! * Defines a state machine called `circuit_breaker`; //! * Derives the `Debug` trait for it (the `derive` section is optional); //! * Sets the initial state of this state machine to `Closed`; //! * Defines state transitions. For example: on receiving the `Successful` @@ -80,33 +80,38 @@ //! //! ```rust,ignore //! // Initialize the state machine. The state is `Closed` now. -//! let mut machine: StateMachine<CircuitBreaker> = StateMachine::new(); +//! let mut machine = circuit_breaker::StateMachine::new(); //! // Consume the `Successful` input. No state transition is performed. -//! let _ = machine.consume(&CircuitBreakerInput::Successful); +//! let _ = machine.consume(&circuit_breaker::Input::Successful); //! // Consume the `Unsuccesful` input. The machine is moved to the `Open` //! // state. The output is `SetupTimer`. -//! let output = machine.consume(&CircuitBreakerInput::Unsuccessful).unwrap(); +//! let output = machine.consume(&circuit_breaker::Input::Unsuccessful).unwrap(); //! // Check the output -//! if let Some(CircuitBreakerOutput::SetupTimer) = output { +//! if let Some(circuit_breaker::Output::SetupTimer) = output { //! // Set up the timer... //! } //! // Check the state -//! if let CircuitBreakerState::Open = machine.state() { +//! if let circuit_breaker::State::Open = machine.state() { //! // Do something... //! } //! ``` //! -//! As you can see, the following entities are generated: +//! The following entities are generated: //! -//! * An empty structure `CircuitBreaker` that implements the `StateMachineImpl` -//! trait. -//! * Enums `CircuitBreakerState`, `CircuitBreakerInput` and -//! `CircuitBreakerOutput` that represent the state, the input alphabet and +//! * An empty structure `circuit_breaker::Impl` that implements the +//! `StateMachineImpl` trait. +//! * Enums `circuit_breaker::State`, `circuit_breaker::Input` and +//! `circuit_breaker::Output` that represent the state, the input alphabet and //! the output alphabet respectively. +//! * Type alias `circuit_breaker::StateMachine` that expands to +//! `StateMachine<circuit_breaker::Impl>`. //! //! Note that if there is no outputs in the specification, the output alphabet -//! is set to `()`. The set of states and the input alphabet must be non-empty -//! sets. +//! is an empty enum and due to technical limitations of many Rust attributes, +//! no attributes (e.g. `derive`, `repr`) are applied to it. +//! +//! Within the `state_machine` macro you must define at least one state +//! transition. //! //! ## Without DSL //! diff --git a/rust-fsm/tests/circuit_breaker_dsl.rs b/rust-fsm/tests/circuit_breaker_dsl.rs index 2293786..7645fdb 100644 --- a/rust-fsm/tests/circuit_breaker_dsl.rs +++ b/rust-fsm/tests/circuit_breaker_dsl.rs @@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; state_machine! { - CircuitBreaker(Closed) + circuit_breaker(Closed) Closed(Unsuccessful) => Open [SetupTimer], Open(TimerTriggered) => HalfOpen, @@ -18,15 +18,15 @@ state_machine! { #[test] fn circit_breaker_dsl() { - let machine: StateMachine<CircuitBreaker> = StateMachine::new(); + let machine = circuit_breaker::StateMachine::new(); // Unsuccessful request let machine = Arc::new(Mutex::new(machine)); { let mut lock = machine.lock().unwrap(); - let res = lock.consume(&CircuitBreakerInput::Unsuccessful).unwrap(); - assert!(matches!(res, Some(CircuitBreakerOutput::SetupTimer))); - assert!(matches!(lock.state(), &CircuitBreakerState::Open)); + let res = lock.consume(&circuit_breaker::Input::Unsuccessful).unwrap(); + assert!(matches!(res, Some(circuit_breaker::Output::SetupTimer))); + assert!(matches!(lock.state(), &circuit_breaker::State::Open)); } // Set up a timer @@ -34,9 +34,11 @@ fn circit_breaker_dsl() { std::thread::spawn(move || { std::thread::sleep(Duration::new(5, 0)); let mut lock = machine_wait.lock().unwrap(); - let res = lock.consume(&CircuitBreakerInput::TimerTriggered).unwrap(); + let res = lock + .consume(&circuit_breaker::Input::TimerTriggered) + .unwrap(); assert!(matches!(res, None)); - assert!(matches!(lock.state(), &CircuitBreakerState::HalfOpen)); + assert!(matches!(lock.state(), &circuit_breaker::State::HalfOpen)); }); // Try to pass a request when the circuit breaker is still open @@ -44,17 +46,17 @@ fn circit_breaker_dsl() { std::thread::spawn(move || { std::thread::sleep(Duration::new(1, 0)); let mut lock = machine_try.lock().unwrap(); - let res = lock.consume(&CircuitBreakerInput::Successful); + let res = lock.consume(&circuit_breaker::Input::Successful); assert!(matches!(res, Err(TransitionImpossibleError))); - assert!(matches!(lock.state(), &CircuitBreakerState::Open)); + assert!(matches!(lock.state(), &circuit_breaker::State::Open)); }); // Test if the circit breaker was actually closed std::thread::sleep(Duration::new(7, 0)); { let mut lock = machine.lock().unwrap(); - let res = lock.consume(&CircuitBreakerInput::Successful).unwrap(); + let res = lock.consume(&circuit_breaker::Input::Successful).unwrap(); assert!(matches!(res, None)); - assert!(matches!(lock.state(), &CircuitBreakerState::Closed)); + assert!(matches!(lock.state(), &circuit_breaker::State::Closed)); } } diff --git a/rust-fsm/tests/simple.rs b/rust-fsm/tests/simple.rs index d889dbb..c16dab8 100644 --- a/rust-fsm/tests/simple.rs +++ b/rust-fsm/tests/simple.rs @@ -3,7 +3,7 @@ use rust_fsm::*; state_machine! { derive(Debug) repr_c(true) - Door(Open) + door(Open) Open(Key) => Closed, Closed(Key) => Open, @@ -13,11 +13,11 @@ state_machine! { #[test] fn simple() { - let mut machine: StateMachine<Door> = StateMachine::new(); - machine.consume(&DoorInput::Key).unwrap(); + let mut machine = door::StateMachine::new(); + machine.consume(&door::Input::Key).unwrap(); println!("{:?}", machine.state()); - machine.consume(&DoorInput::Key).unwrap(); + machine.consume(&door::Input::Key).unwrap(); println!("{:?}", machine.state()); - machine.consume(&DoorInput::Break).unwrap(); + machine.consume(&door::Input::Break).unwrap(); println!("{:?}", machine.state()); } |