Skip to content

Commit

Permalink
allow using existing types in the macro (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
eugene-babichenko committed May 12, 2024
1 parent 89dfabf commit ff103aa
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion rust-fsm-dsl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ proc-macro = true

[dependencies]
proc-macro2 = "1"
syn = "1"
syn = "2"
quote = "1"
86 changes: 57 additions & 29 deletions rust-fsm-dsl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
});
}
Expand All @@ -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! {
Expand All @@ -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) {
Expand Down
52 changes: 43 additions & 9 deletions rust-fsm-dsl/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>);

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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()?;
Expand All @@ -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();

Expand All @@ -151,6 +182,9 @@ impl Parse for StateMachineDef {
initial_state,
transitions,
attributes,
input_type,
state_type,
output_type,
})
}
}
39 changes: 39 additions & 0 deletions rust-fsm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 77 additions & 0 deletions rust-fsm/tests/circuit_breaker_dsl_custom_types.rs
Original file line number Diff line number Diff line change
@@ -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));
}
}

0 comments on commit ff103aa

Please sign in to comment.