edge_schema/pretty_duration/
mod.rs

1mod reltime;
2
3use std::time::Duration;
4
5pub use self::reltime::parse_timestamp_or_relative_time;
6
7/// Duration that can be parsed and serde de/serialized from human-readable values.
8///
9/// Format:
10/// ( [NUMBER][UNIT] [WHITESPACE]* )+
11///
12/// Unit:
13/// * Seconds: s|sec|secs|seconds
14/// * Minutes: m|min|mins|minutes
15/// * Hours: h|hour|hours
16/// * Days: d|day|days
17///
18/// Examples:
19///
20/// * `30s`
21/// * `1m30s`
22/// * `1 day 2 hours 30 seconds`
23/// * ...
24///
25/// Similar to the `humantime` crate.
26/// A custom implementation is used to avoid additional dependencies.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct PrettyDuration(pub std::time::Duration);
29
30impl PrettyDuration {
31    pub const ZERO: PrettyDuration = PrettyDuration(Duration::ZERO);
32    pub const MAX: PrettyDuration = PrettyDuration(Duration::MAX);
33
34    pub fn new(duration: std::time::Duration) -> Self {
35        Self(duration)
36    }
37
38    pub fn as_duration(&self) -> std::time::Duration {
39        self.0
40    }
41
42    pub fn from_secs(secs: u64) -> Self {
43        Self(std::time::Duration::from_secs(secs))
44    }
45
46    pub fn from_mins(mins: u64) -> Self {
47        Self(std::time::Duration::from_secs(mins * 60))
48    }
49
50    pub fn from_hours(hours: u64) -> Self {
51        Self(std::time::Duration::from_secs(hours * 60 * 60))
52    }
53}
54
55impl std::ops::Deref for PrettyDuration {
56    type Target = std::time::Duration;
57
58    fn deref(&self) -> &Self::Target {
59        &self.0
60    }
61}
62
63impl From<std::time::Duration> for PrettyDuration {
64    fn from(duration: std::time::Duration) -> Self {
65        Self::new(duration)
66    }
67}
68
69impl From<PrettyDuration> for std::time::Duration {
70    fn from(duration: PrettyDuration) -> Self {
71        duration.0
72    }
73}
74
75impl std::fmt::Display for PrettyDuration {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        const DAY: u64 = 60 * 60 * 24;
78        const HOUR: u64 = 60 * 60;
79
80        let secs = self.0.as_secs();
81
82        let days = secs / DAY;
83        if days > 0 {
84            write!(f, "{}d", days)?;
85        }
86        let secs = secs % DAY;
87
88        let hours = secs / HOUR;
89        if hours > 0 {
90            write!(f, "{}h", hours)?;
91        }
92        let secs = secs % HOUR;
93
94        let mins = secs / 60;
95        if mins > 0 {
96            write!(f, "{}m", mins)?;
97        }
98        let secs = secs % 60;
99        if secs > 0 {
100            write!(f, "{}s", secs)?;
101        }
102
103        Ok(())
104    }
105}
106
107#[derive(Debug)]
108pub struct PrettyDurationParseError {
109    value: String,
110    message: String,
111}
112
113impl std::fmt::Display for PrettyDurationParseError {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        write!(f, "Invalid time spec '{}': {}", self.value, self.message)
116    }
117}
118
119impl std::error::Error for PrettyDurationParseError {}
120
121impl std::str::FromStr for PrettyDuration {
122    type Err = PrettyDurationParseError;
123
124    fn from_str(mut input: &str) -> Result<Self, Self::Err> {
125        let mut seconds = 0;
126
127        loop {
128            input = input.strip_prefix(' ').unwrap_or(input);
129            if input.is_empty() {
130                break;
131            }
132
133            let nums = input.chars().take_while(|x| x.is_ascii_digit()).count();
134            if nums < 1 {
135                return Err(PrettyDurationParseError {
136                    message: "must start with a number".to_string(),
137                    value: input.to_string(),
138                });
139            }
140
141            let number = &input[..nums]
142                .parse::<u64>()
143                .map_err(|e| PrettyDurationParseError {
144                    message: format!("invalid number: {}", e),
145                    value: input.to_string(),
146                })?;
147
148            input = &input[nums..];
149            let chars = input
150                .chars()
151                .take_while(|x| x.is_ascii_alphabetic())
152                .count();
153            let unit = &input[..chars];
154            input = &input[chars..];
155
156            let scale = match unit {
157                "s" | "sec" | "secs" | "seconds" => 1,
158                "m" | "min" | "mins" | "minutes" => 60,
159                "h" | "hour" | "hours" => 60 * 60,
160                "d" | "day" | "days" => 60 * 60 * 24,
161                _ => {
162                    return Err(PrettyDurationParseError {
163                        message: "unknown unit".to_string(),
164                        value: input.to_string(),
165                    });
166                }
167            };
168
169            seconds += number * scale;
170        }
171
172        let dur = std::time::Duration::from_secs(seconds);
173
174        Ok(Self(dur))
175    }
176}
177
178impl serde::Serialize for PrettyDuration {
179    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
180    where
181        S: serde::Serializer,
182    {
183        self.to_string().serialize(serializer)
184    }
185}
186
187impl<'de> serde::Deserialize<'de> for PrettyDuration {
188    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
189    where
190        D: serde::Deserializer<'de>,
191    {
192        let s = String::deserialize(deserializer)?;
193        s.parse().map_err(serde::de::Error::custom)
194    }
195}
196
197impl schemars::JsonSchema for PrettyDuration {
198    fn schema_name() -> String {
199        "StringWebcPackageIdent".to_string()
200    }
201
202    fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
203        schemars::schema::Schema::Object(schemars::schema::SchemaObject {
204            instance_type: Some(schemars::schema::InstanceType::String.into()),
205            ..Default::default()
206        })
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use std::time::Duration;
213
214    use super::*;
215
216    #[test]
217    fn test_pretty_duration_constructors() {
218        assert_eq!(
219            PrettyDuration::from_secs(1).as_duration(),
220            Duration::from_secs(1)
221        );
222        assert_eq!(
223            PrettyDuration::from_mins(1).as_duration(),
224            Duration::from_secs(60)
225        );
226        assert_eq!(
227            PrettyDuration::from_hours(1).as_duration(),
228            Duration::from_secs(60 * 60)
229        );
230    }
231
232    #[test]
233    fn test_pretty_duration_parse() {
234        let cases = &[
235            ("1s", Duration::from_secs(1), "1s"),
236            ("10s", Duration::from_secs(10), "10s"),
237            ("59s", Duration::from_secs(59), "59s"),
238            ("60s", Duration::from_secs(60), "1m"),
239            ("1m", Duration::from_secs(60), "1m"),
240            ("11m", Duration::from_secs(60) * 11, "11m"),
241            ("60m", Duration::from_secs(60) * 60, "1h"),
242            ("1h", Duration::from_secs(60) * 60, "1h"),
243            ("11h", Duration::from_secs(60) * 60 * 11, "11h"),
244            ("1h1m", Duration::from_secs(61) * 60, "1h1m"),
245            ("1h1m1s", Duration::from_secs(61 * 60 + 1), "1h1m1s"),
246        ];
247
248        for (index, (input, duration, output)) in cases.iter().enumerate() {
249            eprintln!("test case {index}: {input} => {duration:?} => {output}");
250            let p = input.parse::<PrettyDuration>().unwrap();
251            assert_eq!(p, PrettyDuration::new(*duration));
252            assert_eq!(p.to_string(), output.to_string());
253        }
254    }
255
256    #[test]
257    fn test_pretty_duration_serde() {
258        let dur = PrettyDuration::from_secs(1);
259        let json = serde_json::to_string(&dur).unwrap();
260        assert_eq!(json, "\"1s\"");
261
262        let dur2: PrettyDuration = serde_json::from_str(&json).unwrap();
263        assert_eq!(dur, dur2);
264    }
265}