diff --git a/tests/serde/timestamps.rs b/tests/serde/timestamps.rs index 9a93c559d0..3c3ff499e5 100644 --- a/tests/serde/timestamps.rs +++ b/tests/serde/timestamps.rs @@ -10,6 +10,24 @@ struct Test { dt: OffsetDateTime, } +#[derive(Serialize, Deserialize, Debug, PartialEq)] +struct TestMilliseconds { + #[serde(with = "timestamp::milliseconds")] + dt: OffsetDateTime, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +struct TestMicroseconds { + #[serde(with = "timestamp::microseconds")] + dt: OffsetDateTime, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +struct TestNanoseconds { + #[serde(with = "timestamp::nanoseconds")] + dt: OffsetDateTime, +} + #[test] fn serialize_timestamp() { let value = Test { @@ -40,3 +58,78 @@ fn serialize_timestamp() { "invalid type: string \"bad\", expected i64", ); } + +#[test] +fn serialize_timestamp_milliseconds() { + let value_milliseconds = TestMilliseconds { + dt: datetime!(2000-01-01 00:00:00.999 UTC), + }; + assert_de_tokens_error::( + &[ + Token::Struct { + name: "TestMilliseconds", + len: 1, + }, + Token::Str("dt"), + Token::Str("bad"), + Token::StructEnd, + ], + "invalid type: string \"bad\", expected i128", + ); + // serde_test does not support I128, see: https://github.com/serde-rs/test/issues/18 + let milliseconds_str = r#"{"dt":946684800999}"#; + let deserialized_milliseconds: TestMilliseconds = serde_json::from_str(milliseconds_str).unwrap(); + let serialized_milliseconds = serde_json::to_string(&value_milliseconds).unwrap(); + assert_eq!(value_milliseconds.dt, deserialized_milliseconds.dt); + assert_eq!(serialized_milliseconds, milliseconds_str); +} + +#[test] +fn serialize_timestamp_microseconds() { + let value_microseconds = TestMicroseconds { + dt: datetime!(2000-01-01 00:00:00.999_999 UTC), + }; + assert_de_tokens_error::( + &[ + Token::Struct { + name: "TestMicroseconds", + len: 1, + }, + Token::Str("dt"), + Token::Str("bad"), + Token::StructEnd, + ], + "invalid type: string \"bad\", expected i128", + ); + // serde_test does not support I128, see: https://github.com/serde-rs/test/issues/18 + let microseconds_str = r#"{"dt":946684800999999}"#; + let deserialized_microseconds: TestMicroseconds = serde_json::from_str(microseconds_str).unwrap(); + let serialized_microseconds = serde_json::to_string(&value_microseconds).unwrap(); + assert_eq!(value_microseconds.dt, deserialized_microseconds.dt); + assert_eq!(serialized_microseconds, microseconds_str); +} + +#[test] +fn serialize_timestamp_nanoseconds() { + let value_nanoseconds = TestNanoseconds { + dt: datetime!(2000-01-01 00:00:00.999_999_999 UTC), + }; + assert_de_tokens_error::( + &[ + Token::Struct { + name: "TestNanoseconds", + len: 1, + }, + Token::Str("dt"), + Token::Str("bad"), + Token::StructEnd, + ], + "invalid type: string \"bad\", expected i128", + ); + // serde_test does not support I128, see: https://github.com/serde-rs/test/issues/18 + let nanoseconds_str = r#"{"dt":946684800999999999}"#; + let deserialized_nanoseconds: TestNanoseconds = serde_json::from_str(nanoseconds_str).unwrap(); + let serialized_nanoseconds = serde_json::to_string(&value_nanoseconds).unwrap(); + assert_eq!(value_nanoseconds.dt, deserialized_nanoseconds.dt); + assert_eq!(serialized_nanoseconds, nanoseconds_str); +} diff --git a/time/src/serde/timestamp/microseconds.rs b/time/src/serde/timestamp/microseconds.rs new file mode 100644 index 0000000000..65c603ec35 --- /dev/null +++ b/time/src/serde/timestamp/microseconds.rs @@ -0,0 +1,63 @@ +//! Treat an [`OffsetDateTime`] as a [Unix timestamp] with microseconds for +//! the purposes of serde. +//! +//! Use this module in combination with serde's [`#[with]`][with] attribute. +//! +//! When deserializing, the offset is assumed to be UTC. +//! +//! [Unix timestamp]: https://en.wikipedia.org/wiki/Unix_time +//! [with]: https://serde.rs/field-attrs.html#with + +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +use crate::OffsetDateTime; + +/// Serialize an `OffsetDateTime` as its Unix timestamp with microseconds +pub fn serialize( + datetime: &OffsetDateTime, + serializer: S, +) -> Result { + let timestamp = datetime.unix_timestamp_nanos() / 1_000; + timestamp.serialize(serializer) +} + +/// Deserialize an `OffsetDateTime` from its Unix timestamp with microseconds +pub fn deserialize<'a, D: Deserializer<'a>>(deserializer: D) -> Result { + let value: i128 = <_>::deserialize(deserializer)?; + OffsetDateTime::from_unix_timestamp_nanos(value * 1_000) + .map_err(|err| de::Error::invalid_value(de::Unexpected::Signed(err.value), &err)) +} + +/// Treat an `Option` as a [Unix timestamp] with microseconds +/// for the purposes of serde. +/// +/// Use this module in combination with serde's [`#[with]`][with] attribute. +/// +/// When deserializing, the offset is assumed to be UTC. +/// +/// [Unix timestamp]: https://en.wikipedia.org/wiki/Unix_time +/// [with]: https://serde.rs/field-attrs.html#with +pub mod option { + #[allow(clippy::wildcard_imports)] + use super::*; + + /// Serialize an `Option` as its Unix timestamp with microseconds + pub fn serialize( + option: &Option, + serializer: S, + ) -> Result { + option + .map(|timestamp| timestamp.unix_timestamp_nanos() / 1_000) + .serialize(serializer) + } + + /// Deserialize an `Option` from its Unix timestamp with microseconds + pub fn deserialize<'a, D: Deserializer<'a>>( + deserializer: D, + ) -> Result, D::Error> { + Option::deserialize(deserializer)? + .map(|value: i128| OffsetDateTime::from_unix_timestamp_nanos(value * 1_000)) + .transpose() + .map_err(|err| de::Error::invalid_value(de::Unexpected::Signed(err.value), &err)) + } +} diff --git a/time/src/serde/timestamp/milliseconds.rs b/time/src/serde/timestamp/milliseconds.rs new file mode 100644 index 0000000000..e571b6c9e2 --- /dev/null +++ b/time/src/serde/timestamp/milliseconds.rs @@ -0,0 +1,63 @@ +//! Treat an [`OffsetDateTime`] as a [Unix timestamp] with milliseconds for +//! the purposes of serde. +//! +//! Use this module in combination with serde's [`#[with]`][with] attribute. +//! +//! When deserializing, the offset is assumed to be UTC. +//! +//! [Unix timestamp]: https://en.wikipedia.org/wiki/Unix_time +//! [with]: https://serde.rs/field-attrs.html#with + +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +use crate::OffsetDateTime; + +/// Serialize an `OffsetDateTime` as its Unix timestamp with milliseconds +pub fn serialize( + datetime: &OffsetDateTime, + serializer: S, +) -> Result { + let timestamp = datetime.unix_timestamp_nanos() / 1_000_000; + timestamp.serialize(serializer) +} + +/// Deserialize an `OffsetDateTime` from its Unix timestamp with milliseconds +pub fn deserialize<'a, D: Deserializer<'a>>(deserializer: D) -> Result { + let value: i128 = <_>::deserialize(deserializer)?; + OffsetDateTime::from_unix_timestamp_nanos(value * 1_000_000) + .map_err(|err| de::Error::invalid_value(de::Unexpected::Signed(err.value), &err)) +} + +/// Treat an `Option` as a [Unix timestamp] with milliseconds +/// for the purposes of serde. +/// +/// Use this module in combination with serde's [`#[with]`][with] attribute. +/// +/// When deserializing, the offset is assumed to be UTC. +/// +/// [Unix timestamp]: https://en.wikipedia.org/wiki/Unix_time +/// [with]: https://serde.rs/field-attrs.html#with +pub mod option { + #[allow(clippy::wildcard_imports)] + use super::*; + + /// Serialize an `Option` as its Unix timestamp with milliseconds + pub fn serialize( + option: &Option, + serializer: S, + ) -> Result { + option + .map(|timestamp| timestamp.unix_timestamp_nanos() / 1_000_000) + .serialize(serializer) + } + + /// Deserialize an `Option` from its Unix timestamp with milliseconds + pub fn deserialize<'a, D: Deserializer<'a>>( + deserializer: D, + ) -> Result, D::Error> { + Option::deserialize(deserializer)? + .map(|value: i128| OffsetDateTime::from_unix_timestamp_nanos(value * 1_000_000)) + .transpose() + .map_err(|err| de::Error::invalid_value(de::Unexpected::Signed(err.value), &err)) + } +} diff --git a/time/src/serde/timestamp.rs b/time/src/serde/timestamp/mod.rs similarity index 97% rename from time/src/serde/timestamp.rs rename to time/src/serde/timestamp/mod.rs index d86e6b9336..6dd0db03cd 100644 --- a/time/src/serde/timestamp.rs +++ b/time/src/serde/timestamp/mod.rs @@ -7,6 +7,10 @@ //! [Unix timestamp]: https://en.wikipedia.org/wiki/Unix_time //! [with]: https://serde.rs/field-attrs.html#with +pub mod microseconds; +pub mod milliseconds; +pub mod nanoseconds; + use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use crate::OffsetDateTime; diff --git a/time/src/serde/timestamp/nanoseconds.rs b/time/src/serde/timestamp/nanoseconds.rs new file mode 100644 index 0000000000..c71d1e7cdb --- /dev/null +++ b/time/src/serde/timestamp/nanoseconds.rs @@ -0,0 +1,61 @@ +//! Treat an [`OffsetDateTime`] as a [Unix timestamp] with nanoseconds for +//! the purposes of serde. +//! +//! Use this module in combination with serde's [`#[with]`][with] attribute. +//! +//! When deserializing, the offset is assumed to be UTC. +//! +//! [Unix timestamp]: https://en.wikipedia.org/wiki/Unix_time +//! [with]: https://serde.rs/field-attrs.html#with + +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +use crate::OffsetDateTime; + +/// Serialize an `OffsetDateTime` as its Unix timestamp with nanoseconds +pub fn serialize( + datetime: &OffsetDateTime, + serializer: S, +) -> Result { + datetime.unix_timestamp_nanos().serialize(serializer) +} + +/// Deserialize an `OffsetDateTime` from its Unix timestamp with nanoseconds +pub fn deserialize<'a, D: Deserializer<'a>>(deserializer: D) -> Result { + OffsetDateTime::from_unix_timestamp_nanos(<_>::deserialize(deserializer)?) + .map_err(|err| de::Error::invalid_value(de::Unexpected::Signed(err.value), &err)) +} + +/// Treat an `Option` as a [Unix timestamp] with nanoseconds +/// for the purposes of serde. +/// +/// Use this module in combination with serde's [`#[with]`][with] attribute. +/// +/// When deserializing, the offset is assumed to be UTC. +/// +/// [Unix timestamp]: https://en.wikipedia.org/wiki/Unix_time +/// [with]: https://serde.rs/field-attrs.html#with +pub mod option { + #[allow(clippy::wildcard_imports)] + use super::*; + + /// Serialize an `Option` as its Unix timestamp with nanoseconds + pub fn serialize( + option: &Option, + serializer: S, + ) -> Result { + option + .map(OffsetDateTime::unix_timestamp_nanos) + .serialize(serializer) + } + + /// Deserialize an `Option` from its Unix timestamp with nanoseconds + pub fn deserialize<'a, D: Deserializer<'a>>( + deserializer: D, + ) -> Result, D::Error> { + Option::deserialize(deserializer)? + .map(OffsetDateTime::from_unix_timestamp_nanos) + .transpose() + .map_err(|err| de::Error::invalid_value(de::Unexpected::Signed(err.value), &err)) + } +}