architect_api/utils/
duration.rs

1//! Utility functions for working with durations
2
3use crate::json_schema_is_string;
4use anyhow::{anyhow, bail, Result};
5use chrono::{DateTime, Duration, Utc};
6use derive_more::{Deref, DerefMut, From};
7use serde::{Deserialize, Serialize};
8use serde_with::serde_conv;
9use std::str::FromStr;
10
11serde_conv!(pub DurationAsStr, Duration, format_duration, parse_duration);
12
13serde_conv!(
14    pub NonZeroDurationAsStr,
15    std::time::Duration,
16    format_nonzero_duration,
17    parse_nonzero_duration
18);
19
20pub fn format_nonzero_duration(dur: &std::time::Duration) -> String {
21    let secs = dur.as_secs();
22    let nanos = dur.subsec_nanos();
23    format!("{}.{:09}s", secs, nanos)
24}
25
26fn parse_nonzero_duration(s: String) -> Result<std::time::Duration> {
27    let dur = parse_duration(&s)?;
28    if dur.is_zero() {
29        bail!("duration must be non-zero");
30    }
31    Ok(dur.to_std()?)
32}
33
34json_schema_is_string!(DurationAsStr);
35json_schema_is_string!(NonZeroDurationAsStr);
36
37// CR alee: deprecating in favor of DurationAsStr
38#[derive(
39    Debug,
40    Clone,
41    Copy,
42    From,
43    Deref,
44    DerefMut,
45    PartialEq,
46    Eq,
47    PartialOrd,
48    Ord,
49    Serialize,
50    Deserialize,
51)]
52#[serde(transparent)]
53pub struct HumanDuration(
54    #[serde(
55        serialize_with = "serialize_duration",
56        deserialize_with = "deserialize_duration"
57    )]
58    pub Duration,
59);
60
61impl FromStr for HumanDuration {
62    type Err = anyhow::Error;
63
64    fn from_str(s: &str) -> Result<Self> {
65        parse_duration(s).map(HumanDuration)
66    }
67}
68
69json_schema_is_string!(HumanDuration);
70
71/// Helper struct to parse from either an absolute ISO 8601 datetime,
72/// or some duration relative to now (e.g. +1h, -3d, etc.)
73#[derive(Debug, Clone)]
74pub enum AbsoluteOrRelativeTime {
75    Absolute(DateTime<Utc>),
76    RelativeFuture(Duration),
77    RelativePast(Duration),
78    Now,
79}
80
81impl AbsoluteOrRelativeTime {
82    pub fn resolve_to(&self, now: DateTime<Utc>) -> DateTime<Utc> {
83        match self {
84            Self::Absolute(dt) => *dt,
85            Self::RelativeFuture(d) => now + *d,
86            Self::RelativePast(d) => now - *d,
87            Self::Now => now,
88        }
89    }
90
91    pub fn resolve(&self) -> DateTime<Utc> {
92        self.resolve_to(Utc::now())
93    }
94}
95
96impl FromStr for AbsoluteOrRelativeTime {
97    type Err = anyhow::Error;
98
99    fn from_str(s: &str) -> Result<Self> {
100        if s == "now" {
101            Ok(Self::Now)
102        } else if let Some(rest) = s.strip_prefix('+') {
103            Ok(Self::RelativeFuture(parse_duration(rest)?))
104        } else if s.starts_with('_') || s.starts_with("~") || s.starts_with('-') {
105            // CR-someday alee: clap is actually a bad library in a lot of ways, including
106            // not understanding a leading '-' in argument value following a flag
107            Ok(Self::RelativePast(parse_duration(&s[1..])?))
108        } else {
109            Ok(Self::Absolute(DateTime::from_str(s)?))
110        }
111    }
112}
113
114// TODO: pick a more elegant format rather than dumb seconds
115fn format_duration(dur: &Duration) -> String {
116    let secs = dur.num_milliseconds() as f64 / 1000.;
117    format!("{}s", secs)
118}
119
120/// Parse a duration string into a `chrono::Duration`.
121///
122/// A valid duration string is an integer or float followed by a
123/// suffix. Supported suffixes are,
124///
125/// - d: days float
126/// - h: hours float
127/// - m: minutes float
128/// - s: seconds float
129/// - ms: milliseconds int
130/// - us: microseconds int
131/// - ns: nanoseconds int
132///
133/// e.g. 27ns, 1.7d, 22.2233h, 47.3m, ...
134pub fn parse_duration(s: &str) -> Result<Duration> {
135    if s.ends_with("ns") {
136        let s = s.strip_suffix("ns").unwrap().trim();
137        let n = s.parse::<i64>().map_err(|e| anyhow!(e.to_string()))?;
138        Ok(Duration::nanoseconds(n))
139    } else if s.ends_with("us") {
140        let s = s.strip_suffix("us").unwrap().trim();
141        let n = s.parse::<i64>().map_err(|e| anyhow!(e.to_string()))?;
142        Ok(Duration::microseconds(n))
143    } else if s.ends_with("ms") {
144        let s = s.strip_suffix("ms").unwrap().trim();
145        let n = s.parse::<i64>().map_err(|e| anyhow!(e.to_string()))?;
146        Ok(Duration::milliseconds(n))
147    } else if s.ends_with("s") {
148        let s = s.strip_suffix("s").unwrap().trim();
149        let f = s.parse::<f64>().map_err(|e| anyhow!(e.to_string()))?;
150        Ok(Duration::nanoseconds((f * 1e9).trunc() as i64))
151    } else if s.ends_with("m") {
152        let s = s.strip_suffix("m").unwrap().trim();
153        let f = s.parse::<f64>().map_err(|e| anyhow!(e.to_string()))?;
154        Ok(Duration::nanoseconds((f * 60. * 1e9).trunc() as i64))
155    } else if s.ends_with("h") {
156        let s = s.strip_suffix("h").unwrap().trim();
157        let f = s.parse::<f64>().map_err(|e| anyhow!(e.to_string()))?;
158        Ok(Duration::nanoseconds((f * 3600. * 1e9).trunc() as i64))
159    } else if s.ends_with("d") {
160        let s = s.strip_suffix("d").unwrap().trim();
161        let f = s.parse::<f64>().map_err(|e| anyhow!(e.to_string()))?;
162        Ok(Duration::nanoseconds((f * 86400. * 1e9).trunc() as i64))
163    } else {
164        Err(anyhow!("expected a suffix ns, us, ms, s, m, h, d"))
165    }
166}
167
168/// a serde visitor for `chrono::Duration`
169pub struct DurationVisitor;
170
171impl serde::de::Visitor<'_> for DurationVisitor {
172    type Value = Duration;
173
174    fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
175        write!(f, "expecting a string")
176    }
177
178    fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
179    where
180        E: serde::de::Error,
181    {
182        parse_duration(s).map_err(|e| E::custom(e.to_string()))
183    }
184}
185
186/// A serde deserialize function for `chrono::Duration`
187///
188/// using `parse_duration`
189pub fn deserialize_duration<'de, D>(d: D) -> Result<Duration, D::Error>
190where
191    D: serde::Deserializer<'de>,
192{
193    d.deserialize_str(DurationVisitor)
194}
195
196pub fn deserialize_duration_opt<'de, D>(d: D) -> Result<Option<Duration>, D::Error>
197where
198    D: serde::Deserializer<'de>,
199{
200    let s = Option::<String>::deserialize(d)?;
201    match s {
202        Some(s) => Ok(Some(parse_duration(&s).map_err(serde::de::Error::custom)?)),
203        None => Ok(None),
204    }
205}
206
207/// A serde serializer function for `chrono::Duration`
208///
209/// that writes the duration as an f64 number of seconds followed by
210/// the s suffix.
211pub fn serialize_duration<S>(d: &Duration, s: S) -> Result<S::Ok, S::Error>
212where
213    S: serde::Serializer,
214{
215    let secs = d.num_milliseconds() as f64 / 1000.;
216    s.serialize_str(&format!("{}s", secs))
217}
218
219pub fn serialize_duration_opt<S>(d: &Option<Duration>, s: S) -> Result<S::Ok, S::Error>
220where
221    S: serde::Serializer,
222{
223    match d {
224        Some(d) => {
225            let secs = d.num_milliseconds() as f64 / 1000.;
226            s.serialize_some(&format!("{}s", secs))
227        }
228        None => s.serialize_none(),
229    }
230}