use crate::valtron::{ExecutionAction, NoAction, TaskIterator, TaskStatus};
use std::time::Duration;
pub enum StateTransition<S, O, E, A = NoAction>
where
A: ExecutionAction,
{
Continue(S),
Yield(O, S),
Complete(O),
Error(E),
Delay(Duration, S),
Spawn(A, S),
}
pub trait StateMachine {
type State: Clone;
type Output;
type Error;
type Action: ExecutionAction;
fn transition(
&mut self,
state: Self::State,
) -> StateTransition<Self::State, Self::Output, Self::Error, Self::Action>;
fn initial_state(&self) -> Self::State;
}
pub struct StateMachineTask<M: StateMachine> {
machine: M,
current_state: Option<M::State>,
}
impl<M: StateMachine> StateMachineTask<M> {
pub fn new(machine: M) -> Self {
let initial = machine.initial_state();
Self {
machine,
current_state: Some(initial),
}
}
}
impl<M> TaskIterator for StateMachineTask<M>
where
M: StateMachine,
{
type Ready = M::Output;
type Pending = M::State;
type Spawner = M::Action;
fn next_status(&mut self) -> Option<TaskStatus<Self::Ready, Self::Pending, Self::Spawner>> {
let state = self.current_state.take()?;
match self.machine.transition(state) {
StateTransition::Continue(next) => {
self.current_state = Some(next.clone());
Some(TaskStatus::Pending(next))
}
StateTransition::Yield(output, next) => {
self.current_state = Some(next);
Some(TaskStatus::Ready(output))
}
StateTransition::Complete(output) => {
Some(TaskStatus::Ready(output))
}
StateTransition::Error(_err) => {
tracing::error!(
"State machine error: {:?}",
std::any::type_name::<M::Error>()
);
None
}
StateTransition::Delay(duration, next) => {
self.current_state = Some(next);
Some(TaskStatus::Delayed(duration))
}
StateTransition::Spawn(action, next) => {
self.current_state = Some(next);
Some(TaskStatus::Spawn(action))
}
}
}
}
#[allow(dead_code)]
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex as StdMutex};
#[derive(Clone, Debug, PartialEq)]
enum CounterState {
Counting(i32),
Done,
}
struct CounterMachine {
max: i32,
}
impl StateMachine for CounterMachine {
type State = CounterState;
type Output = i32;
type Error = ();
type Action = NoAction;
fn transition(
&mut self,
state: Self::State,
) -> StateTransition<Self::State, Self::Output, Self::Error, Self::Action> {
match state {
CounterState::Counting(n) if n < self.max => {
StateTransition::Yield(n, CounterState::Counting(n + 1))
}
CounterState::Counting(n) => StateTransition::Complete(n),
CounterState::Done => StateTransition::Complete(-1),
}
}
fn initial_state(&self) -> Self::State {
CounterState::Counting(0)
}
}
#[test]
fn test_state_machine_task_yields_values() {
let machine = CounterMachine { max: 3 };
let mut task = StateMachineTask::new(machine);
match task.next_status() {
Some(TaskStatus::Ready(0)) => {}
other => panic!("Expected Ready(0), got {:?}", other),
}
match task.next_status() {
Some(TaskStatus::Ready(1)) => {}
other => panic!("Expected Ready(1), got {:?}", other),
}
match task.next_status() {
Some(TaskStatus::Ready(2)) => {}
other => panic!("Expected Ready(2), got {:?}", other),
}
match task.next_status() {
Some(TaskStatus::Ready(3)) => {}
other => panic!("Expected Ready(3), got {:?}", other),
}
assert!(task.next_status().is_none());
}
#[test]
fn test_state_transition_continue() {
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq)]
enum State {
Init,
Processing,
Done,
}
struct ContinueMachine {
steps: Arc<StdMutex<Vec<State>>>,
}
impl StateMachine for ContinueMachine {
type State = State;
type Output = String;
type Error = ();
type Action = NoAction;
fn transition(
&mut self,
state: Self::State,
) -> StateTransition<Self::State, Self::Output, Self::Error, Self::Action> {
self.steps.lock().unwrap().push(state.clone());
match state {
State::Init => StateTransition::Continue(State::Processing),
State::Processing => StateTransition::Complete("done".to_string()),
State::Done => StateTransition::Complete("already done".to_string()),
}
}
fn initial_state(&self) -> Self::State {
State::Init
}
}
let steps = Arc::new(StdMutex::new(Vec::new()));
let machine = ContinueMachine {
steps: steps.clone(),
};
let mut task = StateMachineTask::new(machine);
match task.next_status() {
Some(TaskStatus::Pending(State::Processing)) => {}
other => panic!("Expected Pending(done), got {other:?}"),
}
match task.next_status() {
Some(TaskStatus::Ready(ref s)) if s == "done" => {}
other => panic!("Expected Ready(done), got {other:?}"),
}
let visited = steps.lock().unwrap();
assert_eq!(*visited, vec![State::Init, State::Processing]);
}
#[test]
fn test_state_transition_delay() {
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq)]
enum State {
Start,
Delayed,
End,
}
struct DelayMachine;
impl StateMachine for DelayMachine {
type State = State;
type Output = String;
type Error = ();
type Action = NoAction;
fn transition(
&mut self,
state: Self::State,
) -> StateTransition<Self::State, Self::Output, Self::Error, Self::Action> {
match state {
State::Start => {
StateTransition::Delay(Duration::from_millis(100), State::Delayed)
}
State::Delayed => StateTransition::Complete("after delay".to_string()),
State::End => StateTransition::Complete("done".to_string()),
}
}
fn initial_state(&self) -> Self::State {
State::Start
}
}
let machine = DelayMachine;
let mut task = StateMachineTask::new(machine);
match task.next_status() {
Some(TaskStatus::Delayed(d)) if d == Duration::from_millis(100) => {}
other => panic!("Expected Delayed(100ms), got {:?}", other),
}
match task.next_status() {
Some(TaskStatus::Ready(ref s)) if s == "after delay" => {}
other => panic!("Expected Ready(after delay), got {:?}", other),
}
}
#[test]
fn test_state_transition_error_stops_task() {
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq)]
enum State {
Start,
Error,
}
struct ErrorMachine;
impl StateMachine for ErrorMachine {
type State = State;
type Output = String;
type Error = String;
type Action = NoAction;
fn transition(
&mut self,
state: Self::State,
) -> StateTransition<Self::State, Self::Output, Self::Error, Self::Action> {
match state {
State::Start => StateTransition::Error("something went wrong".to_string()),
State::Error => StateTransition::Complete("unreachable".to_string()),
}
}
fn initial_state(&self) -> Self::State {
State::Start
}
}
let machine = ErrorMachine;
let mut task = StateMachineTask::new(machine);
assert!(task.next_status().is_none());
}
#[test]
fn test_state_transition_spawn() {
use crate::valtron::executors::SpawnWithSchedule;
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq)]
enum State {
Start,
Spawned,
End,
}
struct SpawnMachine;
impl StateMachine for SpawnMachine {
type State = State;
type Output = String;
type Error = ();
type Action = SpawnWithSchedule<Box<dyn FnOnce() + Send>>;
fn transition(
&mut self,
state: Self::State,
) -> StateTransition<Self::State, Self::Output, Self::Error, Self::Action> {
match state {
State::Start => {
let action =
SpawnWithSchedule::new(Box::new(|| {}) as Box<dyn FnOnce() + Send>);
StateTransition::Spawn(action, State::Spawned)
}
State::Spawned => StateTransition::Complete("spawned task".to_string()),
State::End => StateTransition::Complete("done".to_string()),
}
}
fn initial_state(&self) -> Self::State {
State::Start
}
}
let machine = SpawnMachine;
let mut task = StateMachineTask::new(machine);
match task.next_status() {
Some(TaskStatus::Spawn(_)) => {}
other => panic!("Expected Spawn, got {:?}", other),
}
match task.next_status() {
Some(TaskStatus::Ready(ref s)) if s == "spawned task" => {}
other => panic!("Expected Ready(spawned task), got {:?}", other),
}
}
}