1use schemars::JsonSchema;
2use serde::{Deserialize, Deserializer, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
13#[serde(untagged)]
14pub enum StringOrBool {
15 Bool(bool),
16 String(String),
17}
18
19impl StringOrBool {
20 pub fn as_bool(&self) -> bool {
23 match self {
24 StringOrBool::Bool(b) => *b,
25 StringOrBool::String(s) => matches!(s.trim(), "true" | "1"),
26 }
27 }
28
29 pub fn as_str(&self) -> &str {
31 match self {
32 StringOrBool::Bool(true) => "true",
33 StringOrBool::Bool(false) => "false",
34 StringOrBool::String(s) => s,
35 }
36 }
37
38 pub fn is_template(&self) -> bool {
40 matches!(self, StringOrBool::String(s) if s.contains('{'))
41 }
42
43 pub fn try_evaluates_to_true(
58 &self,
59 render: impl Fn(&str) -> anyhow::Result<String>,
60 ) -> anyhow::Result<bool> {
61 match self {
62 StringOrBool::Bool(b) => Ok(*b),
63 StringOrBool::String(s) => {
64 let rendered = render(s)?;
65 Ok(matches!(rendered.trim(), "true" | "1"))
66 }
67 }
68 }
69}
70
71impl Default for StringOrBool {
72 fn default() -> Self {
73 StringOrBool::Bool(false)
74 }
75}
76
77pub fn evaluate_if_condition(
97 condition: Option<&str>,
98 label: &str,
99 render: impl Fn(&str) -> anyhow::Result<String>,
100) -> anyhow::Result<bool> {
101 use anyhow::Context as _;
102 let Some(template) = condition else {
103 return Ok(true);
104 };
105 if template.is_empty() {
106 return Ok(true);
107 }
108 let rendered = render(template).with_context(|| {
109 format!("{label}: `if` template render failed (expression: {template})")
110 })?;
111 let trimmed = rendered.trim();
112 let falsy = matches!(trimmed, "" | "false" | "0" | "no");
113 Ok(!falsy)
114}
115
116pub(crate) fn deserialize_string_or_bool_opt<'de, D>(
118 deserializer: D,
119) -> Result<Option<StringOrBool>, D::Error>
120where
121 D: Deserializer<'de>,
122{
123 use serde::de::{self, Visitor};
124
125 struct StringOrBoolVisitor;
126
127 impl<'de> Visitor<'de> for StringOrBoolVisitor {
128 type Value = Option<StringOrBool>;
129
130 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 f.write_str("a bool, a string, or null")
132 }
133
134 fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
135 Ok(Some(StringOrBool::Bool(v)))
136 }
137
138 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
139 Ok(Some(StringOrBool::String(v.to_owned())))
140 }
141
142 fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
143 Ok(Some(StringOrBool::String(v)))
144 }
145
146 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
147 Ok(None)
148 }
149
150 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
151 Ok(None)
152 }
153 }
154
155 deserializer.deserialize_any(StringOrBoolVisitor)
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
164pub struct HumanDuration(
165 #[serde(serialize_with = "serialize_human_duration")] pub std::time::Duration,
166);
167
168impl HumanDuration {
169 pub fn duration(&self) -> std::time::Duration {
171 self.0
172 }
173
174 pub fn as_humantime_string(&self) -> String {
178 let total_secs = self.0.as_secs();
179 if total_secs == 0 {
180 return format!("{}ms", self.0.as_millis());
182 }
183 let hours = total_secs / 3600;
184 let mins = (total_secs % 3600) / 60;
185 let secs = total_secs % 60;
186 let mut out = String::new();
187 if hours > 0 {
188 out.push_str(&format!("{hours}h"));
189 }
190 if mins > 0 {
191 out.push_str(&format!("{mins}m"));
192 }
193 if secs > 0 || out.is_empty() {
194 out.push_str(&format!("{secs}s"));
195 }
196 out
197 }
198}
199
200impl<'de> Deserialize<'de> for HumanDuration {
201 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
202 where
203 D: Deserializer<'de>,
204 {
205 use serde::de::{self, Visitor};
206
207 struct DurVisitor;
208
209 impl<'de> Visitor<'de> for DurVisitor {
210 type Value = HumanDuration;
211
212 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 f.write_str(
214 "a duration string with unit suffix (e.g. \"10m\", \"15s\", \"1h30m\", \"500ms\")",
215 )
216 }
217
218 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
219 parse_humantime_duration(v)
220 .map(HumanDuration)
221 .map_err(E::custom)
222 }
223
224 fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
225 self.visit_str(&v)
226 }
227 }
228
229 deserializer.deserialize_str(DurVisitor)
230 }
231}
232
233fn serialize_human_duration<S: serde::Serializer>(
234 d: &std::time::Duration,
235 serializer: S,
236) -> Result<S::Ok, S::Error> {
237 serializer.serialize_str(&HumanDuration(*d).as_humantime_string())
238}
239
240pub(super) fn parse_humantime_duration(input: &str) -> Result<std::time::Duration, String> {
244 let s = input.trim();
245 if s.is_empty() {
246 return Err("empty duration string".to_string());
247 }
248 let mut total = std::time::Duration::ZERO;
249 let mut number_buf = String::new();
250 let mut had_any = false;
251 let mut iter = s.chars().peekable();
252 while let Some(&c) = iter.peek() {
253 if c.is_whitespace() {
254 iter.next();
255 continue;
256 }
257 if c.is_ascii_digit() {
258 number_buf.push(c);
259 iter.next();
260 continue;
261 }
262 if number_buf.is_empty() {
263 return Err(format!("expected digit before unit in '{input}'"));
264 }
265 let mut unit = String::new();
267 unit.push(c);
268 iter.next();
269 if let Some(&next) = iter.peek()
270 && unit == "m"
271 && next == 's'
272 {
273 unit.push('s');
274 iter.next();
275 }
276 let n: u64 = number_buf
277 .parse()
278 .map_err(|e| format!("invalid number '{number_buf}' in '{input}': {e}"))?;
279 let segment = match unit.as_str() {
280 "ms" => std::time::Duration::from_millis(n),
281 "s" => std::time::Duration::from_secs(n),
282 "m" => std::time::Duration::from_secs(n * 60),
283 "h" => std::time::Duration::from_secs(n * 3600),
284 "d" => std::time::Duration::from_secs(n * 86_400),
285 other => return Err(format!("unknown duration unit '{other}' in '{input}'")),
286 };
287 total += segment;
288 number_buf.clear();
289 had_any = true;
290 }
291 if !number_buf.is_empty() {
292 return Err(format!(
293 "trailing number '{number_buf}' without a unit in '{input}'"
294 ));
295 }
296 if !had_any {
297 return Err(format!("no duration components found in '{input}'"));
298 }
299 Ok(total)
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
310#[serde(transparent)]
311pub struct StringOrU32(#[serde(deserialize_with = "deserialize_u32_from_string_or_int")] pub u32);
312
313impl StringOrU32 {
314 pub fn value(&self) -> u32 {
316 self.0
317 }
318}
319
320fn deserialize_u32_from_string_or_int<'de, D>(deserializer: D) -> Result<u32, D::Error>
322where
323 D: Deserializer<'de>,
324{
325 use serde::de::{self, Visitor};
326
327 struct U32Visitor;
328
329 impl<'de> Visitor<'de> for U32Visitor {
330 type Value = u32;
331
332 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333 f.write_str("a u32 integer or a string parseable as octal/decimal (e.g. 18, \"0o022\", \"022\")")
334 }
335
336 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
337 u32::try_from(v).map_err(|_| E::custom(format!("value {v} does not fit in u32")))
338 }
339
340 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
341 u32::try_from(v).map_err(|_| E::custom(format!("value {v} does not fit in u32")))
342 }
343
344 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
345 let trimmed = v.trim();
346 if let Some(rest) = trimmed
347 .strip_prefix("0o")
348 .or_else(|| trimmed.strip_prefix("0O"))
349 {
350 return u32::from_str_radix(rest, 8)
351 .map_err(|e| E::custom(format!("invalid octal '{v}': {e}")));
352 }
353 if trimmed.starts_with('0') && trimmed.len() > 1 {
356 return u32::from_str_radix(trimmed, 8)
357 .map_err(|e| E::custom(format!("invalid octal '{v}': {e}")));
358 }
359 trimmed
360 .parse::<u32>()
361 .map_err(|e| E::custom(format!("invalid u32 '{v}': {e}")))
362 }
363
364 fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
365 self.visit_str(&v)
366 }
367 }
368
369 deserializer.deserialize_any(U32Visitor)
370}
371
372pub(super) fn deserialize_string_or_vec_opt<'de, D>(
375 deserializer: D,
376) -> Result<Option<Vec<String>>, D::Error>
377where
378 D: Deserializer<'de>,
379{
380 use serde::de::{self, Visitor};
381
382 struct StringOrVecVisitor;
383
384 impl<'de> Visitor<'de> for StringOrVecVisitor {
385 type Value = Option<Vec<String>>;
386
387 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388 f.write_str("a string, a list of strings, or null")
389 }
390
391 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
392 Ok(Some(vec![v.to_owned()]))
393 }
394
395 fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
396 Ok(Some(vec![v]))
397 }
398
399 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
400 let mut items = Vec::new();
401 while let Some(item) = seq.next_element::<String>()? {
402 items.push(item);
403 }
404 Ok(Some(items))
405 }
406
407 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
408 Ok(None)
409 }
410
411 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
412 Ok(None)
413 }
414 }
415
416 deserializer.deserialize_any(StringOrVecVisitor)
417}