1#[cfg(feature = "schemars")]
35mod schema;
36mod spelling;
37
38use core::fmt;
39
40use serde_core::de::{self, Visitor};
41use serde_core::{Deserializer, Serializer};
42
43use crate::OutOfRangeError;
44use crate::errors::narrow;
45
46struct CountVisitor;
48
49impl Visitor<'_> for CountVisitor {
50 type Value = i64;
51
52 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53 formatter.write_str("a count, as a number or a decimal string")
54 }
55
56 fn visit_i64<E: de::Error>(self, count: i64) -> Result<i64, E> {
57 Ok(count)
58 }
59
60 fn visit_u64<E: de::Error>(self, count: u64) -> Result<i64, E> {
61 narrow(count).map_err(E::custom)
62 }
63
64 fn visit_str<E: de::Error>(self, count: &str) -> Result<i64, E> {
65 count.parse().map_err(E::custom)
66 }
67}
68
69fn read<'de, D: Deserializer<'de>>(deserializer: D, nanos_per_unit: i64) -> Result<i64, D::Error> {
74 let count = if deserializer.is_human_readable() {
75 deserializer.deserialize_any(CountVisitor)?
76 } else {
77 deserializer.deserialize_i64(CountVisitor)?
78 };
79 count.checked_mul(nanos_per_unit).ok_or_else(|| de::Error::custom(OutOfRangeError))
80}
81
82fn write_number<S: Serializer>(count: i64, serializer: S) -> Result<S::Ok, S::Error> {
84 serializer.serialize_i64(count)
85}
86
87fn write_decimal<S: Serializer>(count: i64, serializer: S) -> Result<S::Ok, S::Error> {
89 if serializer.is_human_readable() {
90 serializer.collect_str(&count)
91 } else {
92 serializer.serialize_i64(count)
93 }
94}
95
96macro_rules! count_module {
98 (
99 $module:ident,
100 $type:ident,
101 $as_unit:ident,
102 $nanos_per_unit:ident,
103 $write:ident,
104 $doc:literal $(,)?
105 ) => {
106 #[doc = concat!("A [`", stringify!($type), "`](crate::", stringify!($type), ") as ", $doc, ".")]
107 pub mod $module {
108 use serde_core::{Deserializer, Serializer};
109
110 use crate::consts::$nanos_per_unit;
111 use crate::serde::{read, $write};
112 use crate::$type;
113
114 pub fn serialize<S: Serializer>(
119 value: &$type, serializer: S,
120 ) -> Result<S::Ok, S::Error> {
121 $write(value.$as_unit(), serializer)
122 }
123
124 pub fn deserialize<'de, D: Deserializer<'de>>(
130 deserializer: D,
131 ) -> Result<$type, D::Error> {
132 read(deserializer, $nanos_per_unit).map($type::from_nanos)
133 }
134
135 pub mod option {
137 use serde_core::{Deserialize, Deserializer, Serialize, Serializer};
138
139 use crate::consts::$nanos_per_unit;
140 use crate::serde::{read, $write};
141 use crate::$type;
142
143 struct Counted($type);
146
147 impl Serialize for Counted {
148 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
149 $write(self.0.$as_unit(), serializer)
150 }
151 }
152
153 impl<'de> Deserialize<'de> for Counted {
154 fn deserialize<D: Deserializer<'de>>(
155 deserializer: D,
156 ) -> Result<Self, D::Error> {
157 read(deserializer, $nanos_per_unit).map($type::from_nanos).map(Self)
158 }
159 }
160
161 pub fn serialize<S: Serializer>(
166 value: &Option<$type>, serializer: S,
167 ) -> Result<S::Ok, S::Error> {
168 value.map(Counted).serialize(serializer)
169 }
170
171 pub fn deserialize<'de, D: Deserializer<'de>>(
177 deserializer: D,
178 ) -> Result<Option<$type>, D::Error> {
179 Ok(Option::<Counted>::deserialize(deserializer)?.map(|counted| counted.0))
180 }
181 }
182 }
183 };
184}
185
186pub mod timestamp {
189 count_module!(
190 secs,
191 Timestamp,
192 as_secs,
193 NANOS_PER_SECOND,
194 write_number,
195 "whole seconds since the Unix epoch",
196 );
197 count_module!(
198 millis,
199 Timestamp,
200 as_millis,
201 NANOS_PER_MILLISECOND,
202 write_number,
203 "whole milliseconds since the Unix epoch",
204 );
205 count_module!(
206 micros,
207 Timestamp,
208 as_micros,
209 NANOS_PER_MICROSECOND,
210 write_number,
211 "whole microseconds since the Unix epoch",
212 );
213 count_module!(
214 nanos,
215 Timestamp,
216 as_nanos,
217 NANOS_PER_NANOSECOND,
218 write_decimal,
219 "nanoseconds since the Unix epoch, in a string",
220 );
221}
222
223pub mod timedelta {
225 count_module!(
226 secs,
227 Timedelta,
228 as_secs,
229 NANOS_PER_SECOND,
230 write_number,
231 "a count of whole seconds",
232 );
233 count_module!(
234 millis,
235 Timedelta,
236 as_millis,
237 NANOS_PER_MILLISECOND,
238 write_number,
239 "a count of whole milliseconds",
240 );
241 count_module!(
242 micros,
243 Timedelta,
244 as_micros,
245 NANOS_PER_MICROSECOND,
246 write_number,
247 "a count of whole microseconds",
248 );
249 count_module!(
250 nanos,
251 Timedelta,
252 as_nanos,
253 NANOS_PER_NANOSECOND,
254 write_number,
255 "a count of nanoseconds",
256 );
257}
258
259#[cfg(test)]
260mod tests {
261 use alloc::format;
262 use alloc::string::ToString as _;
263
264 use rstest::rstest;
265 use serde::{Deserialize, Serialize};
266 use serde_json::{from_str, to_string};
267 use serde_test::{Configure as _, Token, assert_tokens};
268
269 use super::{timedelta, timestamp};
270 use crate::{Timedelta, Timestamp};
271
272 #[derive(Debug, PartialEq, Serialize, Deserialize)]
274 struct Fields {
275 #[serde(with = "timestamp::secs")]
277 expiry: Timestamp,
278 #[serde(with = "timestamp::nanos")]
280 exact: Timestamp,
281 #[serde(with = "timedelta::millis")]
283 poll: Timedelta,
284 #[serde(with = "timedelta::nanos::option")]
286 budget: Option<Timedelta>,
287 }
288
289 const FIELDS: Fields = Fields {
291 expiry: Timestamp::from_secs(1_700_000_300),
292 exact: Timestamp::from_nanos(1_789_544_735_123_456_789),
293 poll: Timedelta::from_millis(250),
294 budget: Some(Timedelta::from_nanos(500)),
295 };
296
297 const OUT_OF_RANGE: &str = "out of range error: the time is outside what the target type holds";
299
300 #[test]
301 fn each_unit_round_trips_as_its_count() {
302 let written = to_string(&FIELDS).expect("a document");
303 assert_eq!(
304 written,
305 r#"{"expiry":1700000300,"exact":"1789544735123456789","poll":250,"budget":500}"#,
306 "each field as a count in its unit"
307 );
308 assert_eq!(from_str::<Fields>(&written).expect("the fields"), FIELDS, "and back");
309 }
310
311 #[test]
312 fn a_format_no_person_reads_takes_each_count_as_an_integer() {
313 assert_tokens(
314 &FIELDS.compact(),
315 &[
316 Token::Struct { name: "Fields", len: 4 },
317 Token::Str("expiry"),
318 Token::I64(1_700_000_300),
319 Token::Str("exact"),
320 Token::I64(1_789_544_735_123_456_789),
321 Token::Str("poll"),
322 Token::I64(250),
323 Token::Str("budget"),
324 Token::Some,
325 Token::I64(500),
326 Token::StructEnd,
327 ],
328 );
329 }
330
331 #[test]
332 fn a_count_is_read_quoted_or_bare() {
333 let read: Fields = from_str(
334 r#"{"expiry":"1700000300","exact":1789544735123456789,"poll":"250","budget":null}"#,
335 )
336 .expect("the fields");
337 assert_eq!(read, Fields { budget: None, ..FIELDS }, "each count, however it came");
338 }
339
340 #[rstest]
341 #[case::the_year_9999("253402300799", OUT_OF_RANGE)]
342 #[case::not_a_count(r#""soon""#, "invalid digit found in string")]
343 #[case::past_an_i64("18446744073709551615", OUT_OF_RANGE)]
344 fn a_count_past_the_range_or_not_a_count_is_refused(
345 #[case] expiry: &str, #[case] reason: &str,
346 ) {
347 let document = format!(r#"{{"expiry":{expiry},"exact":"0","poll":0,"budget":null}}"#);
348 let error = from_str::<Fields>(&document).expect_err("a count the field refuses");
349 let written = error.to_string();
350 assert_eq!(written.split(" at line ").next(), Some(reason), "refused for its reason");
351 }
352}