Finite state machines in rust; bendns fork to add types.
allow using existing types in the macro (#10)
Yevhenii Babichenko 2024-05-13
parent 89dfabf · commit ff103aa
-rw-r--r--CHANGELOG.md1
-rw-r--r--README.md38
-rw-r--r--rust-fsm-dsl/Cargo.toml2
-rw-r--r--rust-fsm-dsl/src/lib.rs86
-rw-r--r--rust-fsm-dsl/src/parser.rs52
-rw-r--r--rust-fsm/src/lib.rs39
-rw-r--r--rust-fsm/tests/circuit_breaker_dsl_custom_types.rs77
7 files changed, 256 insertions, 39 deletions
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<Impl>` 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<Impl>;
- #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<Self::State> {
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<Ident>);
@@ -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<TransitionDef>,
pub attributes: Vec<Attribute>,
+ pub input_type: Option<Path>,
+ pub state_type: Option<Path>,
+ pub output_type: Option<Path>,
}
impl Parse for StateMachineDef {
fn parse(input: ParseStream) -> Result<Self> {
- 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));
+ }
+}