use std::fmt;
use chrono::Utc;
use ironflow_store::entities::RunStatus;
use serde::{Deserialize, Serialize};
use super::{Transition, TransitionError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RunEvent {
PickedUp,
AllStepsCompleted,
StepFailed,
StepFailedRetryable,
RetryStarted,
MaxRetriesExceeded,
CancelRequested,
ApprovalRequested,
Approved,
Rejected,
}
impl fmt::Display for RunEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RunEvent::PickedUp => f.write_str("picked_up"),
RunEvent::AllStepsCompleted => f.write_str("all_steps_completed"),
RunEvent::StepFailed => f.write_str("step_failed"),
RunEvent::StepFailedRetryable => f.write_str("step_failed_retryable"),
RunEvent::RetryStarted => f.write_str("retry_started"),
RunEvent::MaxRetriesExceeded => f.write_str("max_retries_exceeded"),
RunEvent::CancelRequested => f.write_str("cancel_requested"),
RunEvent::ApprovalRequested => f.write_str("approval_requested"),
RunEvent::Approved => f.write_str("approved"),
RunEvent::Rejected => f.write_str("rejected"),
}
}
}
#[derive(Debug, Clone)]
pub struct RunFsm {
state: RunStatus,
history: Vec<Transition<RunStatus, RunEvent>>,
}
impl RunFsm {
pub fn new() -> Self {
Self {
state: RunStatus::Pending,
history: Vec::new(),
}
}
pub fn from_state(state: RunStatus) -> Self {
Self {
state,
history: Vec::new(),
}
}
pub fn state(&self) -> RunStatus {
self.state
}
pub fn history(&self) -> &[Transition<RunStatus, RunEvent>] {
&self.history
}
pub fn is_terminal(&self) -> bool {
self.state.is_terminal()
}
pub fn apply(
&mut self,
event: RunEvent,
) -> Result<RunStatus, TransitionError<RunStatus, RunEvent>> {
let next = next_state(self.state, event).ok_or(TransitionError {
from: self.state,
event,
})?;
let transition = Transition {
from: self.state,
to: next,
event,
at: Utc::now(),
};
self.history.push(transition);
self.state = next;
Ok(next)
}
pub fn can_apply(&self, event: RunEvent) -> bool {
next_state(self.state, event).is_some()
}
}
impl Default for RunFsm {
fn default() -> Self {
Self::new()
}
}
fn next_state(from: RunStatus, event: RunEvent) -> Option<RunStatus> {
match (from, event) {
(RunStatus::Pending, RunEvent::PickedUp) => Some(RunStatus::Running),
(RunStatus::Pending, RunEvent::CancelRequested) => Some(RunStatus::Cancelled),
(RunStatus::Running, RunEvent::AllStepsCompleted) => Some(RunStatus::Completed),
(RunStatus::Running, RunEvent::StepFailed) => Some(RunStatus::Failed),
(RunStatus::Running, RunEvent::StepFailedRetryable) => Some(RunStatus::Retrying),
(RunStatus::Running, RunEvent::CancelRequested) => Some(RunStatus::Cancelled),
(RunStatus::Retrying, RunEvent::RetryStarted) => Some(RunStatus::Running),
(RunStatus::Retrying, RunEvent::MaxRetriesExceeded) => Some(RunStatus::Failed),
(RunStatus::Retrying, RunEvent::CancelRequested) => Some(RunStatus::Cancelled),
(RunStatus::Running, RunEvent::ApprovalRequested) => Some(RunStatus::AwaitingApproval),
(RunStatus::AwaitingApproval, RunEvent::Approved) => Some(RunStatus::Running),
(RunStatus::AwaitingApproval, RunEvent::Rejected) => Some(RunStatus::Failed),
(RunStatus::AwaitingApproval, RunEvent::CancelRequested) => Some(RunStatus::Cancelled),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pending_to_running() {
let mut fsm = RunFsm::new();
let result = fsm.apply(RunEvent::PickedUp);
assert!(result.is_ok());
assert_eq!(fsm.state(), RunStatus::Running);
}
#[test]
fn full_success_path() {
let mut fsm = RunFsm::new();
fsm.apply(RunEvent::PickedUp).unwrap();
fsm.apply(RunEvent::AllStepsCompleted).unwrap();
assert_eq!(fsm.state(), RunStatus::Completed);
assert!(fsm.is_terminal());
assert_eq!(fsm.history().len(), 2);
}
#[test]
fn full_failure_path() {
let mut fsm = RunFsm::new();
fsm.apply(RunEvent::PickedUp).unwrap();
fsm.apply(RunEvent::StepFailed).unwrap();
assert_eq!(fsm.state(), RunStatus::Failed);
assert!(fsm.is_terminal());
}
#[test]
fn retry_then_success() {
let mut fsm = RunFsm::new();
fsm.apply(RunEvent::PickedUp).unwrap();
fsm.apply(RunEvent::StepFailedRetryable).unwrap();
assert_eq!(fsm.state(), RunStatus::Retrying);
fsm.apply(RunEvent::RetryStarted).unwrap();
assert_eq!(fsm.state(), RunStatus::Running);
fsm.apply(RunEvent::AllStepsCompleted).unwrap();
assert_eq!(fsm.state(), RunStatus::Completed);
assert_eq!(fsm.history().len(), 4);
}
#[test]
fn retry_then_max_retries_exceeded() {
let mut fsm = RunFsm::new();
fsm.apply(RunEvent::PickedUp).unwrap();
fsm.apply(RunEvent::StepFailedRetryable).unwrap();
fsm.apply(RunEvent::MaxRetriesExceeded).unwrap();
assert_eq!(fsm.state(), RunStatus::Failed);
}
#[test]
fn cancel_from_pending() {
let mut fsm = RunFsm::new();
fsm.apply(RunEvent::CancelRequested).unwrap();
assert_eq!(fsm.state(), RunStatus::Cancelled);
assert!(fsm.is_terminal());
}
#[test]
fn cancel_from_running() {
let mut fsm = RunFsm::new();
fsm.apply(RunEvent::PickedUp).unwrap();
fsm.apply(RunEvent::CancelRequested).unwrap();
assert_eq!(fsm.state(), RunStatus::Cancelled);
}
#[test]
fn cancel_from_retrying() {
let mut fsm = RunFsm::new();
fsm.apply(RunEvent::PickedUp).unwrap();
fsm.apply(RunEvent::StepFailedRetryable).unwrap();
fsm.apply(RunEvent::CancelRequested).unwrap();
assert_eq!(fsm.state(), RunStatus::Cancelled);
}
#[test]
fn cannot_complete_from_pending() {
let mut fsm = RunFsm::new();
let result = fsm.apply(RunEvent::AllStepsCompleted);
assert!(result.is_err());
assert_eq!(fsm.state(), RunStatus::Pending);
}
#[test]
fn cannot_pick_up_running() {
let mut fsm = RunFsm::new();
fsm.apply(RunEvent::PickedUp).unwrap();
let result = fsm.apply(RunEvent::PickedUp);
assert!(result.is_err());
}
#[test]
fn cannot_transition_from_terminal() {
let mut fsm = RunFsm::new();
fsm.apply(RunEvent::PickedUp).unwrap();
fsm.apply(RunEvent::AllStepsCompleted).unwrap();
assert!(fsm.apply(RunEvent::PickedUp).is_err());
assert!(fsm.apply(RunEvent::CancelRequested).is_err());
assert!(fsm.apply(RunEvent::StepFailed).is_err());
}
#[test]
fn can_apply_checks_without_mutation() {
let fsm = RunFsm::new();
assert!(fsm.can_apply(RunEvent::PickedUp));
assert!(fsm.can_apply(RunEvent::CancelRequested));
assert!(!fsm.can_apply(RunEvent::AllStepsCompleted));
assert!(!fsm.can_apply(RunEvent::StepFailed));
assert_eq!(fsm.state(), RunStatus::Pending);
}
#[test]
fn from_state_resumes_at_given_state() {
let mut fsm = RunFsm::from_state(RunStatus::Running);
assert_eq!(fsm.state(), RunStatus::Running);
assert!(fsm.history().is_empty());
fsm.apply(RunEvent::AllStepsCompleted).unwrap();
assert_eq!(fsm.state(), RunStatus::Completed);
}
#[test]
fn history_records_transitions() {
let mut fsm = RunFsm::new();
fsm.apply(RunEvent::PickedUp).unwrap();
fsm.apply(RunEvent::StepFailedRetryable).unwrap();
fsm.apply(RunEvent::RetryStarted).unwrap();
let history = fsm.history();
assert_eq!(history.len(), 3);
assert_eq!(history[0].from, RunStatus::Pending);
assert_eq!(history[0].to, RunStatus::Running);
assert_eq!(history[0].event, RunEvent::PickedUp);
assert_eq!(history[1].from, RunStatus::Running);
assert_eq!(history[1].to, RunStatus::Retrying);
assert_eq!(history[1].event, RunEvent::StepFailedRetryable);
assert_eq!(history[2].from, RunStatus::Retrying);
assert_eq!(history[2].to, RunStatus::Running);
assert_eq!(history[2].event, RunEvent::RetryStarted);
}
#[test]
fn running_to_awaiting_approval() {
let mut fsm = RunFsm::new();
fsm.apply(RunEvent::PickedUp).unwrap();
fsm.apply(RunEvent::ApprovalRequested).unwrap();
assert_eq!(fsm.state(), RunStatus::AwaitingApproval);
assert!(!fsm.is_terminal());
}
#[test]
fn awaiting_approval_approved_resumes_running() {
let mut fsm = RunFsm::new();
fsm.apply(RunEvent::PickedUp).unwrap();
fsm.apply(RunEvent::ApprovalRequested).unwrap();
fsm.apply(RunEvent::Approved).unwrap();
assert_eq!(fsm.state(), RunStatus::Running);
}
#[test]
fn awaiting_approval_rejected_fails() {
let mut fsm = RunFsm::new();
fsm.apply(RunEvent::PickedUp).unwrap();
fsm.apply(RunEvent::ApprovalRequested).unwrap();
fsm.apply(RunEvent::Rejected).unwrap();
assert_eq!(fsm.state(), RunStatus::Failed);
assert!(fsm.is_terminal());
}
#[test]
fn awaiting_approval_cancel() {
let mut fsm = RunFsm::new();
fsm.apply(RunEvent::PickedUp).unwrap();
fsm.apply(RunEvent::ApprovalRequested).unwrap();
fsm.apply(RunEvent::CancelRequested).unwrap();
assert_eq!(fsm.state(), RunStatus::Cancelled);
assert!(fsm.is_terminal());
}
#[test]
fn cannot_approve_from_pending() {
let mut fsm = RunFsm::new();
assert!(fsm.apply(RunEvent::Approved).is_err());
}
#[test]
fn approval_then_complete() {
let mut fsm = RunFsm::new();
fsm.apply(RunEvent::PickedUp).unwrap();
fsm.apply(RunEvent::ApprovalRequested).unwrap();
fsm.apply(RunEvent::Approved).unwrap();
fsm.apply(RunEvent::AllStepsCompleted).unwrap();
assert_eq!(fsm.state(), RunStatus::Completed);
assert_eq!(fsm.history().len(), 4);
}
#[test]
fn transition_error_display() {
let mut fsm = RunFsm::new();
let err = fsm.apply(RunEvent::AllStepsCompleted).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("all_steps_completed"));
assert!(msg.contains("Pending"));
}
}