Skip to content

Commit

Permalink
Adds support to serialize and deserialize timestamps with different r…
Browse files Browse the repository at this point in the history
…esolutions (#648)
  • Loading branch information
ogarcia committed Jan 31, 2024
1 parent bb397df commit be932d8
Show file tree
Hide file tree
Showing 5 changed files with 284 additions and 0 deletions.
93 changes: 93 additions & 0 deletions tests/serde/timestamps.rs
Expand Up @@ -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 {
Expand Down Expand Up @@ -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::<TestMilliseconds>(
&[
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::<TestMicroseconds>(
&[
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::<TestNanoseconds>(
&[
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);
}
63 changes: 63 additions & 0 deletions 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<S: Serializer>(
datetime: &OffsetDateTime,
serializer: S,
) -> Result<S::Ok, S::Error> {
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<OffsetDateTime, D::Error> {
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<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
pub mod option {
#[allow(clippy::wildcard_imports)]
use super::*;

/// Serialize an `Option<OffsetDateTime>` as its Unix timestamp with microseconds
pub fn serialize<S: Serializer>(
option: &Option<OffsetDateTime>,
serializer: S,
) -> Result<S::Ok, S::Error> {
option
.map(|timestamp| timestamp.unix_timestamp_nanos() / 1_000)
.serialize(serializer)
}

/// Deserialize an `Option<OffsetDateTime>` from its Unix timestamp with microseconds
pub fn deserialize<'a, D: Deserializer<'a>>(
deserializer: D,
) -> Result<Option<OffsetDateTime>, 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))
}
}
63 changes: 63 additions & 0 deletions 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<S: Serializer>(
datetime: &OffsetDateTime,
serializer: S,
) -> Result<S::Ok, S::Error> {
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<OffsetDateTime, D::Error> {
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<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
pub mod option {
#[allow(clippy::wildcard_imports)]
use super::*;

/// Serialize an `Option<OffsetDateTime>` as its Unix timestamp with milliseconds
pub fn serialize<S: Serializer>(
option: &Option<OffsetDateTime>,
serializer: S,
) -> Result<S::Ok, S::Error> {
option
.map(|timestamp| timestamp.unix_timestamp_nanos() / 1_000_000)
.serialize(serializer)
}

/// Deserialize an `Option<OffsetDateTime>` from its Unix timestamp with milliseconds
pub fn deserialize<'a, D: Deserializer<'a>>(
deserializer: D,
) -> Result<Option<OffsetDateTime>, 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))
}
}
Expand Up @@ -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;
Expand Down
61 changes: 61 additions & 0 deletions 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<S: Serializer>(
datetime: &OffsetDateTime,
serializer: S,
) -> Result<S::Ok, S::Error> {
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, D::Error> {
OffsetDateTime::from_unix_timestamp_nanos(<_>::deserialize(deserializer)?)
.map_err(|err| de::Error::invalid_value(de::Unexpected::Signed(err.value), &err))
}

/// Treat an `Option<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
pub mod option {
#[allow(clippy::wildcard_imports)]
use super::*;

/// Serialize an `Option<OffsetDateTime>` as its Unix timestamp with nanoseconds
pub fn serialize<S: Serializer>(
option: &Option<OffsetDateTime>,
serializer: S,
) -> Result<S::Ok, S::Error> {
option
.map(OffsetDateTime::unix_timestamp_nanos)
.serialize(serializer)
}

/// Deserialize an `Option<OffsetDateTime>` from its Unix timestamp with nanoseconds
pub fn deserialize<'a, D: Deserializer<'a>>(
deserializer: D,
) -> Result<Option<OffsetDateTime>, 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))
}
}

0 comments on commit be932d8

Please sign in to comment.