Skip to main content

rustev_contract/
time.rs

1//! Time as a value (spec 002, 3.4.4). Nothing in Rustev's contract or core
2//! reads a clock; the evaluation time is supplied.
3
4use std::fmt;
5
6use serde::{Deserialize, Deserializer, Serialize};
7
8/// The largest timestamp: 9999-12-31T23:59:59.999Z.
9pub const MAX_TIMESTAMP_MS: i64 = 253_402_300_799_999;
10
11/// Milliseconds since the Unix epoch, UTC, in `0..=MAX_TIMESTAMP_MS`.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
13#[serde(transparent)]
14pub struct Timestamp(i64);
15
16impl schemars::JsonSchema for Timestamp {
17    fn schema_name() -> std::borrow::Cow<'static, str> {
18        "Timestamp".into()
19    }
20
21    /// The range the parser enforces.
22    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
23        schemars::json_schema!({
24            "type": "integer",
25            "minimum": 0,
26            "maximum": MAX_TIMESTAMP_MS
27        })
28    }
29}
30
31/// A non-negative span in milliseconds.
32#[derive(
33    Debug,
34    Clone,
35    Copy,
36    PartialEq,
37    Eq,
38    PartialOrd,
39    Ord,
40    Hash,
41    Serialize,
42    Deserialize,
43    schemars::JsonSchema,
44)]
45#[serde(transparent)]
46pub struct DurationMs(pub u64);
47
48/// A timestamp outside the representable range, or an arithmetic result
49/// that would leave it.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct TimeRangeError;
52
53impl fmt::Display for TimeRangeError {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        write!(f, "timestamp outside 0..={MAX_TIMESTAMP_MS} ms")
56    }
57}
58
59impl std::error::Error for TimeRangeError {}
60
61impl Timestamp {
62    /// A validated timestamp.
63    pub fn from_ms(ms: i64) -> Result<Self, TimeRangeError> {
64        if (0..=MAX_TIMESTAMP_MS).contains(&ms) {
65            Ok(Self(ms))
66        } else {
67            Err(TimeRangeError)
68        }
69    }
70
71    pub fn as_ms(self) -> i64 {
72        self.0
73    }
74
75    /// `self - earlier`, refused when negative.
76    pub fn since(self, earlier: Timestamp) -> Result<DurationMs, TimeRangeError> {
77        if self.0 < earlier.0 {
78            return Err(TimeRangeError);
79        }
80        // Both are in range, so the difference fits.
81        Ok(DurationMs((self.0 - earlier.0) as u64))
82    }
83
84    pub fn checked_add(self, d: DurationMs) -> Result<Timestamp, TimeRangeError> {
85        let d = i64::try_from(d.0).map_err(|_| TimeRangeError)?;
86        Timestamp::from_ms(self.0.checked_add(d).ok_or(TimeRangeError)?)
87    }
88
89    pub fn checked_sub(self, d: DurationMs) -> Result<Timestamp, TimeRangeError> {
90        let d = i64::try_from(d.0).map_err(|_| TimeRangeError)?;
91        Timestamp::from_ms(self.0.checked_sub(d).ok_or(TimeRangeError)?)
92    }
93}
94
95impl<'de> Deserialize<'de> for Timestamp {
96    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
97        let ms = i64::deserialize(d)?;
98        Timestamp::from_ms(ms).map_err(serde::de::Error::custom)
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn range_is_enforced_at_both_ends() {
108        assert!(Timestamp::from_ms(0).is_ok());
109        assert!(Timestamp::from_ms(MAX_TIMESTAMP_MS).is_ok());
110        assert!(Timestamp::from_ms(-1).is_err());
111        assert!(Timestamp::from_ms(MAX_TIMESTAMP_MS + 1).is_err());
112        assert!(serde_json::from_str::<Timestamp>("-1").is_err());
113        assert!(serde_json::from_str::<Timestamp>("1.5").is_err());
114    }
115
116    #[test]
117    fn arithmetic_is_checked() {
118        let t = Timestamp::from_ms(1_000).unwrap();
119        assert_eq!(
120            t.since(Timestamp::from_ms(400).unwrap()),
121            Ok(DurationMs(600))
122        );
123        assert!(t.since(Timestamp::from_ms(1_001).unwrap()).is_err());
124        assert!(t.checked_sub(DurationMs(1_001)).is_err());
125        assert!(
126            Timestamp::from_ms(MAX_TIMESTAMP_MS)
127                .unwrap()
128                .checked_add(DurationMs(1))
129                .is_err()
130        );
131        assert!(t.checked_add(DurationMs(u64::MAX)).is_err());
132    }
133}