From 3b2504aaeb395b827cc689283df2c0a8b1a9103f Mon Sep 17 00:00:00 2001 From: Jonas Bushart Date: Wed, 6 Apr 2022 20:33:46 +0000 Subject: [PATCH] Initial support for the `time` crate v0.3 This includes implementations of the Duration*Seconds and Timestamp*Seconds types. The Timestamp*Seconds converters are implemented for both `PrimitiveDateTime` and `OffsetDateTime`. It also includes the well-known format specifiers `Rfc2822` and `Rfc3339` as converters for `OffsetDateTime`: #[serde_as(as = "Rfc2822")] datetime: OffsetDateTime, Simple tests are included. --- serde_with/Cargo.toml | 7 +- serde_with/src/time_0_3.rs | 88 ++++++++++-- serde_with/src/utils/duration.rs | 2 +- serde_with/tests/time_0_3.rs | 221 +++++++++++++++++++++++++++++++ 4 files changed, 308 insertions(+), 10 deletions(-) create mode 100644 serde_with/tests/time_0_3.rs diff --git a/serde_with/Cargo.toml b/serde_with/Cargo.toml index 025aedfe..722f22f0 100644 --- a/serde_with/Cargo.toml +++ b/serde_with/Cargo.toml @@ -43,7 +43,7 @@ rustversion = "1.0.0" serde = {version = "1.0.122", features = ["derive"]} serde_json = {version = "1.0.1", optional = true} serde_with_macros = {path = "../serde_with_macros", version = "1.5.2", optional = true} -time_0_3 = {package = "time", version = "~0.3", optional = true} +time_0_3 = {package = "time", version = "~0.3", optional = true, features = ["serde-well-known"]} [dev-dependencies] expect-test = "1.0.0" @@ -90,6 +90,11 @@ name = "serde_as" path = "tests/serde_as/lib.rs" required-features = ["macros"] +[[test]] +name = "time_0_3" +path = "tests/time_0_3.rs" +required-features = ["macros", "time_0_3"] + [[test]] name = "derives" path = "tests/derives/lib.rs" diff --git a/serde_with/src/time_0_3.rs b/serde_with/src/time_0_3.rs index 4f34705c..be4d74ab 100644 --- a/serde_with/src/time_0_3.rs +++ b/serde_with/src/time_0_3.rs @@ -9,20 +9,23 @@ use crate::{ TimestampMilliSeconds, TimestampMilliSecondsWithFrac, TimestampNanoSeconds, TimestampNanoSecondsWithFrac, TimestampSeconds, TimestampSecondsWithFrac, }; -use serde::{de, Deserializer, Serializer}; +use serde::ser::Error as _; +use serde::{de, Deserializer, Serialize, Serializer}; use std::convert::TryInto; +use std::fmt; use std::time::Duration as StdDuration; -use time_0_3::{Date, Duration, OffsetDateTime, PrimitiveDateTime, Time}; +use time_0_3::format_description::well_known::{Rfc2822, Rfc3339}; +use time_0_3::{Duration, OffsetDateTime, PrimitiveDateTime}; /// Create a [`PrimitiveDateTime`] for the Unix Epoch fn unix_epoch_primitive() -> PrimitiveDateTime { PrimitiveDateTime::new( - Date::from_ordinal_date(1970, 1).unwrap(), - Time::from_hms_nano(0, 0, 0, 0).unwrap(), + time_0_3::Date::from_ordinal_date(1970, 1).unwrap(), + time_0_3::Time::from_hms_nano(0, 0, 0, 0).unwrap(), ) } -/// Convert a [`chrono::Duration`] into a [`DurationSigned`] +/// Convert a [`time::Duration`][time_0_3::Duration] into a [`DurationSigned`] fn duration_into_duration_signed(dur: &Duration) -> DurationSigned { let std_dur = StdDuration::new( dur.whole_seconds().abs() as _, @@ -30,10 +33,11 @@ fn duration_into_duration_signed(dur: &Duration) -> DurationSigned { ); DurationSigned::with_duration( - if dur.is_positive() { - Sign::Positive - } else { + // A duration of 0 is not positive, so check for negative value. + if dur.is_negative() { Sign::Negative + } else { + Sign::Positive }, std_dur, ) @@ -302,3 +306,71 @@ use_duration_signed_de!( {FORMAT, Flexible => FORMAT: Format} } ); + +impl SerializeAs for Rfc2822 { + fn serialize_as(datetime: &OffsetDateTime, serializer: S) -> Result + where + S: Serializer, + { + datetime + .format(&Rfc2822) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} + +impl<'de> DeserializeAs<'de, OffsetDateTime> for Rfc2822 { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct Visitor; + impl<'de> de::Visitor<'de> for Visitor { + type Value = OffsetDateTime; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a RFC2822-formatted `OffsetDateTime`") + } + + fn visit_str(self, value: &str) -> Result { + Self::Value::parse(value, &Rfc2822).map_err(E::custom) + } + } + + deserializer.deserialize_str(Visitor) + } +} + +impl SerializeAs for Rfc3339 { + fn serialize_as(datetime: &OffsetDateTime, serializer: S) -> Result + where + S: Serializer, + { + datetime + .format(&Rfc3339) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} + +impl<'de> DeserializeAs<'de, OffsetDateTime> for Rfc3339 { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct Visitor; + impl<'de> de::Visitor<'de> for Visitor { + type Value = OffsetDateTime; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a RFC3339-formatted `OffsetDateTime`") + } + + fn visit_str(self, value: &str) -> Result { + Self::Value::parse(value, &Rfc3339).map_err(E::custom) + } + } + + deserializer.deserialize_str(Visitor) + } +} diff --git a/serde_with/src/utils/duration.rs b/serde_with/src/utils/duration.rs index bbbc40eb..6caa999c 100644 --- a/serde_with/src/utils/duration.rs +++ b/serde_with/src/utils/duration.rs @@ -54,7 +54,7 @@ impl DurationSigned { } } - #[cfg(feature = "chrono")] + #[cfg(any(feature = "chrono", feature = "time_0_3"))] pub(crate) fn with_duration(sign: Sign, duration: Duration) -> Self { Self { sign, duration } } diff --git a/serde_with/tests/time_0_3.rs b/serde_with/tests/time_0_3.rs new file mode 100644 index 00000000..8d2c13ce --- /dev/null +++ b/serde_with/tests/time_0_3.rs @@ -0,0 +1,221 @@ +mod utils; + +use crate::utils::{check_deserialization, check_error_deserialization, is_equal}; +use expect_test::expect; +use serde::{Deserialize, Serialize}; +use serde_with::{ + serde_as, DurationMicroSeconds, DurationMicroSecondsWithFrac, DurationMilliSeconds, + DurationMilliSecondsWithFrac, DurationNanoSeconds, DurationNanoSecondsWithFrac, + DurationSeconds, DurationSecondsWithFrac, TimestampMicroSeconds, TimestampMicroSecondsWithFrac, + TimestampMilliSeconds, TimestampMilliSecondsWithFrac, TimestampNanoSeconds, + TimestampNanoSecondsWithFrac, TimestampSeconds, TimestampSecondsWithFrac, +}; +use time_0_3::{Duration, OffsetDateTime, PrimitiveDateTime, UtcOffset}; + +/// Create a [`PrimitiveDateTime`] for the Unix Epoch +fn unix_epoch_primitive() -> PrimitiveDateTime { + PrimitiveDateTime::new( + time_0_3::Date::from_ordinal_date(1970, 1).unwrap(), + time_0_3::Time::from_hms_nano(0, 0, 0, 0).unwrap(), + ) +} + +macro_rules! smoketest { + ($($valuety:ty, $adapter:literal, $value:expr, $expect:tt;)*) => { + $({ + #[serde_as] + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct S(#[serde_as(as = $adapter)] $valuety); + #[allow(unused_braces)] + is_equal(S($value), $expect); + })* + }; +} + +#[test] +fn test_duration_smoketest() { + let zero = Duration::seconds(0); + let one_second = Duration::seconds(1); + + smoketest! { + Duration, "DurationSeconds", one_second, {expect![[r#"1"#]]}; + Duration, "DurationSeconds", one_second, {expect![[r#"1.0"#]]}; + Duration, "DurationMilliSeconds", one_second, {expect![[r#"1000"#]]}; + Duration, "DurationMilliSeconds", one_second, {expect![[r#"1000.0"#]]}; + Duration, "DurationMicroSeconds", one_second, {expect![[r#"1000000"#]]}; + Duration, "DurationMicroSeconds", one_second, {expect![[r#"1000000.0"#]]}; + Duration, "DurationNanoSeconds", one_second, {expect![[r#"1000000000"#]]}; + Duration, "DurationNanoSeconds", one_second, {expect![[r#"1000000000.0"#]]}; + }; + + smoketest! { + Duration, "DurationSecondsWithFrac", one_second, {expect![[r#"1.0"#]]}; + Duration, "DurationSecondsWithFrac", one_second, {expect![[r#""1""#]]}; + Duration, "DurationMilliSecondsWithFrac", one_second, {expect![[r#"1000.0"#]]}; + Duration, "DurationMilliSecondsWithFrac", one_second, {expect![[r#""1000""#]]}; + Duration, "DurationMicroSecondsWithFrac", one_second, {expect![[r#"1000000.0"#]]}; + Duration, "DurationMicroSecondsWithFrac", one_second, {expect![[r#""1000000""#]]}; + Duration, "DurationNanoSecondsWithFrac", one_second, {expect![[r#"1000000000.0"#]]}; + Duration, "DurationNanoSecondsWithFrac", one_second, {expect![[r#""1000000000""#]]}; + }; + + smoketest! { + Duration, "DurationSecondsWithFrac", zero, {expect![[r#"0.0"#]]}; + Duration, "DurationSecondsWithFrac", zero + Duration::nanoseconds(500_000_000), {expect![[r#"0.5"#]]}; + Duration, "DurationSecondsWithFrac", zero + Duration::seconds(1), {expect![[r#"1.0"#]]}; + Duration, "DurationSecondsWithFrac", zero - Duration::nanoseconds(500_000_000), {expect![[r#"-0.5"#]]}; + Duration, "DurationSecondsWithFrac", zero - Duration::seconds(1), {expect![[r#"-1.0"#]]}; + }; +} + +#[test] +fn test_datetime_utc_smoketest() { + let zero = OffsetDateTime::UNIX_EPOCH; + let one_second = zero + Duration::seconds(1); + + smoketest! { + OffsetDateTime, "TimestampSeconds", one_second, {expect![[r#"1"#]]}; + OffsetDateTime, "TimestampSeconds", one_second, {expect![[r#"1.0"#]]}; + OffsetDateTime, "TimestampMilliSeconds", one_second, {expect![[r#"1000"#]]}; + OffsetDateTime, "TimestampMilliSeconds", one_second, {expect![[r#"1000.0"#]]}; + OffsetDateTime, "TimestampMicroSeconds", one_second, {expect![[r#"1000000"#]]}; + OffsetDateTime, "TimestampMicroSeconds", one_second, {expect![[r#"1000000.0"#]]}; + OffsetDateTime, "TimestampNanoSeconds", one_second, {expect![[r#"1000000000"#]]}; + OffsetDateTime, "TimestampNanoSeconds", one_second, {expect![[r#"1000000000.0"#]]}; + }; + + smoketest! { + OffsetDateTime, "TimestampSecondsWithFrac", one_second, {expect![[r#"1.0"#]]}; + OffsetDateTime, "TimestampSecondsWithFrac", one_second, {expect![[r#""1""#]]}; + OffsetDateTime, "TimestampMilliSecondsWithFrac", one_second, {expect![[r#"1000.0"#]]}; + OffsetDateTime, "TimestampMilliSecondsWithFrac", one_second, {expect![[r#""1000""#]]}; + OffsetDateTime, "TimestampMicroSecondsWithFrac", one_second, {expect![[r#"1000000.0"#]]}; + OffsetDateTime, "TimestampMicroSecondsWithFrac", one_second, {expect![[r#""1000000""#]]}; + OffsetDateTime, "TimestampNanoSecondsWithFrac", one_second, {expect![[r#"1000000000.0"#]]}; + OffsetDateTime, "TimestampNanoSecondsWithFrac", one_second, {expect![[r#""1000000000""#]]}; + }; + + smoketest! { + OffsetDateTime, "TimestampSecondsWithFrac", zero, {expect![[r#"0.0"#]]}; + OffsetDateTime, "TimestampSecondsWithFrac", zero + Duration::nanoseconds(500_000_000), {expect![[r#"0.5"#]]}; + OffsetDateTime, "TimestampSecondsWithFrac", zero + Duration::seconds(1), {expect![[r#"1.0"#]]}; + OffsetDateTime, "TimestampSecondsWithFrac", zero - Duration::nanoseconds(500_000_000), {expect![[r#"-0.5"#]]}; + OffsetDateTime, "TimestampSecondsWithFrac", zero - Duration::seconds(1), {expect![[r#"-1.0"#]]}; + }; +} + +#[test] +fn test_naive_datetime_smoketest() { + let zero = unix_epoch_primitive(); + let one_second = zero + Duration::seconds(1); + + smoketest! { + PrimitiveDateTime, "TimestampSeconds", one_second, {expect![[r#"1"#]]}; + PrimitiveDateTime, "TimestampSeconds", one_second, {expect![[r#"1.0"#]]}; + PrimitiveDateTime, "TimestampMilliSeconds", one_second, {expect![[r#"1000"#]]}; + PrimitiveDateTime, "TimestampMilliSeconds", one_second, {expect![[r#"1000.0"#]]}; + PrimitiveDateTime, "TimestampMicroSeconds", one_second, {expect![[r#"1000000"#]]}; + PrimitiveDateTime, "TimestampMicroSeconds", one_second, {expect![[r#"1000000.0"#]]}; + PrimitiveDateTime, "TimestampNanoSeconds", one_second, {expect![[r#"1000000000"#]]}; + PrimitiveDateTime, "TimestampNanoSeconds", one_second, {expect![[r#"1000000000.0"#]]}; + }; + + smoketest! { + PrimitiveDateTime, "TimestampSecondsWithFrac", one_second, {expect![[r#"1.0"#]]}; + PrimitiveDateTime, "TimestampSecondsWithFrac", one_second, {expect![[r#""1""#]]}; + PrimitiveDateTime, "TimestampMilliSecondsWithFrac", one_second, {expect![[r#"1000.0"#]]}; + PrimitiveDateTime, "TimestampMilliSecondsWithFrac", one_second, {expect![[r#""1000""#]]}; + PrimitiveDateTime, "TimestampMicroSecondsWithFrac", one_second, {expect![[r#"1000000.0"#]]}; + PrimitiveDateTime, "TimestampMicroSecondsWithFrac", one_second, {expect![[r#""1000000""#]]}; + PrimitiveDateTime, "TimestampNanoSecondsWithFrac", one_second, {expect![[r#"1000000000.0"#]]}; + PrimitiveDateTime, "TimestampNanoSecondsWithFrac", one_second, {expect![[r#""1000000000""#]]}; + }; + + smoketest! { + PrimitiveDateTime, "TimestampSecondsWithFrac", zero, {expect![[r#"0.0"#]]}; + PrimitiveDateTime, "TimestampSecondsWithFrac", zero + Duration::nanoseconds(500_000_000), {expect![[r#"0.5"#]]}; + PrimitiveDateTime, "TimestampSecondsWithFrac", zero + Duration::seconds(1), {expect![[r#"1.0"#]]}; + PrimitiveDateTime, "TimestampSecondsWithFrac", zero - Duration::nanoseconds(500_000_000), {expect![[r#"-0.5"#]]}; + PrimitiveDateTime, "TimestampSecondsWithFrac", zero - Duration::seconds(1), {expect![[r#"-1.0"#]]}; + }; +} + +#[test] +fn test_offset_datetime_rfc2822() { + #[serde_as] + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S(#[serde_as(as = "time_0_3::format_description::well_known::Rfc2822")] OffsetDateTime); + + is_equal( + S(OffsetDateTime::UNIX_EPOCH), + expect![[r#""Thu, 01 Jan 1970 00:00:00 +0000""#]], + ); + + check_error_deserialization::( + r#""Foobar""#, + expect![[r#"the 'weekday' component could not be parsed at line 1 column 8"#]], + ); + check_error_deserialization::( + r#""Fri, 2000""#, + expect![[r#"a character literal was not valid at line 1 column 11"#]], + ); +} + +#[test] +fn test_offset_datetime_rfc3339() { + #[serde_as] + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S(#[serde_as(as = "time_0_3::format_description::well_known::Rfc3339")] OffsetDateTime); + + is_equal( + S(OffsetDateTime::UNIX_EPOCH), + expect![[r#""1970-01-01T00:00:00Z""#]], + ); + check_deserialization::( + S( + OffsetDateTime::from_unix_timestamp_nanos(482_196_050_520_000_000) + .unwrap() + .to_offset(UtcOffset::from_hms(0, 0, 0).unwrap()), + ), + r#""1985-04-12T23:20:50.52Z""#, + ); + check_deserialization::( + S(OffsetDateTime::from_unix_timestamp(851_042_397) + .unwrap() + .to_offset(UtcOffset::from_hms(-8, 0, 0).unwrap())), + r#""1996-12-19T16:39:57-08:00""#, + ); + check_deserialization::( + S( + OffsetDateTime::from_unix_timestamp_nanos(662_687_999_999_999_999) + .unwrap() + .to_offset(UtcOffset::from_hms(0, 0, 0).unwrap()), + ), + r#""1990-12-31T23:59:60Z""#, + ); + check_deserialization::( + S( + OffsetDateTime::from_unix_timestamp_nanos(662_687_999_999_999_999) + .unwrap() + .to_offset(UtcOffset::from_hms(-8, 0, 0).unwrap()), + ), + r#""1990-12-31T15:59:60-08:00""#, + ); + check_deserialization::( + S( + OffsetDateTime::from_unix_timestamp_nanos(-1_041_337_172_130_000_000) + .unwrap() + .to_offset(UtcOffset::from_hms(0, 20, 0).unwrap()), + ), + r#""1937-01-01T12:00:27.87+00:20""#, + ); + + check_error_deserialization::( + r#""Foobar""#, + expect![[r#"the 'year' component could not be parsed at line 1 column 8"#]], + ); + check_error_deserialization::( + r#""2000-AA""#, + expect![[r#"the 'month' component could not be parsed at line 1 column 9"#]], + ); +}