Skip to content

Commit

Permalink
Initial support for the time crate v0.3
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jonasbb committed May 12, 2022
1 parent ccad9af commit 3b2504a
Show file tree
Hide file tree
Showing 4 changed files with 308 additions and 10 deletions.
7 changes: 6 additions & 1 deletion serde_with/Cargo.toml
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
88 changes: 80 additions & 8 deletions serde_with/src/time_0_3.rs
Expand Up @@ -9,31 +9,35 @@ 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 _,
dur.subsec_nanoseconds().abs() as _,
);

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,
)
Expand Down Expand Up @@ -302,3 +306,71 @@ use_duration_signed_de!(
{FORMAT, Flexible => FORMAT: Format}
}
);

impl SerializeAs<OffsetDateTime> for Rfc2822 {
fn serialize_as<S>(datetime: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
datetime
.format(&Rfc2822)
.map_err(S::Error::custom)?
.serialize(serializer)
}
}

impl<'de> DeserializeAs<'de, OffsetDateTime> for Rfc2822 {
fn deserialize_as<D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
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<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
Self::Value::parse(value, &Rfc2822).map_err(E::custom)
}
}

deserializer.deserialize_str(Visitor)
}
}

impl SerializeAs<OffsetDateTime> for Rfc3339 {
fn serialize_as<S>(datetime: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
datetime
.format(&Rfc3339)
.map_err(S::Error::custom)?
.serialize(serializer)
}
}

impl<'de> DeserializeAs<'de, OffsetDateTime> for Rfc3339 {
fn deserialize_as<D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
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<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
Self::Value::parse(value, &Rfc3339).map_err(E::custom)
}
}

