Finite state machines in rust; bendns fork to add types.
Implement a DSL compiler for defining state machines based on `rust-fsm` and add examples.
| -rw-r--r-- | rust_fsm/Cargo.toml | 3 | ||||
| -rw-r--r-- | rust_fsm/examples/circuit_breaker_dsl.rs | 59 | ||||
| -rw-r--r-- | rust_fsm/examples/simple.rs | 23 | ||||
| -rw-r--r-- | rust_fsm/src/lib.rs | 2 | ||||
| -rw-r--r-- | rust_fsm_dsl/Cargo.toml | 4 | ||||
| -rw-r--r-- | rust_fsm_dsl/src/lib.rs | 177 |
6 files changed, 263 insertions, 5 deletions
diff --git a/rust_fsm/Cargo.toml b/rust_fsm/Cargo.toml index 09d6a83..486cf51 100644 --- a/rust_fsm/Cargo.toml +++ b/rust_fsm/Cargo.toml @@ -11,4 +11,5 @@ version = "0.1.0" authors = ["Yevhenii Babichenko"] edition = "2018" -[dependencies] +[dev-dependencies] +rust-fsm-dsl = { path = "../rust_fsm_dsl" } diff --git a/rust_fsm/examples/circuit_breaker_dsl.rs b/rust_fsm/examples/circuit_breaker_dsl.rs new file mode 100644 index 0000000..6a22aab --- /dev/null +++ b/rust_fsm/examples/circuit_breaker_dsl.rs @@ -0,0 +1,59 @@ +/// A dummy implementation of the Circuit Breaker pattern to demonstrate +/// capabilities of its library DSL for defining finite state machines. +/// https://martinfowler.com/bliki/CircuitBreaker.html +#[macro_use] +extern crate rust_fsm_dsl; + +use rust_fsm::*; +use std::sync::{Arc, Mutex}; + +state_machine! { + CircuitBreaker(Closed) + + Closed(Unsuccessful) => Open [SetupTimer], + Open(TimerTriggered) => HalfOpen, + HalfOpen(Successful) => Closed, + HalfOpen(Unsuccessful) => Open [SetupTimer], +} + +fn main() { + let machine: StateMachineWrapper<CircuitBreaker> = StateMachineWrapper::new(); + + // Unsuccessful request + let machine = Arc::new(Mutex::new(machine)); + { + let mut lock = machine.lock().unwrap(); + let res = lock.consume_anyway(&CircuitBreakerInput::Unsuccessful); + assert_eq!(res, Some(CircuitBreakerOutput::SetupTimer)); + assert_eq!(lock.state(), &CircuitBreakerState::Open); + } + + // Set up a timer + let machine_wait = machine.clone(); + std::thread::spawn(move || { + std::thread::sleep_ms(5000); + let mut lock = machine_wait.lock().unwrap(); + let res = lock.consume_anyway(&CircuitBreakerInput::TimerTriggered); + assert_eq!(res, None); + assert_eq!(lock.state(), &CircuitBreakerState::HalfOpen); + }); + + // Try to pass a request when the circuit breaker is still open + let machine_try = machine.clone(); + std::thread::spawn(move || { + std::thread::sleep_ms(1000); + let mut lock = machine_try.lock().unwrap(); + let res = lock.consume_anyway(&CircuitBreakerInput::Successful); + assert_eq!(res, None); + assert_eq!(lock.state(), &CircuitBreakerState::Open); + }); + + // Test if the circit breaker was actually closed + std::thread::sleep_ms(7000); + { + let mut lock = machine.lock().unwrap(); + let res = lock.consume_anyway(&CircuitBreakerInput::Successful); + assert_eq!(res, None); + assert_eq!(lock.state(), &CircuitBreakerState::Closed); + } +} diff --git a/rust_fsm/examples/simple.rs b/rust_fsm/examples/simple.rs new file mode 100644 index 0000000..46ef7be --- /dev/null +++ b/rust_fsm/examples/simple.rs @@ -0,0 +1,23 @@ +#[macro_use] +extern crate rust_fsm_dsl; + +use rust_fsm::*; + +state_machine! { + Door(Open) + + Open(Key) => Closed, + Closed(Key) => Open, + Open(Break) => Broken, + Closed(Break) => Broken, +} + +fn main() { + let mut machine: StateMachineWrapper<Door> = StateMachineWrapper::new(); + machine.consume(&DoorInput::Key).unwrap(); + println!("{:?}", machine.state()); + machine.consume(&DoorInput::Key).unwrap(); + println!("{:?}", machine.state()); + machine.consume(&DoorInput::Break).unwrap(); + println!("{:?}", machine.state()); +} diff --git a/rust_fsm/src/lib.rs b/rust_fsm/src/lib.rs index 259e038..7c5fbd2 100644 --- a/rust_fsm/src/lib.rs +++ b/rust_fsm/src/lib.rs @@ -30,7 +30,7 @@ //! All you need to do to build a state machine is to implement the //! `StateMachine` trait and use it in conjuctions with some of the provided //! wrappers (for now there is only `StateMachineWrapper`). -//! +//! //! You can see an example of the Circuit Breaker state machine in the //! [project repository][repo]. //! diff --git a/rust_fsm_dsl/Cargo.toml b/rust_fsm_dsl/Cargo.toml index e85b73d..6b8a2a5 100644 --- a/rust_fsm_dsl/Cargo.toml +++ b/rust_fsm_dsl/Cargo.toml @@ -16,6 +16,4 @@ proc-macro = true [dependencies] syn = "*" - -[dev-dependencies] -rust-fsm = { path = "../rust_fsm" } +quote = "*" diff --git a/rust_fsm_dsl/src/lib.rs b/rust_fsm_dsl/src/lib.rs index e69de29..479e119 100644 --- a/rust_fsm_dsl/src/lib.rs +++ b/rust_fsm_dsl/src/lib.rs @@ -0,0 +1,177 @@ +#![recursion_limit = "128"] +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use std::collections::HashSet; +use syn::{ + bracketed, parenthesized, + parse::{Parse, ParseStream, Result}, + parse_macro_input, + punctuated::Punctuated, + token::Bracket, + Ident, Token, +}; + +struct TransitionDef { + initial_state: Ident, + input_value: Ident, + final_state: Ident, + output: Option<Ident>, +} + +impl Parse for TransitionDef { + fn parse(input: ParseStream) -> Result<Self> { + let initial_state = input.parse()?; + let input_content; + parenthesized!(input_content in input); + let input_value = input_content.parse()?; + input.parse::<Token![=>]>()?; + let final_state = input.parse()?; + let output = if input.lookahead1().peek(Bracket) { + let output_content; + bracketed!(output_content in input); + Some(output_content.parse()?) + } else { + None + }; + Ok(Self { + initial_state, + input_value, + final_state, + output, + }) + } +} + +struct StateMachineDef { + name: Ident, + initial_state: Ident, + transitions: Punctuated<TransitionDef, Token![,]>, +} + +impl Parse for StateMachineDef { + fn parse(input: ParseStream) -> Result<Self> { + let name = input.parse()?; + let initial_state_content; + parenthesized!(initial_state_content in input); + let initial_state = initial_state_content.parse()?; + let transitions = input.parse_terminated(TransitionDef::parse)?; + Ok(Self { + name, + initial_state, + transitions, + }) + } +} + +#[proc_macro] +pub fn state_machine(tokens: TokenStream) -> TokenStream { + let input = parse_macro_input!(tokens as StateMachineDef); + + let struct_name = input.name; + + let mut states = HashSet::new(); + let mut inputs = HashSet::new(); + let mut outputs = HashSet::new(); + + states.insert(&input.initial_state); + + for transition in input.transitions.iter() { + states.insert(&transition.initial_state); + states.insert(&transition.final_state); + inputs.insert(&transition.input_value); + if let Some(ref output) = transition.output { + outputs.insert(output); + } + } + + 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 input.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) + } + }); + } + + 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! { + #[derive(Debug, PartialEq)] + enum #outputs_type_name { + #(#outputs),* + } + }; + + let outputs_type = quote! { #outputs_type_name }; + + let mut output_cases = vec![]; + for transition in input.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 output_impl = quote! { + match (state, input) { + #(#output_cases)* + _ => None, + } + }; + + (outputs_repr, outputs_type, output_impl) + } else { + (quote! {}, quote! { () }, quote! {None}) + }; + + let output = quote! { + struct #struct_name; + + #[derive(Debug, PartialEq)] + enum #states_enum_name { + #(#states),* + } + + #[derive(Debug, PartialEq)] + enum #inputs_enum_name { + #(#inputs),* + } + + #outputs_repr + + impl rust_fsm::StateMachine 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; + + 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 + } + } + }; + + output.into() +} |