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)]
26#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
27#[serde(rename_all = "camelCase")]
28#[non_exhaustive]
29pub enum TaskPhase {
30 Pending,
32 Running,
34 Succeeded,
36 Failed,
38 Timeout,
40 Canceled,
42 Exhausted,
44}
45
46impl fmt::Display for TaskPhase {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 TaskPhase::Pending => f.write_str("pending"),
50 TaskPhase::Running => f.write_str("running"),
51 TaskPhase::Succeeded => f.write_str("succeeded"),
52 TaskPhase::Failed => f.write_str("failed"),
53 TaskPhase::Timeout => f.write_str("timeout"),
54 TaskPhase::Canceled => f.write_str("canceled"),
55 TaskPhase::Exhausted => f.write_str("exhausted"),
56 }
57 }
58}
59
60impl FromStr for TaskPhase {
61 type Err = ModelError;
62
63 fn from_str(s: &str) -> ModelResult<Self> {
71 let trimmed = s.trim();
72 match trimmed.to_ascii_lowercase().as_str() {
73 "pending" => Ok(TaskPhase::Pending),
74 "running" => Ok(TaskPhase::Running),
75 "succeeded" => Ok(TaskPhase::Succeeded),
76 "failed" => Ok(TaskPhase::Failed),
77 "timeout" => Ok(TaskPhase::Timeout),
78 "canceled" => Ok(TaskPhase::Canceled),
79 "exhausted" => Ok(TaskPhase::Exhausted),
80 _ => Err(ModelError::UnknownTaskPhase(trimmed.to_string())),
81 }
82 }
83}
84
85impl TaskPhase {
86 #[inline]
100 pub fn is_terminal(&self) -> bool {
101 matches!(
102 self,
103 TaskPhase::Succeeded
104 | TaskPhase::Failed
105 | TaskPhase::Timeout
106 | TaskPhase::Canceled
107 | TaskPhase::Exhausted
108 )
109 }
110
111 #[inline]
122 pub fn is_active(&self) -> bool {
123 matches!(self, TaskPhase::Pending | TaskPhase::Running)
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn active_and_terminal_classification_covers_every_phase() {
133 for phase in [TaskPhase::Pending, TaskPhase::Running] {
134 assert!(phase.is_active());
135 assert!(!phase.is_terminal());
136 }
137 for phase in [
138 TaskPhase::Succeeded,
139 TaskPhase::Failed,
140 TaskPhase::Timeout,
141 TaskPhase::Canceled,
142 TaskPhase::Exhausted,
143 ] {
144 assert!(!phase.is_active());
145 assert!(phase.is_terminal());
146 }
147 }
148
149 #[test]
150 fn display_parse_and_serde_roundtrip_every_phase() {
151 let cases = [
152 ("pending", TaskPhase::Pending),
153 ("running", TaskPhase::Running),
154 ("succeeded", TaskPhase::Succeeded),
155 ("failed", TaskPhase::Failed),
156 ("timeout", TaskPhase::Timeout),
157 ("canceled", TaskPhase::Canceled),
158 ("exhausted", TaskPhase::Exhausted),
159 ];
160 for (wire, phase) in cases {
161 assert_eq!(phase.to_string(), wire);
162 assert_eq!(wire.parse::<TaskPhase>().unwrap(), phase);
163 let json = serde_json::to_string(&phase).unwrap();
164 assert_eq!(serde_json::from_str::<TaskPhase>(&json).unwrap(), phase);
165 }
166 }
167
168 #[test]
169 fn parsing_normalizes_case_and_whitespace_and_rejects_unknown_values() {
170 assert_eq!("RUNNING".parse::<TaskPhase>().unwrap(), TaskPhase::Running);
171 assert_eq!(
172 " Succeeded ".parse::<TaskPhase>().unwrap(),
173 TaskPhase::Succeeded
174 );
175 let err = "bogus".parse::<TaskPhase>().unwrap_err();
176 assert!(matches!(err, ModelError::UnknownTaskPhase(_)));
177 }
178}