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