#[derive(Debug, Clone, thiserror::Error)]
pub enum StateMachineError {
#[error("Invalid state: {0}")]
InvalidState(String),
#[error("Invalid transition: {0}")]
InvalidTransition(String),
#[error("Transition '{transition}' not allowed from state '{from}'")]
TransitionNotAllowed {
transition: String,
from: String,
},
#[error("Role '{role}' not authorized for transition '{transition}'")]
RoleNotAuthorized {
role: String,
transition: String,
},
#[error("Guard condition failed for transition '{0}'")]
GuardFailed(String),
#[error("Cannot transition from final state '{0}'")]
FinalStateReached(String),
}
pub trait TransitionMeta<S>: Copy + std::fmt::Display {
fn target_state(&self) -> S;
fn all() -> Vec<Self>;
fn allowed_roles(&self) -> &'static [&'static str];
}
pub trait StateMachineBehavior: Sized {
type State: Copy + PartialEq + Default + std::fmt::Display;
type Transition: TransitionMeta<Self::State>;
fn current_state(&self) -> Self::State;
fn set_current_state(&mut self, state: Self::State);
fn can_transition(&self, transition: Self::Transition) -> bool;
fn new() -> Self where Self: Default { Self::default() }
fn from_state(state: Self::State) -> Self where Self: Default {
let mut sm = Self::default();
sm.set_current_state(state);
sm
}
fn can_transition_with_role(&self, transition: Self::Transition, role: &str) -> bool {
if !self.can_transition(transition) {
return false;
}
let allowed = transition.allowed_roles();
allowed.is_empty() || allowed.iter().any(|r| *r == role || *r == "*")
}
fn transition(&mut self, transition: Self::Transition) -> Result<Self::State, StateMachineError> {
if !self.can_transition(transition) {
return Err(StateMachineError::TransitionNotAllowed {
transition: transition.to_string(),
from: self.current_state().to_string(),
});
}
let next = transition.target_state();
self.set_current_state(next);
Ok(next)
}
fn transition_with_role(
&mut self,
transition: Self::Transition,
role: &str,
) -> Result<Self::State, StateMachineError> {
if !self.can_transition(transition) {
return Err(StateMachineError::TransitionNotAllowed {
transition: transition.to_string(),
from: self.current_state().to_string(),
});
}
if !self.can_transition_with_role(transition, role) {
return Err(StateMachineError::RoleNotAuthorized {
role: role.to_string(),
transition: transition.to_string(),
});
}
let next = transition.target_state();
self.set_current_state(next);
Ok(next)
}
fn available_transitions(&self) -> Vec<Self::Transition> {
Self::Transition::all()
.into_iter()
.filter(|t| self.can_transition(*t))
.collect()
}
fn available_transitions_for_role(&self, role: &str) -> Vec<Self::Transition> {
Self::Transition::all()
.into_iter()
.filter(|t| self.can_transition_with_role(*t, role))
.collect()
}
fn transition_to_state(
&mut self,
target: Self::State,
) -> Result<Self::State, StateMachineError> {
let valid = Self::Transition::all()
.into_iter()
.filter(|t| self.can_transition(*t))
.find(|t| t.target_state() == target);
match valid {
Some(t) => self.transition(t),
None => Err(StateMachineError::TransitionNotAllowed {
transition: target.to_string(),
from: self.current_state().to_string(),
}),
}
}
}