use crate::core::{Guard, State};
use std::sync::Arc;
use stillwater::effect::BoxedEffect;
#[derive(Clone, Debug, PartialEq)]
pub enum TransitionResult<S: State> {
Success(S),
Retry { feedback: String, current_state: S },
Abort { reason: String, error_state: S },
}
#[derive(Debug, thiserror::Error)]
pub enum TransitionError {
#[error("No transition available from state '{from}'")]
NoTransition { from: String },
#[error("Guard blocked transition from '{from}' to '{to}'")]
GuardBlocked { from: String, to: String },
#[error("Transition action failed: {0}")]
ActionFailed(String),
}
pub type TransitionAction<S, Env> =
Arc<dyn Fn() -> BoxedEffect<TransitionResult<S>, TransitionError, Env> + Send + Sync>;
pub struct Transition<S: State, Env> {
pub from: S,
pub to: S,
pub guard: Option<Guard<S>>,
pub action: TransitionAction<S, Env>,
}
impl<S: State, Env> Transition<S, Env> {
pub fn can_execute(&self, current: &S) -> bool {
if *current != self.from {
return false;
}
self.guard.as_ref().is_none_or(|g| g.check(current))
}
}
impl<S: State, Env> Clone for Transition<S, Env> {
fn clone(&self) -> Self {
Self {
from: self.from.clone(),
to: self.to.clone(),
guard: self.guard.clone(),
action: Arc::clone(&self.action),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Guard;
use serde::{Deserialize, Serialize};
use stillwater::prelude::*;
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
enum TestState {
Start,
Middle,
End,
}
impl State for TestState {
fn name(&self) -> &str {
match self {
Self::Start => "Start",
Self::Middle => "Middle",
Self::End => "End",
}
}
fn is_final(&self) -> bool {
matches!(self, Self::End)
}
}
#[test]
fn can_execute_matches_from_state() {
let transition: Transition<TestState, ()> = Transition {
from: TestState::Start,
to: TestState::Middle,
guard: None,
action: Arc::new(|| pure(TransitionResult::Success(TestState::Middle)).boxed()),
};
assert!(transition.can_execute(&TestState::Start));
assert!(!transition.can_execute(&TestState::Middle));
}
#[test]
fn can_execute_respects_guard() {
let guard = Guard::new(|s: &TestState| s.is_final());
let transition: Transition<TestState, ()> = Transition {
from: TestState::End,
to: TestState::Start,
guard: Some(guard),
action: Arc::new(|| pure(TransitionResult::Success(TestState::Start)).boxed()),
};
assert!(transition.can_execute(&TestState::End));
let transition2: Transition<TestState, ()> = Transition {
from: TestState::Start,
to: TestState::Middle,
guard: Some(Guard::new(|s: &TestState| s.is_final())),
action: Arc::new(|| pure(TransitionResult::Success(TestState::Middle)).boxed()),
};
assert!(!transition2.can_execute(&TestState::Start));
}
}