wasmer_deploy_util/
pretty_duration.rs

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