pub trait StateMachine: Copy + Eq + std::fmt::Debug + Sized + 'static {
const STATES: &'static [Self];
const EDGES: &'static [(Self, Self)];
const ESCAPE: &'static [Self];
fn is_terminal(self) -> bool {
!Self::EDGES.iter().any(|(from, _)| *from == self)
}
fn can_transition(from: Self, to: Self) -> bool {
Self::EDGES.contains(&(from, to))
}
}
macro_rules! edges {
($ty:ident: $($from:ident -> $to:ident),+ $(,)?) => {
&[$(($ty::$from, $ty::$to)),+]
};
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
specta::Type,
)]
#[serde(rename_all = "snake_case")]
pub enum TaskState {
Submitted,
Working,
InputRequired,
Completed,
Failed,
Canceled,
}
impl StateMachine for TaskState {
const STATES: &'static [Self] = &[
Self::Submitted,
Self::Working,
Self::InputRequired,
Self::Completed,
Self::Failed,
Self::Canceled,
];
const EDGES: &'static [(Self, Self)] = edges![TaskState:
Submitted -> Working,
Submitted -> Canceled,
Working -> InputRequired,
Working -> Completed,
Working -> Failed,
Working -> Canceled,
InputRequired -> Working,
InputRequired -> Canceled,
];
const ESCAPE: &'static [Self] = &[Self::Canceled, Self::Failed];
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
specta::Type,
)]
#[serde(rename_all = "snake_case")]
pub enum AttemptState {
Queued,
Leased,
Starting,
Running,
Blocked,
Canceling,
Canceled,
Failed,
Unknown,
Succeeded,
}
impl StateMachine for AttemptState {
const STATES: &'static [Self] = &[
Self::Queued,
Self::Leased,
Self::Starting,
Self::Running,
Self::Blocked,
Self::Canceling,
Self::Canceled,
Self::Failed,
Self::Unknown,
Self::Succeeded,
];
const EDGES: &'static [(Self, Self)] = edges![AttemptState:
Queued -> Leased,
Queued -> Canceled,
Leased -> Starting,
Leased -> Canceled,
Starting -> Running,
Starting -> Failed,
Starting -> Unknown,
Starting -> Canceling,
Running -> Blocked,
Running -> Canceling,
Running -> Failed,
Running -> Unknown,
Running -> Succeeded,
Blocked -> Running,
Blocked -> Canceling,
Blocked -> Failed,
Blocked -> Unknown,
Canceling -> Canceled,
Canceling -> Unknown,
];
const ESCAPE: &'static [Self] = &[Self::Canceling, Self::Canceled, Self::Failed, Self::Unknown];
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
specta::Type,
)]
#[serde(rename_all = "snake_case")]
pub enum MessageState {
Accepted,
Delivered,
Acknowledged,
Applied,
Rejected,
DeadLetter,
}
impl StateMachine for MessageState {
const STATES: &'static [Self] = &[
Self::Accepted,
Self::Delivered,
Self::Acknowledged,
Self::Applied,
Self::Rejected,
Self::DeadLetter,
];
const EDGES: &'static [(Self, Self)] = edges![MessageState:
Accepted -> Delivered,
Accepted -> DeadLetter,
Delivered -> Acknowledged,
Delivered -> DeadLetter,
Acknowledged -> Applied,
Acknowledged -> Rejected,
Acknowledged -> DeadLetter,
];
const ESCAPE: &'static [Self] = &[Self::DeadLetter];
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
specta::Type,
)]
#[serde(rename_all = "snake_case")]
pub enum CommandState {
Issued,
Targeted,
Signaled,
VerificationComplete,
}
impl StateMachine for CommandState {
const STATES: &'static [Self] = &[
Self::Issued,
Self::Targeted,
Self::Signaled,
Self::VerificationComplete,
];
const EDGES: &'static [(Self, Self)] = edges![CommandState:
Issued -> Targeted,
Targeted -> Signaled,
Signaled -> VerificationComplete,
];
const ESCAPE: &'static [Self] = &[];
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
specta::Type,
)]
#[serde(rename_all = "snake_case")]
pub enum Outcome {
Clean,
Partial,
Unknown,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
specta::Type,
)]
#[serde(rename_all = "snake_case")]
pub enum GateVerdict {
Pending,
Pass,
Fail,
Partial,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
specta::Type,
)]
#[serde(rename_all = "snake_case")]
pub enum LeaseState {
Held,
Released,
Expired,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
specta::Type,
)]
#[serde(rename_all = "snake_case")]
pub enum LeaseMode {
Exclusive,
Shared,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn terminality_is_derived_from_edges() {
assert!(TaskState::Completed.is_terminal());
assert!(!TaskState::Submitted.is_terminal());
assert!(AttemptState::Unknown.is_terminal());
assert!(!AttemptState::Canceling.is_terminal());
assert!(MessageState::DeadLetter.is_terminal());
assert!(CommandState::VerificationComplete.is_terminal());
}
#[test]
fn wire_values_are_snake_case() {
assert_eq!(
serde_json::to_value(TaskState::InputRequired).expect("serialize"),
serde_json::json!("input_required")
);
assert_eq!(
serde_json::to_value(CommandState::VerificationComplete).expect("serialize"),
serde_json::json!("verification_complete")
);
assert_eq!(
serde_json::to_value(MessageState::DeadLetter).expect("serialize"),
serde_json::json!("dead_letter")
);
}
}