Skip to main content

tanzim_validate/
duration.rs

1use crate::error::{Error, ErrorKind};
2use crate::{Meta, Validator};
3use tanzim_value::{Value, ValueType};
4
5/// (`duration` feature) Accepts a human duration string (e.g. `"30s"`, `"5m"`, `"1h30m"`) and coerces it to an
6/// integer number of seconds (or milliseconds with [`Duration::millis`]).
7#[derive(Debug, Clone, Default)]
8pub struct Duration {
9    meta: Meta,
10    millis: bool,
11}
12
13impl Duration {
14    /// Attach human-facing metadata (name, description, examples, default, output conversion).
15    pub fn with_meta(mut self, meta: Meta) -> Self {
16        self.meta = meta;
17        self
18    }
19
20    pub fn new() -> Self {
21        Self::default()
22    }
23
24    /// Coerce to milliseconds instead of seconds.
25    pub fn millis(mut self) -> Self {
26        self.millis = true;
27        self
28    }
29}
30
31crate::impl_meta_methods!(Duration);
32
33impl Validator for Duration {
34    fn meta(&self) -> &Meta {
35        &self.meta
36    }
37
38    fn meta_mut(&mut self) -> &mut Meta {
39        &mut self.meta
40    }
41
42    fn check(&self, value: &mut Value) -> Result<(), Error> {
43        let text = match value {
44            Value::String(text) => text,
45            other => {
46                return Err(Error::new(ErrorKind::Type {
47                    expected: ValueType::String,
48                    found: other.type_name(),
49                }));
50            }
51        };
52
53        let parsed = match humantime::parse_duration(text) {
54            Ok(parsed) => parsed,
55            Err(_) => {
56                return Err(Error::new(ErrorKind::Format {
57                    expected: "duration",
58                }));
59            }
60        };
61
62        let amount = if self.millis {
63            parsed.as_millis()
64        } else {
65            parsed.as_secs() as u128
66        };
67        let coerced = match isize::try_from(amount) {
68            Ok(coerced) => coerced,
69            Err(_) => {
70                return Err(Error::new(ErrorKind::NotConvertible {
71                    target: ValueType::Int,
72                    found: ValueType::String,
73                }));
74            }
75        };
76
77        *value = Value::Int(coerced);
78        Ok(())
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn coerces_to_seconds() {
88        let mut value = Value::String("1h30m".into());
89        Duration::new().validate(&mut value).unwrap();
90        assert_eq!(value, Value::Int(5400));
91    }
92
93    #[test]
94    fn coerces_to_millis() {
95        let mut value = Value::String("250ms".into());
96        Duration::new().millis().validate(&mut value).unwrap();
97        assert_eq!(value, Value::Int(250));
98    }
99
100    #[test]
101    fn rejects_garbage() {
102        let mut value = Value::String("soon".into());
103        assert!(Duration::new().validate(&mut value).is_err());
104    }
105}