Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple guarded transitions triggered by the same event #72

Merged
merged 16 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ Cargo.lock
*.gv
*.svg
.vscode
.idea/
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Add name to statemachine and make dot output stable and unique ([issue-62](https://github.com/korken89/smlang-rs/pull/62))
- Add derive macros to states and events ([issue-62](https://github.com/korken89/smlang-rs/pull/62))
- Add hooks to `StateMachineContext` for logging events, guards, actions, and state changes
- Add support multiple guarded transitions for a triggering event
- Add support for guard boolean expressions in the state machine declaration

### Fixed

Expand All @@ -24,6 +26,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- [breaking] `state()` now returns a `Result`
- `StateMachine::new` and `StateMachine::new_with_state` are now const functions
- Fixed clippy warnings
- [breaking] Changed guard functions return type from Result<(),_> to Result<bool,_>

## [v0.6.0] - 2022-11-02

Expand Down
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,30 @@ statemachine!{
// ...
}
```

See example `examples/input_state_pattern_match.rs` for a usage example.

#### Guard expressions

Guard expression in square brackets [] allows to define a boolean expressions of multiple guard functions.
For example:
```rust
statemachine! {
transitions: {
*Init + Login(Entry) [valid_entry] / attempt = LoggedIn,
Init + Login(Entry) [!valid_entry && !too_many_attempts] / attempt = Init,
Init + Login(Entry) [!valid_entry && too_many_attempts] / attempt = LoginDenied,
LoggedIn + Logout / reset = Init,
}
}
```
Guard expressions may consist of guard function names, and their combinations with &&, || and ! operations.

#### Multiple guarded transitions for the same state and triggering event
Multiple guarded transitions for the same state and triggering event are supported (see the example above).
It is assumed that only one guard is enabled in such a case to avoid a conflict over which transition should be selected.
However, if there is a conflict and more than one guard is enabled, the first enabled transition,
in the order they appear in the state machine definition, will be selected.

### State machine context

The state machine needs a context to be defined.
Expand Down
29 changes: 17 additions & 12 deletions examples/async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use smlang::{async_trait, statemachine};
statemachine! {
transitions: {
*State1 + Event1 [guard1] / async action1 = State2,
State2 + Event2 [async guard2] / async action2 = State3,
State2 + Event2 [async guard2 && guard3] / async action2 = State3,
State3 + Event3 / action3 = State4(bool),
}
}
Expand All @@ -22,22 +22,21 @@ pub struct Context {

#[async_trait]
impl StateMachineContext for Context {
fn guard1(&mut self) -> Result<(), ()> {
println!("`guard1` called from sync context");
Ok(())
}

async fn action1(&mut self) -> () {
println!("`action1` called from async context");
let mut lock = self.lock.write().await;
*lock = true;
fn guard3(&mut self) -> Result<bool, ()> {
println!("`guard3` called from async context");
Ok(true)
}

async fn guard2(&mut self) -> Result<(), ()> {
async fn guard2(&mut self) -> Result<bool, ()> {
println!("`guard2` called from async context");
let mut lock = self.lock.write().await;
*lock = false;
Ok(())
Ok(true)
}

fn guard1(&mut self) -> Result<bool, ()> {
println!("`guard1` called from sync context");
Ok(true)
}

async fn action2(&mut self) -> () {
Expand All @@ -47,6 +46,12 @@ impl StateMachineContext for Context {
}
}

async fn action1(&mut self) -> () {
println!("`action1` called from async context");
let mut lock = self.lock.write().await;
*lock = true;
}

fn action3(&mut self) -> bool {
println!("`action3` called from sync context, done = `{}`", self.done);
self.done
Expand Down
10 changes: 3 additions & 7 deletions examples/event_with_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,8 @@ statemachine! {
pub struct Context;

impl StateMachineContext for Context {
fn guard(&mut self, event_data: &MyEventData) -> Result<(), ()> {
if event_data == &MyEventData(42) {
Ok(())
} else {
Err(())
}
fn guard(&mut self, event_data: &MyEventData) -> Result<bool, ()> {
Ok(event_data == &MyEventData(42))
}

fn action(&mut self, event_data: MyEventData) {
Expand All @@ -38,7 +34,7 @@ fn main() {
let mut sm = StateMachine::new(Context);
let result = sm.process_event(Events::Event1(MyEventData(1))); // Guard will fail

assert!(matches!(result, Err(Error::GuardFailed(()))));
assert!(matches!(result, Err(Error::TransitionsFailed)));

let result = sm.process_event(Events::Event1(MyEventData(42))); // Guard will pass

Expand Down
4 changes: 2 additions & 2 deletions examples/event_with_mutable_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ statemachine! {
pub struct Context;

impl StateMachineContext for Context {
fn guard(&mut self, event_data: &mut MyEventData) -> Result<(), ()> {
fn guard(&mut self, event_data: &mut MyEventData) -> Result<bool, ()> {
event_data.0 = 55;
Ok(())
Ok(true)
}

fn action(&mut self, event_data: &mut MyEventData) {
Expand Down
20 changes: 6 additions & 14 deletions examples/event_with_reference_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,17 @@ statemachine! {
pub struct Context;

impl StateMachineContext for Context {
fn guard1(&mut self, event_data: &[u8]) -> Result<(), ()> {
fn guard1(&mut self, event_data: &[u8]) -> Result<bool, ()> {
// Only ok if the slice is not empty
if !event_data.is_empty() {
Ok(())
} else {
Err(())
}
Ok(!event_data.is_empty())
}

fn action1(&mut self, event_data: &[u8]) {
println!("Got valid Event Data = {:?}", event_data);
}

fn guard2(&mut self, event_data: &MyReferenceWrapper) -> Result<(), ()> {
if *event_data.0 > 9000 {
Ok(())
} else {
Err(())
}
fn guard2(&mut self, event_data: &MyReferenceWrapper) -> Result<bool, ()> {
Ok(*event_data.0 > 9000)
}

fn action2(&mut self, event_data: MyReferenceWrapper) {
Expand All @@ -51,13 +43,13 @@ fn main() {
let mut sm = StateMachine::new(Context);

let result = sm.process_event(Events::Event1(&[])); // Guard will fail
assert!(matches!(result, Err(Error::GuardFailed(()))));
assert!(matches!(result, Err(Error::TransitionsFailed)));
let result = sm.process_event(Events::Event1(&[1, 2, 3])); // Guard will pass
assert!(matches!(result, Ok(&States::State2)));

let r = 42;
let result = sm.process_event(Events::Event2(MyReferenceWrapper(&r))); // Guard will fail
assert!(matches!(result, Err(Error::GuardFailed(()))));
assert!(matches!(result, Err(Error::TransitionsFailed)));

let r = 9001;
let result = sm.process_event(Events::Event2(MyReferenceWrapper(&r))); // Guard will pass
Expand Down
10 changes: 5 additions & 5 deletions examples/ex3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ statemachine! {
pub struct Context;

impl StateMachineContext for Context {
fn guard(&mut self) -> Result<(), ()> {
fn guard(&mut self) -> Result<bool, ()> {
// Always ok
Ok(())
Ok(true)
}

fn guard_fail(&mut self) -> Result<(), ()> {
fn guard_fail(&mut self) -> Result<bool, ()> {
// Always fail
Err(())
Ok(false)
}

fn action1(&mut self) {
Expand Down Expand Up @@ -53,7 +53,7 @@ fn main() {

// The action will never run as the guard will fail
let r = sm.process_event(Events::Event2);
assert!(matches!(r, Err(Error::GuardFailed(()))));
assert!(matches!(r, Err(Error::TransitionsFailed)));

println!("After action 2");

Expand Down
4 changes: 2 additions & 2 deletions examples/guard_action_syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub struct Context;

impl StateMachineContext for Context {
// Guard1 has access to the data from Event1
fn guard1(&mut self, _event_data: &MyEventData) -> Result<(), ()> {
fn guard1(&mut self, _event_data: &MyEventData) -> Result<bool, ()> {
todo!()
}

Expand All @@ -37,7 +37,7 @@ impl StateMachineContext for Context {
}

// Guard2 has access to the data from State2
fn guard2(&mut self, _state_data: &MyStateData) -> Result<(), ()> {
fn guard2(&mut self, _state_data: &MyStateData) -> Result<bool, ()> {
todo!()
}

Expand Down
8 changes: 4 additions & 4 deletions examples/guard_action_syntax_with_temporary_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ pub struct Context;

impl StateMachineContext for Context {
// Guard1 has access to the data from Event1
fn guard1(&mut self, temp_context: &mut u16, _event_data: &MyEventData) -> Result<(), ()> {
fn guard1(&mut self, temp_context: &mut u16, _event_data: &MyEventData) -> Result<bool, ()> {
*temp_context += 1;

Ok(())
Ok(true)
}

// Action1 has access to the data from Event1, and need to return the state data for State2
Expand All @@ -42,10 +42,10 @@ impl StateMachineContext for Context {
}

// Guard2 has access to the data from State2
fn guard2(&mut self, temp_context: &mut u16, _state_data: &MyStateData) -> Result<(), ()> {
fn guard2(&mut self, temp_context: &mut u16, _state_data: &MyStateData) -> Result<bool, ()> {
*temp_context += 1;

Ok(())
Ok(true)
}

// Action2 has access to the data from State2
Expand Down
8 changes: 3 additions & 5 deletions examples/guard_custom_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,8 @@ statemachine! {
pub struct Context;

impl StateMachineContext for Context {
type GuardError = GuardError;

// Guard1 has access to the data from Event1
fn guard1(&mut self, _event_data: &MyEventData) -> Result<(), GuardError> {
type GuardError = GuardError; // Guard1 has access to the data from Event1
fn guard1(&mut self, _event_data: &MyEventData) -> Result<bool, GuardError> {
dkumsh marked this conversation as resolved.
Show resolved Hide resolved
Err(GuardError::Custom)
}

Expand All @@ -47,7 +45,7 @@ impl StateMachineContext for Context {
}

// Guard2 has access to the data from State2
fn guard2(&mut self, _state_data: &MyStateData) -> Result<(), GuardError> {
fn guard2(&mut self, _state_data: &MyStateData) -> Result<bool, GuardError> {
todo!()
}

Expand Down
24 changes: 12 additions & 12 deletions examples/named_async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,16 @@ pub struct Context {

#[async_trait]
impl AsyncSimpleStateMachineContext for Context {
fn guard1(&mut self) -> Result<(), ()> {
fn guard1(&mut self) -> Result<bool, ()> {
println!("`guard1` called from sync context");
Ok(())
Ok(true)
}

async fn guard2(&mut self) -> Result<bool, ()> {
println!("`guard2` called from async context");
let mut lock = self.lock.write().await;
*lock = false;
Ok(true)
}

async fn action1(&mut self) -> () {
Expand All @@ -34,11 +41,9 @@ impl AsyncSimpleStateMachineContext for Context {
*lock = true;
}

async fn guard2(&mut self) -> Result<(), ()> {
println!("`guard2` called from async context");
let mut lock = self.lock.write().await;
*lock = false;
Ok(())
fn action3(&mut self) -> bool {
println!("`action3` called from sync context, done = `{}`", self.done);
self.done
}

async fn action2(&mut self) -> () {
Expand All @@ -47,11 +52,6 @@ impl AsyncSimpleStateMachineContext for Context {
self.done = true;
}
}

fn action3(&mut self) -> bool {
println!("`action3` called from sync context, done = `{}`", self.done);
self.done
}
}

fn main() {
Expand Down
10 changes: 5 additions & 5 deletions examples/named_ex3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ statemachine! {
pub struct Context;

impl LoopingWithGuardsStateMachineContext for Context {
fn guard(&mut self) -> Result<(), ()> {
fn guard(&mut self) -> Result<bool, ()> {
// Always ok
Ok(())
Ok(true)
}

fn guard_fail(&mut self) -> Result<(), ()> {
fn guard_fail(&mut self) -> Result<bool, ()> {
// Always fail
Err(())
Ok(false)
}

fn action1(&mut self) {
Expand Down Expand Up @@ -54,7 +54,7 @@ fn main() {

// The action will never run as the guard will fail
let r = sm.process_event(LoopingWithGuardsEvents::Event2);
assert!(matches!(r, Err(LoopingWithGuardsError::GuardFailed(()))));
assert!(matches!(r, Err(LoopingWithGuardsError::TransitionsFailed)));

println!("After action 2");

Expand Down
Loading
Loading