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> {
60 let trimmed = s.trim();
61 match trimmed.to_ascii_lowercase().as_str() {
62 "pending" => Ok(TaskPhase::Pending),
63 "running" => Ok(TaskPhase::Running),
64 "succeeded" => Ok(TaskPhase::Succeeded),
65 "failed" => Ok(TaskPhase::Failed),
66 "timeout" => Ok(TaskPhase::Timeout),
67 "canceled" => Ok(TaskPhase::Canceled),
68 "exhausted" => Ok(TaskPhase::Exhausted),
69 _ => Err(ModelError::UnknownTaskPhase(trimmed.to_string())),
70 }
71 }
72}
73
74impl TaskPhase {
75 #[inline]
80 pub fn is_terminal(&self) -> bool {
81 matches!(
82 self,
83 TaskPhase::Succeeded
84 | TaskPhase::Failed
85 | TaskPhase::Timeout
86 | TaskPhase::Canceled
87 | TaskPhase::Exhausted
88 )
89 }
90
91 #[inline]
93 pub fn is_active(&self) -> bool {
94 matches!(self, TaskPhase::Pending | TaskPhase::Running)
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn terminal_states() {
104 assert!(TaskPhase::Succeeded.is_terminal());
105 assert!(TaskPhase::Failed.is_terminal());
106 assert!(TaskPhase::Timeout.is_terminal());
107 assert!(TaskPhase::Canceled.is_terminal());
108 assert!(TaskPhase::Exhausted.is_terminal());
109
110 assert!(!TaskPhase::Pending.is_terminal());
111 assert!(!TaskPhase::Running.is_terminal());
112 }
113
114 #[test]
115 fn active_states() {
116 assert!(TaskPhase::Pending.is_active());
117 assert!(TaskPhase::Running.is_active());
118
119 assert!(!TaskPhase::Succeeded.is_active());
120 assert!(!TaskPhase::Failed.is_active());
121 }
122
123 #[test]
124 fn serde_roundtrip() {
125 let status = TaskPhase::Running;
126 let json = serde_json::to_string(&status).unwrap();
127 assert_eq!(json, r#""running""#);
128
129 let back: TaskPhase = serde_json::from_str(&json).unwrap();
130 assert_eq!(back, status);
131 }
132
133 #[test]
134 fn from_str_all_variants() {
135 let cases = [
136 ("pending", TaskPhase::Pending),
137 ("running", TaskPhase::Running),
138 ("succeeded", TaskPhase::Succeeded),
139 ("failed", TaskPhase::Failed),
140 ("timeout", TaskPhase::Timeout),
141 ("canceled", TaskPhase::Canceled),
142 ("exhausted", TaskPhase::Exhausted),
143 ];
144 for (s, expected) in cases {
145 assert_eq!(s.parse::<TaskPhase>().unwrap(), expected);
146 }
147 }
148
149 #[test]
150 fn from_str_is_case_insensitive_and_trims() {
151 assert_eq!("RUNNING".parse::<TaskPhase>().unwrap(), TaskPhase::Running);
152 assert_eq!(
153 " Succeeded ".parse::<TaskPhase>().unwrap(),
154 TaskPhase::Succeeded
155 );
156 }
157
158 #[test]
159 fn from_str_roundtrips_display() {
160 for phase in [
161 TaskPhase::Pending,
162 TaskPhase::Running,
163 TaskPhase::Succeeded,
164 TaskPhase::Failed,
165 TaskPhase::Timeout,
166 TaskPhase::Canceled,
167 TaskPhase::Exhausted,
168 ] {
169 let rendered = phase.to_string();
170 assert_eq!(rendered.parse::<TaskPhase>().unwrap(), phase);
171 }
172 }
173
174 #[test]
175 fn from_str_unknown_errors() {
176 let err = "bogus".parse::<TaskPhase>().unwrap_err();
177 assert!(matches!(err, ModelError::UnknownTaskPhase(_)));
178 }
179}