ainl-mission 0.1.0

Host-neutral mission engine: state machine, DAG, scheduler, stall, task ledger (zero armaraos-* deps)
Documentation
//! Pure [`MissionState`](ainl_contracts::MissionState) transition rules.

use ainl_contracts::MissionState;
use thiserror::Error;

/// A validated state transition.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ValidTransition {
    pub from: MissionState,
    pub to: MissionState,
}

/// State machine violation.
#[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,
    },
}

/// Returns true when `from` may move to `to`.
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
        ),
    }
}

/// Apply a transition, returning the target state or an error.
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
        ));
    }
}