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