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 #[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}