diff --git a/CHANGELOG.md b/CHANGELOG.md index 6376006..a0bab69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ adheres to [Semantic Versioning][semver]. ## Added * A type alias `StateMachine` for `rust_fsm::StateMachine` is now generated inside the said module. +* Supplying ones own enums for state, input and output in the proc-macro. ## [0.6.2] - 2024-05-11 ### Changed diff --git a/README.md b/README.md index 0f98d1d..0496f25 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,44 @@ state_machine! { The default visibility is private. +#### Custom allphabet types + +You can supply your own types to use as input, output or state. All of them are +optional: you can use only one of them or all of them at once if you want to. +The current limitation is that you have to supply a fully qualified type path. + +```rust +use rust_fsm::*; + +pub enum Input { + Successful, + Unsuccessful, + TimerTriggered, +} + +pub enum State { + Closed, + HalfOpen, + Open, +} + +pub enum Output { + SetupTimer, +} + +state_machine! { + #[state_machine(input(crate::Input), state(crate::State), output(crate::Output))] + circuit_breaker(Closed) + + Closed(Unsuccessful) => Open [SetupTimer], + Open(TimerTriggered) => HalfOpen, + HalfOpen => { + Successful => Closed, + Unsuccessful => Open [SetupTimer] + } +} +``` + ### Without DSL The `state_machine` macro has limited capabilities (for example, a state diff --git a/rust-fsm-dsl/Cargo.toml b/rust-fsm-dsl/Cargo.toml index 1155858..82f3b9f 100644 --- a/rust-fsm-dsl/Cargo.toml +++ b/rust-fsm-dsl/Cargo.toml @@ -17,5 +17,5 @@ proc-macro = true [dependencies] proc-macro2 = "1" -syn = "1" +syn = "2" quote = "1" diff --git a/rust-fsm-dsl/src/lib.rs b/rust-fsm-dsl/src/lib.rs index ce1e5b5..7bed6d9 100644 --- a/rust-fsm-dsl/src/lib.rs +++ b/rust-fsm-dsl/src/lib.rs @@ -78,8 +78,8 @@ pub fn state_machine(tokens: TokenStream) -> TokenStream { let input_value = &transition.input_value; let final_state = &transition.final_state; transition_cases.push(quote! { - (State::#initial_state, Input::#input_value) => { - Some(State::#final_state) + (Self::State::#initial_state, Self::Input::#input_value) => { + Some(Self::State::#final_state) } }); } @@ -90,20 +90,59 @@ pub fn state_machine(tokens: TokenStream) -> TokenStream { 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) + (Self::State::#initial_state, Self::Input::#input_value) => { + Some(Self::Output::#output_value) } }); } } - // 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 { - attrs.clone() + let (input_type, input_impl) = match input.input_type { + Some(t) => (quote!(#t), quote!()), + None => ( + quote!(Input), + quote! { + #attrs + pub enum Input { + #(#inputs),* + } + }, + ), + }; + + let (state_type, state_impl) = match input.state_type { + Some(t) => (quote!(#t), quote!()), + None => ( + quote!(State), + quote! { + #attrs + pub enum State { + #(#states),* + } + }, + ), + }; + + let (output_type, output_impl) = match input.output_type { + Some(t) => (quote!(#t), quote!()), + None => { + // 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 attrs = if outputs.is_empty() { + quote!() + } else { + attrs.clone() + }; + ( + quote!(Output), + quote! { + #attrs + pub enum Output { + #(#outputs),* + } + }, + ) + } }; let output = quote! { @@ -113,26 +152,15 @@ pub fn state_machine(tokens: TokenStream) -> TokenStream { pub type StateMachine = rust_fsm::StateMachine; - #attrs - pub enum Input { - #(#inputs),* - } - - #attrs - pub enum State { - #(#states),* - } - - #output_attrs - pub enum Output { - #(#outputs),* - } + #input_impl + #state_impl + #output_impl impl rust_fsm::StateMachineImpl for Impl { - type Input = Input; - type State = State; - type Output = Output; - const INITIAL_STATE: Self::State = State::#initial_state_name; + type Input = #input_type; + type State = #state_type; + type Output = #output_type; + const INITIAL_STATE: Self::State = Self::State::#initial_state_name; fn transition(state: &Self::State, input: &Self::Input) -> Option { match (state, input) { diff --git a/rust-fsm-dsl/src/parser.rs b/rust-fsm-dsl/src/parser.rs index 8b0d957..7b97a03 100644 --- a/rust-fsm-dsl/src/parser.rs +++ b/rust-fsm-dsl/src/parser.rs @@ -2,14 +2,9 @@ use syn::{ braced, bracketed, parenthesized, parse::{Error, Parse, ParseStream, Result}, token::{Bracket, Paren}, - Attribute, Ident, Token, Visibility, + Attribute, Ident, Path, Token, Visibility, }; -mod kw { - syn::custom_keyword!(derive); - syn::custom_keyword!(repr_c); -} - /// The output of a state transition pub struct Output(Option); @@ -88,7 +83,7 @@ impl Parse for TransitionDef { braced!(entries_content in input); let entries: Vec<_> = entries_content - .parse_terminated::<_, Token![,]>(TransitionEntry::parse)? + .parse_terminated(TransitionEntry::parse, Token![,])? .into_iter() .collect(); if entries.is_empty() { @@ -127,11 +122,47 @@ pub struct StateMachineDef { pub initial_state: Ident, pub transitions: Vec, pub attributes: Vec, + pub input_type: Option, + pub state_type: Option, + pub output_type: Option, } impl Parse for StateMachineDef { fn parse(input: ParseStream) -> Result { - let attributes = Attribute::parse_outer(input)?; + let mut state_machine_attributes = Vec::new(); + let attributes = Attribute::parse_outer(input)? + .into_iter() + .filter_map(|attribute| { + if attribute.path().is_ident("state_machine") { + state_machine_attributes.push(attribute); + None + } else { + Some(attribute) + } + }) + .collect(); + + let mut input_type = None; + let mut state_type = None; + let mut output_type = None; + + for attribute in state_machine_attributes { + attribute.parse_nested_meta(|meta| { + let content; + parenthesized!(content in meta.input); + let p: Path = content.parse()?; + + if meta.path.is_ident("input") { + input_type = Some(p); + } else if meta.path.is_ident("state") { + state_type = Some(p); + } else if meta.path.is_ident("output") { + output_type = Some(p); + } + + Ok(()) + })?; + } let visibility = input.parse()?; let name = input.parse()?; @@ -141,7 +172,7 @@ impl Parse for StateMachineDef { let initial_state = initial_state_content.parse()?; let transitions = input - .parse_terminated::<_, Token![,]>(TransitionDef::parse)? + .parse_terminated(TransitionDef::parse, Token![,])? .into_iter() .collect(); @@ -151,6 +182,9 @@ impl Parse for StateMachineDef { initial_state, transitions, attributes, + input_type, + state_type, + output_type, }) } } diff --git a/rust-fsm/src/lib.rs b/rust-fsm/src/lib.rs index 752a9b4..98e1c57 100644 --- a/rust-fsm/src/lib.rs +++ b/rust-fsm/src/lib.rs @@ -138,6 +138,45 @@ //! //! The default visibility is private. //! +//! ### Custom allphabet types +//! +//! You can supply your own types to use as input, output or state. All of them +//! are optional: you can use only one of them or all of them at once if you +//! want to. The current limitation is that you have to supply a fully qualified +//! type path. +//! +//! ```rust,ignore +//! use rust_fsm::*; +//! +//! pub enum Input { +//! Successful, +//! Unsuccessful, +//! TimerTriggered, +//! } +//! +//! pub enum State { +//! Closed, +//! HalfOpen, +//! Open, +//! } +//! +//! pub enum Output { +//! SetupTimer, +//! } +//! +//! state_machine! { +//! #[state_machine(input(crate::Input), state(crate::State), output(crate::Output))] +//! circuit_breaker(Closed) +//! +//! Closed(Unsuccessful) => Open [SetupTimer], +//! Open(TimerTriggered) => HalfOpen, +//! HalfOpen => { +//! Successful => Closed, +//! Unsuccessful => Open [SetupTimer] +//! } +//! } +//! ``` +//! //! ## Without DSL //! //! The `state_machine` macro has limited capabilities (for example, a state diff --git a/rust-fsm/tests/circuit_breaker_dsl_custom_types.rs b/rust-fsm/tests/circuit_breaker_dsl_custom_types.rs new file mode 100644 index 0000000..ca878b3 --- /dev/null +++ b/rust-fsm/tests/circuit_breaker_dsl_custom_types.rs @@ -0,0 +1,77 @@ +/// 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 +use rust_fsm::*; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +pub enum Input { + Successful, + Unsuccessful, + TimerTriggered, +} + +pub enum State { + Closed, + HalfOpen, + Open, +} + +pub enum Output { + SetupTimer, +} + +state_machine! { + #[state_machine(input(crate::Input), state(crate::State), output(crate::Output))] + circuit_breaker(Closed) + + Closed(Unsuccessful) => Open [SetupTimer], + Open(TimerTriggered) => HalfOpen, + HalfOpen => { + Successful => Closed, + Unsuccessful => Open [SetupTimer] + } +} + +#[test] +fn circit_breaker_dsl() { + 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(&Input::Unsuccessful).unwrap(); + assert!(matches!(res, Some(Output::SetupTimer))); + assert!(matches!(lock.state(), &State::Open)); + } + + // Set up a timer + let machine_wait = machine.clone(); + std::thread::spawn(move || { + std::thread::sleep(Duration::new(5, 0)); + let mut lock = machine_wait.lock().unwrap(); + let res = lock.consume(&Input::TimerTriggered).unwrap(); + assert!(matches!(res, None)); + assert!(matches!(lock.state(), &State::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(Duration::new(1, 0)); + let mut lock = machine_try.lock().unwrap(); + let res = lock.consume(&Input::Successful); + assert!(matches!(res, Err(TransitionImpossibleError))); + assert!(matches!(lock.state(), &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(&Input::Successful).unwrap(); + assert!(matches!(res, None)); + assert!(matches!(lock.state(), &State::Closed)); + } +}