use crate::domain::{CommitmentState, RuntimeState}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RuntimeAction { EnterPlanning, CancelOrTimeout, Activate { policy_accepted: bool }, StartTransition, CompleteForReview, SevereViolation, ReturnToActive, SwitchTask, EndTransitionForReview, ContinuePlanning, EndWorkPeriod, EnterAdminOverride, ExitAdminOverride(RuntimeState), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CommitmentAction { Activate, PauseForTransition, ReturnFromPause, Complete, Abandon, Violate, RecoverExplicitly, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum TransitionError { PolicyRejected, IllegalRuntimeTransition { current: RuntimeState, action: RuntimeAction, }, IllegalCommitmentTransition { current: CommitmentState, action: CommitmentAction, }, InvalidAdminOverrideExitTarget { target: RuntimeState, }, } impl std::fmt::Display for TransitionError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { TransitionError::PolicyRejected => write!(f, "policy was rejected"), TransitionError::IllegalRuntimeTransition { current, action } => { write!( f, "illegal runtime transition from {:?} via {:?}", current, action ) } TransitionError::IllegalCommitmentTransition { current, action } => { write!( f, "illegal commitment transition from {:?} via {:?}", current, action ) } TransitionError::InvalidAdminOverrideExitTarget { target } => { write!(f, "invalid admin override exit target {:?}", target) } } } } impl std::error::Error for TransitionError {} #[derive(Clone, Debug, PartialEq, Eq)] pub struct CompositeTransition { pub runtime_state: RuntimeState, pub commitment_state: CommitmentState, } pub fn transition_runtime( current: RuntimeState, action: RuntimeAction, ) -> Result { use RuntimeAction::*; use RuntimeState::*; match (current, action) { (Locked, EnterPlanning) => Ok(Planning), ( Planning, Activate { policy_accepted: true, }, ) => Ok(Active), ( Planning, Activate { policy_accepted: false, }, ) => Err(TransitionError::PolicyRejected), (Planning, CancelOrTimeout) => Ok(Locked), (Active, StartTransition) => Ok(Transition), (Active, CompleteForReview) => Ok(Review), (Active, SevereViolation) => Ok(Locked), (Transition, ReturnToActive) => Ok(Active), (Transition, SwitchTask) => Ok(Planning), (Transition, EndTransitionForReview) => Ok(Review), (Transition, CancelOrTimeout) => Ok(Locked), (Review, ContinuePlanning) => Ok(Planning), (Review, EndWorkPeriod) => Ok(Locked), (_, EnterAdminOverride) => Ok(AdminOverride), (AdminOverride, ExitAdminOverride(Locked)) => Ok(Locked), (AdminOverride, ExitAdminOverride(Planning)) => Ok(Planning), (AdminOverride, ExitAdminOverride(target)) => { Err(TransitionError::InvalidAdminOverrideExitTarget { target }) } _ => Err(TransitionError::IllegalRuntimeTransition { current, action }), } } pub fn transition_commitment( current: CommitmentState, action: CommitmentAction, ) -> Result { use CommitmentAction::*; use CommitmentState::*; match (current.clone(), action) { (Draft, Activate) => Ok(Active), (Active, PauseForTransition) => Ok(Paused), (Paused, ReturnFromPause) => Ok(Active), (Active, Complete) => Ok(Completed), (Active, Abandon) => Ok(Abandoned), (Active, Violate) => Ok(Violated), (Paused, Abandon) => Ok(Abandoned), (Violated, RecoverExplicitly) => Ok(Active), (Violated, Abandon) => Ok(Abandoned), _ => Err(TransitionError::IllegalCommitmentTransition { current, action }), } } pub fn start_transition( runtime_state: RuntimeState, commitment_state: CommitmentState, ) -> Result { let next_runtime = transition_runtime(runtime_state, RuntimeAction::StartTransition)?; let next_commitment = transition_commitment(commitment_state, CommitmentAction::PauseForTransition)?; Ok(CompositeTransition { runtime_state: next_runtime, commitment_state: next_commitment, }) } #[cfg(test)] mod tests { use super::*; #[test] fn planning_can_only_activate_with_valid_policy() { assert_eq!( transition_runtime( RuntimeState::Planning, RuntimeAction::Activate { policy_accepted: true, } ), Ok(RuntimeState::Active) ); assert_eq!( transition_runtime( RuntimeState::Planning, RuntimeAction::Activate { policy_accepted: false, } ), Err(TransitionError::PolicyRejected) ); } #[test] fn active_can_enter_transition_or_review_or_locked() { assert_eq!( transition_runtime(RuntimeState::Active, RuntimeAction::StartTransition), Ok(RuntimeState::Transition) ); assert_eq!( transition_runtime(RuntimeState::Active, RuntimeAction::CompleteForReview), Ok(RuntimeState::Review) ); assert_eq!( transition_runtime(RuntimeState::Active, RuntimeAction::SevereViolation), Ok(RuntimeState::Locked) ); } #[test] fn locked_cannot_directly_become_active() { let err = transition_runtime( RuntimeState::Locked, RuntimeAction::Activate { policy_accepted: true, }, ) .unwrap_err(); assert_eq!( err, TransitionError::IllegalRuntimeTransition { current: RuntimeState::Locked, action: RuntimeAction::Activate { policy_accepted: true, }, } ); assert_eq!( err.to_string(), "illegal runtime transition from Locked via Activate { policy_accepted: true }" ); } #[test] fn admin_override_cannot_exit_to_admin_override() { let err = transition_runtime( RuntimeState::AdminOverride, RuntimeAction::ExitAdminOverride(RuntimeState::AdminOverride), ) .unwrap_err(); assert_eq!( err, TransitionError::InvalidAdminOverrideExitTarget { target: RuntimeState::AdminOverride, } ); } #[test] fn admin_override_cannot_exit_to_active_or_transition() { for target in [RuntimeState::Active, RuntimeState::Transition] { let err = transition_runtime( RuntimeState::AdminOverride, RuntimeAction::ExitAdminOverride(target), ) .unwrap_err(); assert_eq!( err, TransitionError::InvalidAdminOverrideExitTarget { target } ); } } #[test] fn admin_override_can_exit_to_locked_or_planning() { assert_eq!( transition_runtime( RuntimeState::AdminOverride, RuntimeAction::ExitAdminOverride(RuntimeState::Locked), ), Ok(RuntimeState::Locked) ); assert_eq!( transition_runtime( RuntimeState::AdminOverride, RuntimeAction::ExitAdminOverride(RuntimeState::Planning), ), Ok(RuntimeState::Planning) ); } #[test] fn commitment_violation_recovery_is_explicit() { assert_eq!( transition_commitment( CommitmentState::Violated, CommitmentAction::RecoverExplicitly ), Ok(CommitmentState::Active) ); let err = transition_commitment(CommitmentState::Violated, CommitmentAction::ReturnFromPause) .unwrap_err(); assert_eq!( err, TransitionError::IllegalCommitmentTransition { current: CommitmentState::Violated, action: CommitmentAction::ReturnFromPause, } ); assert_eq!( err.to_string(), "illegal commitment transition from Violated via ReturnFromPause" ); } #[test] fn start_transition_moves_runtime_and_commitment_together() { assert_eq!( start_transition(RuntimeState::Active, CommitmentState::Active), Ok(CompositeTransition { runtime_state: RuntimeState::Transition, commitment_state: CommitmentState::Paused, }) ); } #[test] fn start_transition_fails_before_returning_partial_state() { assert_eq!( start_transition(RuntimeState::Active, CommitmentState::Draft), Err(TransitionError::IllegalCommitmentTransition { current: CommitmentState::Draft, action: CommitmentAction::PauseForTransition, }) ); assert_eq!( start_transition(RuntimeState::Locked, CommitmentState::Active), Err(TransitionError::IllegalRuntimeTransition { current: RuntimeState::Locked, action: RuntimeAction::StartTransition, }) ); } }