Skip to main content

defuse_time/
borsh.rs

1use std::{fmt::Display, io, marker::PhantomData};
2
3use borsh::{BorshDeserialize, BorshSerialize};
4use defuse_borsh_utils::{BorshDeserializeAs, BorshSerializeAs};
5
6use crate::{Overflow, Timestamp};
7
8macro_rules! borsh_as {
9    ($($vis:vis struct $name:ident: $int:ty {
10        $as:ident,
11        $from:ident,
12    })*) => {$(
13        $vis struct $name<I>(PhantomData<I>);
14
15        impl<I> BorshSerializeAs<Timestamp> for $name<I>
16        where
17            I: TryFrom<$int> + BorshSerialize,
18            I::Error: Display,
19        {
20            #[inline]
21            fn serialize_as<W>(source: &Timestamp, writer: &mut W) -> io::Result<()>
22            where
23                W: io::Write,
24            {
25                let timestamp: $int = source.$as();
26                I::try_from(timestamp)
27                    .map_err(|err| io::Error::other(err.to_string()))?
28                    .serialize(writer)
29            }
30        }
31
32        impl<I> BorshDeserializeAs<Timestamp> for $name<I>
33        where
34            I: TryInto<$int> + BorshDeserialize,
35            I::Error: Display,
36        {
37            #[inline]
38            fn deserialize_as<R>(reader: &mut R) -> io::Result<Timestamp>
39            where
40                R: io::Read,
41            {
42                let timestamp: $int = I::deserialize_reader(reader)?
43                    .try_into()
44                    .map_err(|err| io::Error::other(err.to_string()))?;
45                Timestamp::$from(timestamp).ok_or(Overflow).map_err( io::Error::other)
46            }
47        }
48
49        #[cfg(feature = "borsh-schema")]
50        const _: () = {
51            use borsh::{BorshSchema, schema::{Declaration, Definition}};
52            use defuse_borsh_utils::BorshSchemaAs;
53
54            impl<I> BorshSchemaAs<Timestamp> for $name<I>
55            where
56                I: BorshSchema,
57            {
58                #[inline]
59                fn declaration_as() -> borsh::schema::Declaration {
60                    <I as BorshSchema>::declaration()
61                }
62
63                #[inline]
64                fn add_definitions_recursively_as(
65                    definitions: &mut std::collections::BTreeMap<
66                        Declaration,
67                        Definition,
68                    >,
69                ) {
70                    <I as BorshSchema>::add_definitions_recursively(definitions);
71                }
72            }
73        };
74    )*};
75}
76
77borsh_as! {
78    pub struct TimestampSeconds: i64 {
79        as_secs,
80        from_secs,
81    }
82
83    pub struct TimestampMilliSeconds: i64 {
84        as_millis,
85        from_millis,
86    }
87
88    pub struct TimestampMicroSeconds: i128 {
89        as_micros,
90        from_micros,
91    }
92
93    pub struct TimestampNanoSeconds: i128 {
94        as_nanos,
95        from_nanos,
96    }
97}
98
99#[cfg(test)]
100#[allow(clippy::inconsistent_digit_grouping)]
101mod tests {
102    use std::fmt::Debug;
103
104    use rstest::rstest;
105
106    use super::*;
107
108    #[rstest]
109    fn timestamp_secs_roundtrip<I>(
110        #[values(
111            0i64, 0u64, 0i32, 0u32,
112            1_600_000_000i64, 1_600_000_000u64,
113            1782395622i64, 1782395622u64,
114            -1782395622i64, -1782395622i32,
115        )]
116        secs: I,
117    ) where
118        I: TryInto<i64, Error: Debug + Display>
119            + TryFrom<i64, Error: Display>
120            + BorshSerialize
121            + BorshDeserialize,
122    {
123        roundtrip_as::<_, TimestampSeconds<I>>(
124            &Timestamp::from_secs(secs.try_into().unwrap()).unwrap(),
125        );
126    }
127
128    #[rstest]
129    fn timestamp_millis_roundtrip<I>(
130        #[values(
131            0i64, 0u64, 0i32, 0u32,
132            1_600_000_000i64, 1_600_000_000u64,
133            1782395622_123i64, 1782395622_123u64,
134            -1782395622_123i64
135        )]
136        millis: I,
137    ) where
138        I: TryInto<i64, Error: Debug + Display>
139            + TryFrom<i64, Error: Display>
140            + BorshSerialize
141            + BorshDeserialize,
142    {
143        roundtrip_as::<_, TimestampMilliSeconds<I>>(
144            &Timestamp::from_millis(millis.try_into().unwrap()).unwrap(),
145        );
146    }
147
148    #[rstest]
149    fn timestamp_micros_roundtrip<I>(
150        #[values(
151            0i128, 0u128, 0i64, 0u64, 0i32, 0u32,
152            1_600_000_000i128, 1_600_000_000u128,
153            1_600_000_000i64, 1_600_000_000u64,
154            1782395622_123456i128, 1782395622_123456u128,
155            -1782395622_123456i128, 1782395622_123456i64,
156            -1782395622_123456i64, 1782395622_123456u64,
157        )]
158        micros: I,
159    ) where
160        I: TryInto<i128, Error: Debug + Display>
161            + TryFrom<i128, Error: Display>
162            + BorshSerialize
163            + BorshDeserialize,
164    {
165        roundtrip_as::<_, TimestampMicroSeconds<I>>(
166            &Timestamp::from_micros(micros.try_into().unwrap()).unwrap(),
167        );
168    }
169
170    #[rstest]
171    fn timestamp_nanos_roundtrip<I>(
172        #[values(
173            0i128, 0u128, 0i64, 0u64, 0i32, 0u32,
174            1_600_000_000i128, 1_600_000_000u128,
175            1_600_000_000i64, 1_600_000_000u64,
176            1782395622_123456789i128, 1782395622_123456789u128,
177            -1782395622_123456789i128, 1782395622_123456789i64,
178            -1782395622_123456789i64, 1782395622_123456789u64,
179        )]
180        nanos: I,
181    ) where
182        I: TryInto<i128, Error: Debug + Display>
183            + TryFrom<i128, Error: Display>
184            + BorshSerialize
185            + BorshDeserialize,
186    {
187        roundtrip_as::<_, TimestampNanoSeconds<I>>(
188            &Timestamp::from_nanos(nanos.try_into().unwrap()).unwrap(),
189        );
190    }
191
192    // Helper roundtrip
193    #[track_caller]
194    fn roundtrip_as<T, As>(orig: &T)
195    where
196        As: BorshSerializeAs<T> + BorshDeserializeAs<T>,
197        T: PartialEq + Debug,
198    {
199        let mut buf = Vec::new();
200        As::serialize_as(orig, &mut buf).expect("serialize_as");
201        let deserialized = As::deserialize_as(&mut buf.as_slice()).expect("deserialize_as");
202        assert_eq!(
203            &deserialized, orig,
204            "deserialized value differs from the original one"
205        );
206    }
207
208    #[cfg(feature = "borsh-schema")]
209    #[test]
210    fn schema_as_usage() {
211        use borsh::BorshSchema;
212        use defuse_borsh_utils::As;
213
214        #[derive(BorshSerialize, BorshDeserialize, BorshSchema)]
215        struct S {
216            #[borsh(
217                serialize_with = "As::<TimestampNanoSeconds<i64>>::serialize",
218                deserialize_with = "As::<TimestampNanoSeconds<i64>>::deserialize",
219                schema(with_funcs(
220                    declaration = "As::<TimestampNanoSeconds<i64>>::declaration",
221                    definitions = "As::<TimestampNanoSeconds<i64>>::add_definitions_recursively",
222                ))
223            )]
224            pub deadline: Timestamp,
225        }
226
227        let val = S {
228            deadline: Timestamp::from_nanos(1_600_000_000_123_456_789).unwrap(),
229        };
230        let bytes = borsh::to_vec(&val).unwrap();
231        let decoded = S::try_from_slice(&bytes).unwrap();
232        assert_eq!(val.deadline, decoded.deadline);
233    }
234}