use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::error::{ModelError, ModelResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum TaskPhase {
Pending,
Running,
Succeeded,
Failed,
Timeout,
Canceled,
Exhausted,
}
impl fmt::Display for TaskPhase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TaskPhase::Pending => f.write_str("pending"),
TaskPhase::Running => f.write_str("running"),
TaskPhase::Succeeded => f.write_str("succeeded"),
TaskPhase::Failed => f.write_str("failed"),
TaskPhase::Timeout => f.write_str("timeout"),
TaskPhase::Canceled => f.write_str("canceled"),
TaskPhase::Exhausted => f.write_str("exhausted"),
}
}
}
impl FromStr for TaskPhase {
type Err = ModelError;
fn from_str(s: &str) -> ModelResult<Self> {
let trimmed = s.trim();
match trimmed.to_ascii_lowercase().as_str() {
"pending" => Ok(TaskPhase::Pending),
"running" => Ok(TaskPhase::Running),
"succeeded" => Ok(TaskPhase::Succeeded),
"failed" => Ok(TaskPhase::Failed),
"timeout" => Ok(TaskPhase::Timeout),
"canceled" => Ok(TaskPhase::Canceled),
"exhausted" => Ok(TaskPhase::Exhausted),
_ => Err(ModelError::UnknownTaskPhase(trimmed.to_string())),
}
}
}
impl TaskPhase {
#[inline]
pub fn is_terminal(&self) -> bool {
matches!(
self,
TaskPhase::Succeeded
| TaskPhase::Failed
| TaskPhase::Timeout
| TaskPhase::Canceled
| TaskPhase::Exhausted
)
}
#[inline]
pub fn is_active(&self) -> bool {
matches!(self, TaskPhase::Pending | TaskPhase::Running)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn terminal_states() {
assert!(TaskPhase::Succeeded.is_terminal());
assert!(TaskPhase::Failed.is_terminal());
assert!(TaskPhase::Timeout.is_terminal());
assert!(TaskPhase::Canceled.is_terminal());
assert!(TaskPhase::Exhausted.is_terminal());
assert!(!TaskPhase::Pending.is_terminal());
assert!(!TaskPhase::Running.is_terminal());
}
#[test]
fn active_states() {
assert!(TaskPhase::Pending.is_active());
assert!(TaskPhase::Running.is_active());
assert!(!TaskPhase::Succeeded.is_active());
assert!(!TaskPhase::Failed.is_active());
}
#[test]
fn serde_roundtrip() {
let status = TaskPhase::Running;
let json = serde_json::to_string(&status).unwrap();
assert_eq!(json, r#""running""#);
let back: TaskPhase = serde_json::from_str(&json).unwrap();
assert_eq!(back, status);
}
#[test]
fn from_str_all_variants() {
let cases = [
("pending", TaskPhase::Pending),
("running", TaskPhase::Running),
("succeeded", TaskPhase::Succeeded),
("failed", TaskPhase::Failed),
("timeout", TaskPhase::Timeout),
("canceled", TaskPhase::Canceled),
("exhausted", TaskPhase::Exhausted),
];
for (s, expected) in cases {
assert_eq!(s.parse::<TaskPhase>().unwrap(), expected);
}
}
#[test]
fn from_str_is_case_insensitive_and_trims() {
assert_eq!("RUNNING".parse::<TaskPhase>().unwrap(), TaskPhase::Running);
assert_eq!(
" Succeeded ".parse::<TaskPhase>().unwrap(),
TaskPhase::Succeeded
);
}
#[test]
fn from_str_roundtrips_display() {
for phase in [
TaskPhase::Pending,
TaskPhase::Running,
TaskPhase::Succeeded,
TaskPhase::Failed,
TaskPhase::Timeout,
TaskPhase::Canceled,
TaskPhase::Exhausted,
] {
let rendered = phase.to_string();
assert_eq!(rendered.parse::<TaskPhase>().unwrap(), phase);
}
}
#[test]
fn from_str_unknown_errors() {
let err = "bogus".parse::<TaskPhase>().unwrap_err();
assert!(matches!(err, ModelError::UnknownTaskPhase(_)));
}
}