use crate::error::{
ContractResult, ContractViolation, CODE_TRANSITION_FORWARD_AFTER_COMPENSATION,
CODE_TRANSITION_ILLEGAL, CODE_TRANSITION_MANUAL_CLOSE_UNAUDITED,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum SagaStatus {
Running,
Cancelling,
WaitingResolution,
Compensating,
Completed,
Compensated,
ManualIntervention,
ManuallyClosed,
}
impl SagaStatus {
pub const INITIAL: Self = Self::Running;
pub fn as_str(self) -> &'static str {
match self {
Self::Running => "RUNNING",
Self::Cancelling => "CANCELLING",
Self::WaitingResolution => "WAITING_RESOLUTION",
Self::Compensating => "COMPENSATING",
Self::Completed => "COMPLETED",
Self::Compensated => "COMPENSATED",
Self::ManualIntervention => "MANUAL_INTERVENTION",
Self::ManuallyClosed => "MANUALLY_CLOSED",
}
}
pub fn parse(raw: &str) -> Option<Self> {
match raw {
"RUNNING" => Some(Self::Running),
"CANCELLING" => Some(Self::Cancelling),
"WAITING_RESOLUTION" => Some(Self::WaitingResolution),
"COMPENSATING" => Some(Self::Compensating),
"COMPLETED" => Some(Self::Completed),
"COMPENSATED" => Some(Self::Compensated),
"MANUAL_INTERVENTION" => Some(Self::ManualIntervention),
"MANUALLY_CLOSED" => Some(Self::ManuallyClosed),
_ => None,
}
}
pub fn is_terminal(self) -> bool {
matches!(
self,
Self::Completed | Self::Compensated | Self::ManuallyClosed
)
}
pub fn allows_automatic_commands(self) -> bool {
matches!(
self,
Self::Running | Self::Cancelling | Self::WaitingResolution | Self::Compensating
)
}
pub fn can_transition_to(self, to: Self) -> bool {
match (self, to) {
(Self::Running, Self::Running)
| (Self::Cancelling, Self::Cancelling)
| (Self::WaitingResolution, Self::WaitingResolution)
| (Self::Compensating, Self::Compensating) => true,
(Self::Running, Self::Completed)
| (Self::Running, Self::Compensating)
| (Self::Running, Self::Cancelling)
| (Self::Running, Self::WaitingResolution) => true,
(Self::Cancelling, Self::WaitingResolution)
| (Self::Cancelling, Self::Running)
| (Self::Cancelling, Self::Completed)
| (Self::Cancelling, Self::Compensating) => true,
(Self::WaitingResolution, Self::Running)
| (Self::WaitingResolution, Self::Completed)
| (Self::WaitingResolution, Self::Compensating) => true,
(Self::Compensating, Self::WaitingResolution)
| (Self::Compensating, Self::Compensated) => true,
(Self::Running, Self::ManualIntervention)
| (Self::Cancelling, Self::ManualIntervention)
| (Self::WaitingResolution, Self::ManualIntervention)
| (Self::Compensating, Self::ManualIntervention) => true,
(Self::ManualIntervention, Self::Compensating)
| (Self::ManualIntervention, Self::Running)
| (Self::ManualIntervention, Self::WaitingResolution) => true,
(Self::ManualIntervention, Self::ManuallyClosed) => true,
_ => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Direction {
Forward,
Compensating,
}
impl Direction {
pub fn as_str(self) -> &'static str {
match self {
Self::Forward => "FORWARD",
Self::Compensating => "COMPENSATING",
}
}
pub fn parse(raw: &str) -> Option<Self> {
match raw {
"FORWARD" => Some(Self::Forward),
"COMPENSATING" => Some(Self::Compensating),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ControlState {
Active,
Paused,
}
impl ControlState {
pub fn as_str(self) -> &'static str {
match self {
Self::Active => "ACTIVE",
Self::Paused => "PAUSED",
}
}
pub fn parse(raw: &str) -> Option<Self> {
match raw {
"ACTIVE" => Some(Self::Active),
"PAUSED" => Some(Self::Paused),
_ => None,
}
}
pub fn allows_automatic_actions(self) -> bool {
matches!(self, Self::Active)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TransitionGuard {
pub any_compensation_succeeded: bool,
pub manual_close_audited: bool,
}
impl TransitionGuard {
pub fn new(any_compensation_succeeded: bool) -> Self {
Self {
any_compensation_succeeded,
manual_close_audited: false,
}
}
pub fn manual_close(audited: bool) -> Self {
Self {
any_compensation_succeeded: false,
manual_close_audited: audited,
}
}
}
pub fn check_transition(
from: SagaStatus,
to: SagaStatus,
guard: TransitionGuard,
) -> ContractResult<()> {
if !from.can_transition_to(to) {
return Err(ContractViolation::new(
CODE_TRANSITION_ILLEGAL,
format!(
"transition {} -> {} is not in the closed state machine",
from.as_str(),
to.as_str()
),
));
}
if from == SagaStatus::ManualIntervention
&& to == SagaStatus::Running
&& guard.any_compensation_succeeded
{
return Err(ContractViolation::new(
CODE_TRANSITION_FORWARD_AFTER_COMPENSATION,
"cannot resume forward execution after a compensation has already succeeded",
));
}
if to == SagaStatus::ManuallyClosed && !guard.manual_close_audited {
return Err(ContractViolation::new(
CODE_TRANSITION_MANUAL_CLOSE_UNAUDITED,
"MANUALLY_CLOSED requires a same-transaction management audit record",
));
}
Ok(())
}