deserializer.deserialize_str(Visitor)
}
}
2 changes: 1 addition & 1 deletion serde_with/src/utils/duration.rs
Expand Up @@ -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 }
}
Expand Down
221 changes: 221 additions & 0 deletions 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<i64>", one_second, {expect![[r#"1"#]]};
Duration, "DurationSeconds<f64>", one_second, {expect![[r#"1.0"#]]};
Duration, "DurationMilliSeconds<i64>", one_second, {expect![[r#"1000"#]]};
Duration, "DurationMilliSeconds<f64>", one_second, {expect![[r#"1000.0"#]]};
Duration, "DurationMicroSeconds<i64>", one_second, {expect![[r#"1000000"#]]};
Duration, "DurationMicroSeconds<f64>", one_second, {expect![[r#"1000000.0"#]]};
Duration, "DurationNanoSeconds<i64>", one_second, {expect![[r#"1000000000"#]]};
Duration, "DurationNanoSeconds<f64>", one_second, {expect![[r#"1000000000.0"#]]};
};

smoketest! {
Duration, "DurationSecondsWithFrac", one_second, {expect![[r#"1.0"#]]};
Duration, "DurationSecondsWithFrac<String>", one_second, {expect![[r#""1""#]]};
Duration, "DurationMilliSecondsWithFrac", one_second, {expect![[r#"1000.0"#]]};
Duration, "DurationMilliSecondsWithFrac<String>", one_second, {expect![[r#""1000""#]]};
Duration, "DurationMicroSecondsWithFrac", one_second, {expect![[r#"1000000.0"#]]};
Duration, "DurationMicroSecondsWithFrac<String>", one_second, {expect![[r#""1000000""#]]};
Duration, "DurationNanoSecondsWithFrac", one_second, {expect![[r#"1000000000.0"#]]};
Duration, "DurationNanoSecondsWithFrac<String>", 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<i64>", one_second, {expect![[r#"1"#]]};
OffsetDateTime, "TimestampSeconds<f64>", one_second, {expect![[r#"1.0"#]]};
OffsetDateTime, "TimestampMilliSeconds<i64>", one_second, {expect![[r#"1000"#]]};
OffsetDateTime, "TimestampMilliSeconds<f64>", one_second, {expect![[r#"1000.0"#]]};
OffsetDateTime, "TimestampMicroSeconds<i64>", one_second, {expect![[r#"1000000"#]]};
OffsetDateTime, "TimestampMicroSeconds<f64>", one_second, {expect![[r#"1000000.0"#]]};
OffsetDateTime, "TimestampNanoSeconds<i64>", one_second, {expect![[r#"1000000000"#]]};
OffsetDateTime, "TimestampNanoSeconds<f64>", one_second, {expect![[r#"1000000000.0"#]]};
};

smoketest! {
OffsetDateTime, "TimestampSecondsWithFrac", one_second, {expect![[r#"1.0"#]]};
OffsetDateTime, "TimestampSecondsWithFrac<String>", one_second, {expect![[r#""1""#]]};
OffsetDateTime, "TimestampMilliSecondsWithFrac", one_second, {expect![[r#"1000.0"#]]};
OffsetDateTime, "TimestampMilliSecondsWithFrac<String>", one_second, {expect![[r#""1000""#]]};
OffsetDateTime, "TimestampMicroSecondsWithFrac", one_second, {expect![[r#"1000000.0"#]]};
OffsetDateTime, "TimestampMicroSecondsWithFrac<String>", one_second, {expect![[r#""1000000""#]]};
OffsetDateTime, "TimestampNanoSecondsWithFrac", one_second, {expect![[r#"1000000000.0"#]]};
OffsetDateTime, "TimestampNanoSecondsWithFrac<String>", 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<i64>", one_second, {expect![[r#"1"#]]};
PrimitiveDateTime, "TimestampSeconds<f64>", one_second, {expect![[r#"1.0"#]]};
PrimitiveDateTime, "TimestampMilliSeconds<i64>", one_second, {expect![[r#"1000"#]]};
PrimitiveDateTime, "TimestampMilliSeconds<f64>", one_second, {expect![[r#"1000.0"#]]};
PrimitiveDateTime, "TimestampMicroSeconds<i64>", one_second, {expect![[r#"1000000"#]]};
PrimitiveDateTime, "TimestampMicroSeconds<f64>", one_second, {expect![[r#"1000000.0"#]]};
PrimitiveDateTime, "TimestampNanoSeconds<i64>", one_second, {expect![[r#"1000000000"#]]};
PrimitiveDateTime, "TimestampNanoSeconds<f64>", one_second, {expect![[r#"1000000000.0"#]]};
};

smoketest! {
PrimitiveDateTime, "TimestampSecondsWithFrac", one_second, {expect![[r#"1.0"#]]};
PrimitiveDateTime, "TimestampSecondsWithFrac<String>", one_second, {expect![[r#""1""#]]};
PrimitiveDateTime, "TimestampMilliSecondsWithFrac", one_second, {expect![[r#"1000.0"#]]};
PrimitiveDateTime, "TimestampMilliSecondsWithFrac<String>", one_second, {expect![[r#""1000""#]]};
PrimitiveDateTime, "TimestampMicroSecondsWithFrac", one_second, {expect![[r#"1000000.0"#]]};
PrimitiveDateTime, "TimestampMicroSecondsWithFrac<String>", one_second, {expect![[r#""1000000""#]]};
PrimitiveDateTime, "TimestampNanoSecondsWithFrac", one_second, {expect![[r#"1000000000.0"#]]};
PrimitiveDateTime, "TimestampNanoSecondsWithFrac<String>", 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::<S>(
r#""Foobar""#,
expect![[r#"the 'weekday' component could not be parsed at line 1 column 8"#]],
);
check_error_deserialization::<S>(
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>(
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>(
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>(
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>(
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>(
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::<S>(
r#""Foobar""#,
expect![[r#"the 'year' component could not be parsed at line 1 column 8"#]],
);
check_error_deserialization::<S>(
r#""2000-AA""#,
expect![[r#"the 'month' component could not be parsed at line 1 column 9"#]],
);
}

0 comments on commit 3b2504a

Please sign in to comment.