tenderdash_proto/serializers/
timestamp.rs

1//! Serialize/deserialize Timestamp type from and into string:
2#[cfg(not(feature = "std"))]
3use core::fmt::{self, Debug};
4#[cfg(feature = "std")]
5use std::fmt::{self, Debug};
6
7use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _, ser::Error};
8use time::{
9    OffsetDateTime, format_description::well_known::Rfc3339 as Rfc3339Format, macros::offset,
10};
11
12use crate::{google::protobuf::Timestamp, prelude::*};
13
14/// Helper struct to serialize and deserialize Timestamp into an
15/// RFC3339-compatible string This is required because the serde `with`
16/// attribute is only available to fields of a struct but not the whole struct.
17#[derive(Debug, Serialize, Deserialize)]
18#[serde(transparent)]
19pub struct Rfc3339(#[serde(with = "crate::serializers::timestamp")] Timestamp);
20
21impl From<Timestamp> for Rfc3339 {
22    fn from(value: Timestamp) -> Self {
23        Rfc3339(value)
24    }
25}
26impl From<Rfc3339> for Timestamp {
27    fn from(value: Rfc3339) -> Self {
28        value.0
29    }
30}
31
32// Code moved to crate::time, but kept here for compatibility
33#[deprecated = "use crate::prelude::FromMillis instead"]
34pub use crate::time::FromMillis as FromMilis;
35#[deprecated = "use crate::prelude::ToMillis instead"]
36pub use crate::time::ToMillis as ToMilis;
37
38/// Deserialize string into Timestamp
39pub fn deserialize<'de, D>(deserializer: D) -> Result<Timestamp, D::Error>
40where
41    D: Deserializer<'de>,
42{
43    let value_string = String::deserialize(deserializer)?;
44    let t = OffsetDateTime::parse(&value_string, &Rfc3339Format).map_err(D::Error::custom)?;
45    let t = t.to_offset(offset!(UTC));
46    if !matches!(t.year(), 1..=9999) {
47        return Err(D::Error::custom("date is out of range"));
48    }
49    let seconds = t.unix_timestamp();
50    // Safe to convert to i32 because .nanosecond()
51    // is guaranteed to return a value in 0..1_000_000_000 range.
52    let nanos = t.nanosecond() as i32;
53    Ok(Timestamp { seconds, nanos })
54}
55
56/// Serialize from Timestamp into string
57pub fn serialize<S>(value: &Timestamp, serializer: S) -> Result<S::Ok, S::Error>
58where
59    S: Serializer,
60{
61    if value.nanos < 0 || value.nanos > 999_999_999 {
62        return Err(S::Error::custom("invalid nanoseconds in time"));
63    }
64    let total_nanos = value.seconds as i128 * 1_000_000_000 + value.nanos as i128;
65    let datetime = OffsetDateTime::from_unix_timestamp_nanos(total_nanos)
66        .map_err(|_| S::Error::custom("invalid time"))?;
67    to_rfc3339_nanos(datetime).serialize(serializer)
68}
69
70/// Serialization helper for converting an [`OffsetDateTime`] object to a
71/// string.
72///
73/// This reproduces the behavior of Go's `time.RFC3339Nano` format,
74/// ie. a RFC3339 date-time with left-padded subsecond digits without
75///     trailing zeros and no trailing dot.
76pub fn to_rfc3339_nanos(t: OffsetDateTime) -> String {
77    // Can't use OffsetDateTime::format because the feature enabling it
78    // currently requires std (https://github.com/time-rs/time/issues/400)
79
80    // Preallocate enough string capacity to fit the shortest possible form,
81    // yyyy-mm-ddThh:mm:ssZ
82    let mut buf = String::with_capacity(20);
83
84    fmt_as_rfc3339_nanos(t, &mut buf).unwrap();
85
86    buf
87}
88
89/// Helper for formatting an [`OffsetDateTime`] value.
90///
91/// This function can be used to efficiently format date-time values
92/// in [`Display`] or [`Debug`] implementations.
93///
94/// The format reproduces Go's `time.RFC3339Nano` format,
95/// ie. a RFC3339 date-time with left-padded subsecond digits without
96///     trailing zeros and no trailing dot.
97///
98/// [`Display`]: fmt::Display
99/// [`Debug`]: fmt::Debug
100pub fn fmt_as_rfc3339_nanos(t: OffsetDateTime, f: &mut impl fmt::Write) -> fmt::Result {
101    let t = t.to_offset(offset!(UTC));
102    let nanos = t.nanosecond();
103    if nanos == 0 {
104        write!(
105            f,
106            "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z",
107            year = t.year(),
108            month = t.month() as u8,
109            day = t.day(),
110            hour = t.hour(),
111            minute = t.minute(),
112            second = t.second(),
113        )
114    } else {
115        let mut secfrac = nanos;
116        let mut secfrac_width = 9;
117        while secfrac % 10 == 0 {
118            secfrac /= 10;
119            secfrac_width -= 1;
120        }
121        write!(
122            f,
123            "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{secfrac:0sfw$}Z",
124            year = t.year(),
125            month = t.month() as u8,
126            day = t.day(),
127            hour = t.hour(),
128            minute = t.minute(),
129            second = t.second(),
130            secfrac = secfrac,
131            sfw = secfrac_width,
132        )
133    }
134}
135
136#[allow(warnings)]
137#[cfg(test)]
138mod test {
139    use serde::{Deserialize, Serialize};
140
141    use super::*;
142    use crate::google::protobuf::Timestamp;
143
144    // The Go code with which the following timestamps
145    // were tested is as follows:
146    //
147    // ```go
148    // package main
149    //
150    // import (
151    //     "fmt"
152    //     "time"
153    // )
154    //
155    // func main() {
156    //     timestamps := []string{
157    //         "1970-01-01T00:00:00Z",
158    //         "0001-01-01T00:00:00Z",
159    //         "2020-09-14T16:33:00Z",
160    //         "2020-09-14T16:33:00.1Z",
161    //         "2020-09-14T16:33:00.211914212Z",
162    //         "2020-09-14T16:33:54.21191421Z",
163    //         "2021-01-07T20:25:56.045576Z",
164    //         "2021-01-07T20:25:57.039219Z",
165    //         "2021-01-07T20:26:05.00509Z",
166    //         "2021-01-07T20:26:05.005096Z",
167    //         "2021-01-07T20:26:05.0005096Z",
168    //     }
169    //     for _, timestamp := range timestamps {
170    //         ts, err := time.Parse(time.RFC3339Nano, timestamp)
171    //         if err != nil {
172    //             panic(err)
173    //         }
174    //         tss := ts.Format(time.RFC3339Nano)
175    //         if timestamp != tss {
176    //             panic(fmt.Sprintf("\nExpected : %s\nActual   : %s", timestamp, tss))
177    //         }
178    //     }
179    //     fmt.Println("All good!")
180    // }
181    // ```
182    #[test]
183    fn json_timestamp_precision() {
184        let test_timestamps = vec![
185            "1970-01-01T00:00:00Z",
186            "0001-01-01T00:00:00Z",
187            "2020-09-14T16:33:00Z",
188            "2020-09-14T16:33:00.1Z",
189            "2020-09-14T16:33:00.211914212Z",
190            "2020-09-14T16:33:54.21191421Z",
191            "2021-01-07T20:25:56.045576Z",
192            "2021-01-07T20:25:57.039219Z",
193            "2021-01-07T20:26:05.00509Z",
194            "2021-01-07T20:26:05.005096Z",
195            "2021-01-07T20:26:05.0005096Z",
196        ];
197
198        for timestamp in test_timestamps {
199            let json = format!("\"{}\"", timestamp);
200            let rfc = serde_json::from_str::<Rfc3339>(&json).unwrap();
201            assert_eq!(json, serde_json::to_string(&rfc).unwrap());
202        }
203    }
204
205    #[test]
206    fn timestamp_from_to() {
207        let time_ms = 1687848809533;
208
209        let from = Timestamp::from_milis(time_ms);
210        let to = from.to_milis();
211
212        assert_eq!(to, time_ms);
213    }
214
215    #[test]
216    #[should_panic]
217    fn timestamp_millis_out_of_range() {
218        let time_ms = u64::MAX - 1;
219
220        let from = Timestamp::from_milis(time_ms);
221    }
222
223    #[test]
224    #[should_panic]
225    fn timestamp_negative() {
226        let ts = Timestamp {
227            nanos: 1000,
228            seconds: -12,
229        };
230
231        let to = ts.to_milis();
232    }
233}