tendermint_proto/serializers/
timestamp.rs

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