use ainl_contracts::MissionState;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ValidTransition {
pub from: MissionState,
pub to: MissionState,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum StateMachineError {
#[error("terminal state {0:?} cannot transition")]
Terminal(MissionState),
#[error("invalid transition {from:?} -> {to:?}")]
InvalidTransition {
from: MissionState,
to: MissionState,
},
}
pub fn can_transition(from: MissionState, to: MissionState) -> bool {
if from == to {
return true;
}
match from {
MissionState::Completed | MissionState::Cancelled => false,
MissionState::AwaitingInput => {
matches!(to, MissionState::Initializing | MissionState::Cancelled)
}
MissionState::Initializing => {
matches!(to, MissionState::Running | MissionState::Cancelled)
}
MissionState::Running => matches!(
to,
MissionState::Paused
| MissionState::OrchestratorTurn
| MissionState::Completed
| MissionState::Cancelled
),
MissionState::Paused => matches!(
to,
MissionState::Running
| MissionState::OrchestratorTurn
| MissionState::Cancelled
),
MissionState::OrchestratorTurn => matches!(
to,
MissionState::Running | MissionState::Completed | MissionState::Cancelled
),
}
}
pub fn transition(from: MissionState, to: MissionState) -> Result<MissionState, StateMachineError> {
if from == to {
return Ok(to);
}
if matches!(from, MissionState::Completed | MissionState::Cancelled) {
return Err(StateMachineError::Terminal(from));
}
if can_transition(from, to) {
Ok(to)
} else {
Err(StateMachineError::InvalidTransition { from, to })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn awaiting_input_to_initializing() {
assert!(can_transition(
MissionState::AwaitingInput,
MissionState::Initializing
));
assert_eq!(
transition(
MissionState::AwaitingInput,
MissionState::Initializing
)
.unwrap(),
MissionState::Initializing
);
}
#[test]
fn running_pause_orchestrator_complete() {
assert!(can_transition(MissionState::Running, MissionState::Paused));
assert!(can_transition(
MissionState::Running,
MissionState::OrchestratorTurn
));
assert!(can_transition(MissionState::Running, MissionState::Completed));
}
#[test]
fn terminal_states_reject_moves() {
assert!(!can_transition(MissionState::Completed, MissionState::Running));
assert_eq!(
transition(MissionState::Completed, MissionState::Running),
Err(StateMachineError::Terminal(MissionState::Completed))
);
}
#[test]
fn invalid_skip_initializing() {
assert_eq!(
transition(MissionState::AwaitingInput, MissionState::Running),
Err(StateMachineError::InvalidTransition {
from: MissionState::AwaitingInput,
to: MissionState::Running,
})
);
}
#[test]
fn orchestrator_turn_returns_to_running() {
assert!(can_transition(
MissionState::OrchestratorTurn,
MissionState::Running
));
}
}