solti_model/domain/policy/
restart.rs1use serde::{Deserialize, Serialize};
6use std::str::FromStr;
7
8use crate::error::{ModelError, ModelResult};
9
10#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34#[serde(tag = "type", rename_all = "camelCase", deny_unknown_fields)]
35#[non_exhaustive]
36pub enum RestartPolicy {
37 #[default]
39 Never,
40 OnFailure,
42 #[serde(rename_all = "camelCase")]
44 Always {
45 #[serde(skip_serializing_if = "Option::is_none")]
49 interval_ms: Option<u64>,
50 },
51}
52
53impl RestartPolicy {
54 pub const fn always() -> Self {
67 RestartPolicy::Always { interval_ms: None }
68 }
69
70 pub const fn periodic(interval_ms: u64) -> Self {
83 RestartPolicy::Always {
84 interval_ms: Some(interval_ms),
85 }
86 }
87}
88
89impl FromStr for RestartPolicy {
90 type Err = ModelError;
91
92 fn from_str(s: &str) -> ModelResult<Self> {
93 let original = s.trim();
94 if original.is_empty() {
95 return Ok(RestartPolicy::Never);
96 }
97
98 let (head, rest) = match original.find(':') {
99 Some(pos) => (&original[..pos], Some(original[pos + 1..].trim())),
100 None => (original, None),
101 };
102
103 if head.eq_ignore_ascii_case("never") {
104 Ok(RestartPolicy::Never)
105 } else if head.eq_ignore_ascii_case("on-failure") || head.eq_ignore_ascii_case("failure") {
106 Ok(RestartPolicy::OnFailure)
107 } else if head.eq_ignore_ascii_case("always") {
108 let interval_ms = match rest {
109 None | Some("") => None,
110 Some(v) => {
111 let v = v.parse::<u64>().map_err(|_| {
112 ModelError::UnknownRestart(format!(
113 "invalid interval in '{}': must be u64",
114 original
115 ))
116 })?;
117 Some(v)
118 }
119 };
120 Ok(RestartPolicy::Always { interval_ms })
121 } else {
122 Err(ModelError::UnknownRestart(original.to_string()))
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::RestartPolicy;
130 use crate::error::ModelError;
131 use std::str::FromStr;
132
133 #[test]
134 fn parsing_accepts_policies_aliases_case_and_intervals() {
135 let cases = [
136 ("", RestartPolicy::Never),
137 ("never", RestartPolicy::Never),
138 (" NeVeR ", RestartPolicy::Never),
139 ("on-failure", RestartPolicy::OnFailure),
140 ("failure", RestartPolicy::OnFailure),
141 (" Failure ", RestartPolicy::OnFailure),
142 ("always", RestartPolicy::Always { interval_ms: None }),
143 (" ALWAYS ", RestartPolicy::Always { interval_ms: None }),
144 ("always:", RestartPolicy::Always { interval_ms: None }),
145 ("always: ", RestartPolicy::Always { interval_ms: None }),
146 (
147 "always:1000",
148 RestartPolicy::Always {
149 interval_ms: Some(1_000),
150 },
151 ),
152 (
153 " Always: 60000 ",
154 RestartPolicy::Always {
155 interval_ms: Some(60_000),
156 },
157 ),
158 ];
159 for (value, expected) in cases {
160 assert_eq!(RestartPolicy::from_str(value).unwrap(), expected);
161 }
162 }
163
164 #[test]
165 fn parsing_rejects_unknown_policy_and_invalid_interval() {
166 for value in ["always:not-a-number", "random"] {
167 assert!(matches!(
168 RestartPolicy::from_str(value),
169 Err(ModelError::UnknownRestart(_))
170 ));
171 }
172 }
173}