Skip to main content

dinio_sdk/
time.rs

1//! Time bounds for queries that take `?from=`/`?to=` (audit log, statistics). Core reads
2//! them as ISO 8601 with an offset; the SDK writes UTC with second precision.
3
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use crate::error::{Error, Result};
7
8/// A point in time for a `from`/`to` filter: a [`SystemTime`], or text Core parses
9/// itself (ISO 8601 with an offset, e.g. `2026-10-27T09:00:00+01:00`).
10///
11/// # Examples
12///
13/// ```
14/// use std::time::{Duration, SystemTime};
15/// use dinio_sdk::Timestamp;
16///
17/// let week_ago: Timestamp = (SystemTime::now() - Duration::from_secs(7 * 86_400)).into();
18/// let fixed: Timestamp = "2026-10-27T00:00:00Z".into();
19/// # let _ = (week_ago, fixed);
20/// ```
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum Timestamp {
23    /// Sent as UTC, whole seconds.
24    At(SystemTime),
25    /// Sent as given; Core answers [`ErrorCode::Validation`](crate::ErrorCode::Validation)
26    /// if it is not ISO 8601 with an offset.
27    Text(String),
28}
29
30impl From<SystemTime> for Timestamp {
31    fn from(time: SystemTime) -> Self {
32        Self::At(time)
33    }
34}
35
36impl From<String> for Timestamp {
37    fn from(text: String) -> Self {
38        Self::Text(text)
39    }
40}
41
42impl From<&str> for Timestamp {
43    fn from(text: &str) -> Self {
44        Self::Text(text.to_owned())
45    }
46}
47
48impl Timestamp {
49    /// The query value.
50    pub(crate) fn to_query(&self) -> Result<String> {
51        match self {
52            Self::Text(text) => Ok(text.clone()),
53            Self::At(time) => {
54                let secs = time.duration_since(UNIX_EPOCH).map_err(|_| {
55                    Error::InvalidInput("from/to before 1970 is not supported".into())
56                })?;
57                Ok(rfc3339(secs.as_secs()))
58            }
59        }
60    }
61}
62
63/// `2026-10-27T09:14:03Z` for Unix seconds.
64fn rfc3339(secs: u64) -> String {
65    let days = (secs / 86_400) as i64;
66    let rem = secs % 86_400;
67    let (y, m, d) = civil_from_days(days);
68    format!(
69        "{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z",
70        rem / 3_600,
71        rem % 3_600 / 60,
72        rem % 60
73    )
74}
75
76/// Days since 1970-01-01 → (year, month, day), proleptic Gregorian (H. Hinnant).
77fn civil_from_days(days: i64) -> (i64, u32, u32) {
78    let z = days + 719_468;
79    let era = z.div_euclid(146_097);
80    let doe = z.rem_euclid(146_097);
81    let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
82    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
83    let mp = (5 * doy + 2) / 153;
84    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
85    let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
86    let y = yoe + era * 400 + i64::from(m <= 2);
87    (y, m, d)
88}
89
90#[cfg(test)]
91mod tests {
92    use std::time::Duration;
93
94    use super::*;
95
96    #[test]
97    fn formats_utc_seconds() {
98        assert_eq!(rfc3339(0), "1970-01-01T00:00:00Z");
99        assert_eq!(rfc3339(951_782_400), "2000-02-29T00:00:00Z");
100        assert_eq!(rfc3339(1_793_093_643), "2026-10-27T09:34:03Z");
101        let at = Timestamp::At(UNIX_EPOCH + Duration::from_millis(1_500));
102        assert_eq!(at.to_query().unwrap(), "1970-01-01T00:00:01Z");
103        assert_eq!(Timestamp::from("x").to_query().unwrap(), "x");
104    }
105}