solti_model/domain/
phase.rs1use std::fmt;
6use std::str::FromStr;
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::{ModelError, ModelResult};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23#[non_exhaustive]
24pub enum TaskPhase {
25 Pending,
27 Running,
29 Succeeded,
31 Failed,
33 Timeout,
35 Canceled,
37 Exhausted,
39}
40
41impl fmt::Display for TaskPhase {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 TaskPhase::Pending => f.write_str("pending"),
45 TaskPhase::Running => f.write_str("running"),
46 TaskPhase::Succeeded => f.write_str("succeeded"),
47 TaskPhase::Failed => f.write_str("failed"),
48 TaskPhase::Timeout => f.write_str("timeout"),
49 TaskPhase::Canceled => f.write_str("canceled"),
50 TaskPhase::Exhausted => f.write_str("exhausted"),
51 }
52 }
53}
54
55impl FromStr for TaskPhase {
56 type Err = ModelError;
57
58 fn from_str(s: &str) -> ModelResult<Self> {
61 let trimmed = s.trim();
62 match trimmed.to_ascii_lowercase().as_str() {
63 "pending" => Ok(TaskPhase::Pending),
64 "running" => Ok(TaskPhase::Running),
65 "succeeded" => Ok(TaskPhase::Succeeded),
66 "failed" => Ok(TaskPhase::Failed),
67 "timeout" => Ok(TaskPhase::Timeout),
68 "canceled" => Ok(TaskPhase::Canceled),
69 "exhausted" => Ok(TaskPhase::Exhausted),
70 _ => Err(ModelError::UnknownTaskPhase(trimmed.to_string())),
71 }
72 }
73}
74
75impl TaskPhase {
76 #[inline]
81 pub fn is_terminal(&self) -> bool {
82 matches!(
83 self,
84 TaskPhase::Succeeded
85 | TaskPhase::Failed
86 | TaskPhase::Timeout
87 | TaskPhase::Canceled
88 | TaskPhase::Exhausted
89 )
90 }
91
92 #[inline]
94 pub fn is_active(&self) -> bool {
95 matches!(self, TaskPhase::Pending | TaskPhase::Running)
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn terminal_states() {
105 assert!(TaskPhase::Succeeded.is_terminal());
106 assert!(TaskPhase::Failed.is_terminal());
107 assert!(TaskPhase::Timeout.is_terminal());
108 assert!(TaskPhase::Canceled.is_terminal());
109 assert!(TaskPhase::Exhausted.is_terminal());
110
111 assert!(!TaskPhase::Pending.is_terminal());
112 assert!(!TaskPhase::Running.is_terminal());
113 }
114
115 #[test]
116 fn active_states() {
117 assert!(TaskPhase::Pending.is_active());
118 assert!(TaskPhase::Running.is_active());
119
120 assert!(!TaskPhase::Succeeded.is_active());
121 assert!(!TaskPhase::Failed.is_active());
122 }
123
124 #[test]
125 fn serde_roundtrip() {
126 let status = TaskPhase::Running;
127 let json = serde_json::to_string(&status).unwrap();
128 assert_eq!(json, r#""running""#);
129
130 let back: TaskPhase = serde_json::from_str(&json).unwrap();
131 assert_eq!(back, status);
132 }
133
134 #[test]
135 fn from_str_all_variants() {
136 let cases = [
137 ("pending", TaskPhase::Pending),
138 ("running", TaskPhase::Running),
139 ("succeeded", TaskPhase::Succeeded),
140 ("failed", TaskPhase::Failed),
141 ("timeout", TaskPhase::Timeout),
142 ("canceled", TaskPhase::Canceled),
143 ("exhausted", TaskPhase::Exhausted),
144 ];
145 for (s, expected) in cases {
146 assert_eq!(s.parse::<TaskPhase>().unwrap(), expected);
147 }
148 }
149
150 #[test]
151 fn from_str_is_case_insensitive_and_trims() {
152 assert_eq!("RUNNING".parse::<TaskPhase>().unwrap(), TaskPhase::Running);
153 assert_eq!(
154 " Succeeded ".parse::<TaskPhase>().unwrap(),
155 TaskPhase::Succeeded
156 );
157 }
158
159 #[test]
160 fn from_str_roundtrips_display() {
161 for phase in [
162 TaskPhase::Pending,
163 TaskPhase::Running,
164 TaskPhase::Succeeded,
165 TaskPhase::Failed,
166 TaskPhase::Timeout,
167 TaskPhase::Canceled,
168 TaskPhase::Exhausted,
169 ] {
170 let rendered = phase.to_string();
171 assert_eq!(rendered.parse::<TaskPhase>().unwrap(), phase);
172 }
173 }
174
175 #[test]
176 fn from_str_unknown_errors() {
177 let err = "bogus".parse::<TaskPhase>().unwrap_err();
178 assert!(matches!(err, ModelError::UnknownTaskPhase(_)));
179 }
180}