#[cfg(feature = "schemars")]
mod schema;
mod spelling;
use core::fmt;
use serde_core::de::{self, Visitor};
use serde_core::{Deserializer, Serializer};
use crate::OutOfRangeError;
use crate::errors::narrow;
struct CountVisitor;
impl Visitor<'_> for CountVisitor {
type Value = i64;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a count, as a number or a decimal string")
}
fn visit_i64<E: de::Error>(self, count: i64) -> Result<i64, E> {
Ok(count)
}
fn visit_u64<E: de::Error>(self, count: u64) -> Result<i64, E> {
narrow(count).map_err(E::custom)
}
fn visit_str<E: de::Error>(self, count: &str) -> Result<i64, E> {
count.parse().map_err(E::custom)
}
}
fn read<'de, D: Deserializer<'de>>(deserializer: D, nanos_per_unit: i64) -> Result<i64, D::Error> {
let count = if deserializer.is_human_readable() {
deserializer.deserialize_any(CountVisitor)?
} else {
deserializer.deserialize_i64(CountVisitor)?
};
count.checked_mul(nanos_per_unit).ok_or_else(|| de::Error::custom(OutOfRangeError))
}
fn write_number<S: Serializer>(count: i64, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_i64(count)
}
fn write_decimal<S: Serializer>(count: i64, serializer: S) -> Result<S::Ok, S::Error> {
if serializer.is_human_readable() {
serializer.collect_str(&count)
} else {
serializer.serialize_i64(count)
}
}
macro_rules! count_module {
(
$module:ident,
$type:ident,
$as_unit:ident,
$nanos_per_unit:ident,
$write:ident,
$doc:literal $(,)?
) => {
#[doc = concat!("A [`", stringify!($type), "`](crate::", stringify!($type), ") as ", $doc, ".")]
pub mod $module {
use serde_core::{Deserializer, Serializer};
use crate::consts::$nanos_per_unit;
use crate::serde::{read, $write};
use crate::$type;
pub fn serialize<S: Serializer>(
value: &$type, serializer: S,
) -> Result<S::Ok, S::Error> {
$write(value.$as_unit(), serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<$type, D::Error> {
read(deserializer, $nanos_per_unit).map($type::from_nanos)
}
pub mod option {
use serde_core::{Deserialize, Deserializer, Serialize, Serializer};
use crate::consts::$nanos_per_unit;
use crate::serde::{read, $write};
use crate::$type;
struct Counted($type);
impl Serialize for Counted {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
$write(self.0.$as_unit(), serializer)
}
}
impl<'de> Deserialize<'de> for Counted {
fn deserialize<D: Deserializer<'de>>(
deserializer: D,
) -> Result<Self, D::Error> {
read(deserializer, $nanos_per_unit).map($type::from_nanos).map(Self)
}
}
pub fn serialize<S: Serializer>(
value: &Option<$type>, serializer: S,
) -> Result<S::Ok, S::Error> {
value.map(Counted).serialize(serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<$type>, D::Error> {
Ok(Option::<Counted>::deserialize(deserializer)?.map(|counted| counted.0))
}
}
}
};
}
pub mod timestamp {
count_module!(
secs,
Timestamp,
as_secs,
NANOS_PER_SECOND,
write_number,
"whole seconds since the Unix epoch",
);
count_module!(
millis,
Timestamp,
as_millis,
NANOS_PER_MILLISECOND,
write_number,
"whole milliseconds since the Unix epoch",
);
count_module!(
micros,
Timestamp,
as_micros,
NANOS_PER_MICROSECOND,
write_number,
"whole microseconds since the Unix epoch",
);
count_module!(
nanos,
Timestamp,
as_nanos,
NANOS_PER_NANOSECOND,
write_decimal,
"nanoseconds since the Unix epoch, in a string",
);
}
pub mod timedelta {
count_module!(
secs,
Timedelta,
as_secs,
NANOS_PER_SECOND,
write_number,
"a count of whole seconds",
);
count_module!(
millis,
Timedelta,
as_millis,
NANOS_PER_MILLISECOND,
write_number,
"a count of whole milliseconds",
);
count_module!(
micros,
Timedelta,
as_micros,
NANOS_PER_MICROSECOND,
write_number,
"a count of whole microseconds",
);
count_module!(
nanos,
Timedelta,
as_nanos,
NANOS_PER_NANOSECOND,
write_number,
"a count of nanoseconds",
);
}
#[cfg(test)]
mod tests {
use alloc::format;
use alloc::string::ToString as _;
use rstest::rstest;
use serde::{Deserialize, Serialize};
use serde_json::{from_str, to_string};
use serde_test::{Configure as _, Token, assert_tokens};
use super::{timedelta, timestamp};
use crate::{Timedelta, Timestamp};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Fields {
#[serde(with = "timestamp::secs")]
expiry: Timestamp,
#[serde(with = "timestamp::nanos")]
exact: Timestamp,
#[serde(with = "timedelta::millis")]
poll: Timedelta,
#[serde(with = "timedelta::nanos::option")]
budget: Option<Timedelta>,
}
const FIELDS: Fields = Fields {
expiry: Timestamp::from_secs(1_700_000_300),
exact: Timestamp::from_nanos(1_789_544_735_123_456_789),
poll: Timedelta::from_millis(250),
budget: Some(Timedelta::from_nanos(500)),
};
const OUT_OF_RANGE: &str = "out of range error: the time is outside what the target type holds";
#[test]
fn each_unit_round_trips_as_its_count() {
let written = to_string(&FIELDS).expect("a document");
assert_eq!(
written,
r#"{"expiry":1700000300,"exact":"1789544735123456789","poll":250,"budget":500}"#,
"each field as a count in its unit"
);
assert_eq!(from_str::<Fields>(&written).expect("the fields"), FIELDS, "and back");
}
#[test]
fn a_format_no_person_reads_takes_each_count_as_an_integer() {
assert_tokens(
&FIELDS.compact(),
&[
Token::Struct { name: "Fields", len: 4 },
Token::Str("expiry"),
Token::I64(1_700_000_300),
Token::Str("exact"),
Token::I64(1_789_544_735_123_456_789),
Token::Str("poll"),
Token::I64(250),
Token::Str("budget"),
Token::Some,
Token::I64(500),
Token::StructEnd,
],
);
}
#[test]
fn a_count_is_read_quoted_or_bare() {
let read: Fields = from_str(
r#"{"expiry":"1700000300","exact":1789544735123456789,"poll":"250","budget":null}"#,
)
.expect("the fields");
assert_eq!(read, Fields { budget: None, ..FIELDS }, "each count, however it came");
}
#[rstest]
#[case::the_year_9999("253402300799", OUT_OF_RANGE)]
#[case::not_a_count(r#""soon""#, "invalid digit found in string")]
#[case::past_an_i64("18446744073709551615", OUT_OF_RANGE)]
fn a_count_past_the_range_or_not_a_count_is_refused(
#[case] expiry: &str, #[case] reason: &str,
) {
let document = format!(r#"{{"expiry":{expiry},"exact":"0","poll":0,"budget":null}}"#);
let error = from_str::<Fields>(&document).expect_err("a count the field refuses");
let written = error.to_string();
assert_eq!(written.split(" at line ").next(), Some(reason), "refused for its reason");
}
}