From 11eb142b5ad904093d499bf8af9b6fb9cfb146c7 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 28 Aug 2024 17:30:45 -0400 Subject: [PATCH] fmt: add new `serde` sub-module for integer timestamp integration This adds a new `jiff::fmt::serde` sub-module that contains helper sub-modules that work with Serde's `with` attribute. For example: ```rust use jiff::Timestamp; struct Record { #[serde(with = "jiff::fmt::serde::timestamp::second::required")] timestamp: Timestamp, } let json = r#"{"timestamp":1517644800}"#; let got: Record = serde_json::from_str(&json)?; assert_eq!(got.timestamp, Timestamp::from_second(1517644800)?); assert_eq!(serde_json::to_string(&got)?, json); ``` This is inspired in part by how Chrono supports a similar use case. It is expected that the behavior should be the same, although this implementation does support the full gamut of integer types (including a 128-bit integer number of nanoseconds). Moreover, the naming is different. Chrono uses a flatter namespace, where as here, we bury everything into sub-modules. The idea is to leave some room for future expansion, although I'm not sure there is much else to add. I also feel like spelling out `timestamp` instead of `ts` is a bit clearer. The module paths are quite long, e.g., `jiff::fmt::serde::timestamp::second::required` and `jiff::fmt::serde::timestamp::second::optional`. But they are predictable. And to mitigate users needing to click around through a deep module tree, we include the full tree in the `jiff::fmt::serde` module documentation. Ref #100, Closes #101 --- .github/workflows/ci.yml | 6 +- CHANGELOG.md | 2 + Cargo.toml | 2 +- src/fmt/mod.rs | 2 + src/fmt/serde.rs | 940 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 36 +- 6 files changed, 983 insertions(+), 5 deletions(-) create mode 100644 src/fmt/serde.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6bafa4c..d7543af7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: with: toolchain: ${{ matrix.rust }} - run: cargo build --verbose - - run: cargo doc --verbose + - run: cargo doc --features serde --verbose - run: cargo test --verbose --all - run: cargo test --verbose -p jiff-cli # Skip on Windows because it takes freaking forever. @@ -82,7 +82,7 @@ jobs: with: toolchain: stable-x86_64-gnu - run: cargo build --verbose - - run: cargo doc --verbose + - run: cargo doc --features serde --verbose - run: cargo test --verbose --lib - run: cargo test --verbose --test integration @@ -109,7 +109,7 @@ jobs: - name: Build jiff-tzdb-platform run: cargo build -p jiff-tzdb-platform --verbose - name: Build docs - run: cargo doc --verbose + run: cargo doc --features serde --verbose - name: Run library tests run: cargo test --lib - name: Run integration tests diff --git a/CHANGELOG.md b/CHANGELOG.md index a4c5b393..bae395dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ Add `BrokenDownTime::set_{offset,iana_time_zone}` APIs. * [#93](https://github.com/BurntSushi/jiff/issues/93): Add note about using `Timestamp::now().to_zoned()` instead of `Zoned::now().with_time_zone()`. +* [#101](https://github.com/BurntSushi/jiff/issues/101): +Add new `jiff::fmt::serde` module for integration with integer timestamps. * [#117](https://github.com/BurntSushi/jiff/pull/117): Remove `unsafe` usage in `libm` functions (applicable only to no-std users). diff --git a/Cargo.toml b/Cargo.toml index 16d5493e..790530d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,7 +101,7 @@ anyhow = "1.0.81" chrono = { version = "0.4.38", features = ["serde"] } chrono-tz = "0.9.0" insta = "1.39.0" -# We force `serde` to be enable in dev mode so that the docs render and test +# We force `serde` to be enabled in dev mode so that the docs render and test # correctly. This is highly suspicious. jiff = { path = "./", features = ["serde"] } quickcheck = { version = "1.0.3", default-features = false } diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index a0a185ab..96265b31 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -19,6 +19,8 @@ use self::util::{Decimal, DecimalFormatter, Fractional, FractionalFormatter}; mod offset; pub mod rfc2822; mod rfc9557; +#[cfg(feature = "serde")] +pub mod serde; pub mod strtime; pub mod temporal; mod util; diff --git a/src/fmt/serde.rs b/src/fmt/serde.rs new file mode 100644 index 00000000..bdbc9892 --- /dev/null +++ b/src/fmt/serde.rs @@ -0,0 +1,940 @@ +/*! +This module provides helpers to use with [Serde]. + +The helpers are exposed as modules meant to be used with +Serde's [`with` attribute]. + +At present, the helpers are limited to serializing and deserializing +[`Timestamp`](crate::Timestamp) values as an integer number of seconds, +milliseconds, microseconds or nanoseconds. + +# Module hierarchy + +The available helpers can be more quickly understood by looking at a fully +rendered tree of this module's hierarchy. Only the leaves of the tree are +usable with Serde's `with` attribute. For each leaf, the full path is spelled +out for easy copy & paste. + +* [`timestamp`] + * [`second`](self::timestamp::second) + * [`jiff::fmt::serde::timestamp::second::required`](self::timestamp::second::required) + * [`jiff::fmt::serde::timestamp::second::optional`](self::timestamp::second::optional) + * [`millisecond`](self::timestamp::millisecond) + * [`jiff::fmt::serde::timestamp::millisecond::required`](self::timestamp::millisecond::required) + * [`jiff::fmt::serde::timestamp::millisecond::optional`](self::timestamp::millisecond::optional) + * [`microsecond`](self::timestamp::millisecond) + * [`jiff::fmt::serde::timestamp::microsecond::required`](self::timestamp::microsecond::required) + * [`jiff::fmt::serde::timestamp::microsecond::optional`](self::timestamp::microsecond::optional) + * [`nanosecond`](self::timestamp::millisecond) + * [`jiff::fmt::serde::timestamp::nanosecond::required`](self::timestamp::nanosecond::required) + * [`jiff::fmt::serde::timestamp::nanosecond::optional`](self::timestamp::nanosecond::optional) + +# Advice + +In general, these helpers should only be used to interface with "legacy" APIs +that transmit times as integer number of seconds (or milliseconds or whatever). +If you're designing a new API and need to transmit instants in time that don't +care about time zones, then you should use `Timestamp` directly. It will +automatically use RFC 3339. (And if you do want to include the time zone, then +using [`Zoned`](crate::Zoned) directly will work as well by utilizing the +RFC 9557 extension to RFC 3339.) + +# Example + +This example shows how to deserialize an integer number of seconds since the +Unix epoch into a [`Timestamp`](crate::Timestamp). And the reverse operation +for serialization: + +``` +use jiff::Timestamp; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct Record { + #[serde(with = "jiff::fmt::serde::timestamp::second::required")] + timestamp: Timestamp, +} + +let json = r#"{"timestamp":1517644800}"#; +let got: Record = serde_json::from_str(&json)?; +assert_eq!(got.timestamp, Timestamp::from_second(1517644800)?); +assert_eq!(serde_json::to_string(&got)?, json); + +# Ok::<(), Box>(()) +``` + +# Example: optional support + +And this example shows how to use an `Option` instead of a +`Timestamp`. Note that in this case, we show how to roundtrip the number of +**milliseconds** since the Unix epoch: + +``` +use jiff::Timestamp; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct Record { + #[serde(with = "jiff::fmt::serde::timestamp::millisecond::optional")] + timestamp: Option, +} + +let json = r#"{"timestamp":1517644800123}"#; +let got: Record = serde_json::from_str(&json)?; +assert_eq!(got.timestamp, Some(Timestamp::from_millisecond(1517644800_123)?)); +assert_eq!(serde_json::to_string(&got)?, json); + +# Ok::<(), Box>(()) +``` + +[Serde]: https://serde.rs/ +[`with` attribute]: https://serde.rs/field-attrs.html#with +*/ + +/// Convenience routines for (de)serializing [`Timestamp`](crate::Timestamp) as +/// raw integer values. +pub mod timestamp { + use serde::de; + + /// A generic visitor for `Option`. + struct OptionalVisitor(V); + + impl<'de, V: de::Visitor<'de, Value = crate::Timestamp>> de::Visitor<'de> + for OptionalVisitor + { + type Value = Option; + + fn expecting( + &self, + f: &mut core::fmt::Formatter, + ) -> core::fmt::Result { + f.write_str( + "an integer number of seconds from the Unix epoch or `None`", + ) + } + + #[inline] + fn visit_some>( + self, + de: D, + ) -> Result, D::Error> { + de.deserialize_i64(self.0).map(Some) + } + + #[inline] + fn visit_none( + self, + ) -> Result, E> { + Ok(None) + } + } + + /// (De)serialize an integer number of seconds from the Unix epoch. + pub mod second { + use serde::de; + + struct Visitor; + + impl<'de> de::Visitor<'de> for Visitor { + type Value = crate::Timestamp; + + fn expecting( + &self, + f: &mut core::fmt::Formatter, + ) -> core::fmt::Result { + f.write_str("an integer number of seconds from the Unix epoch") + } + + #[inline] + fn visit_i8( + self, + v: i8, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_u8( + self, + v: u8, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_i16( + self, + v: i16, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_u16( + self, + v: u16, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_i32( + self, + v: i32, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_u32( + self, + v: u32, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_i64( + self, + v: i64, + ) -> Result { + crate::Timestamp::from_second(v).map_err(de::Error::custom) + } + + #[inline] + fn visit_u64( + self, + v: u64, + ) -> Result { + let v = i64::try_from(v).map_err(|_| { + de::Error::custom(alloc::format!( + "got unsigned integer {v} seconds, \ + which is too big to fit in a Jiff `Timestamp`", + )) + })?; + self.visit_i64(v) + } + + #[inline] + fn visit_i128( + self, + v: i128, + ) -> Result { + let v = i64::try_from(v).map_err(|_| { + de::Error::custom(alloc::format!( + "got signed integer {v} seconds, \ + which is too big to fit in a Jiff `Timestamp`", + )) + })?; + self.visit_i64(v) + } + + #[inline] + fn visit_u128( + self, + v: u128, + ) -> Result { + let v = i64::try_from(v).map_err(|_| { + de::Error::custom(alloc::format!( + "got unsigned integer {v} seconds, \ + which is too big to fit in a Jiff `Timestamp`", + )) + })?; + self.visit_i64(v) + } + } + + /// (De)serialize a required integer number of seconds from the Unix + /// epoch. + pub mod required { + /// Serialize a required integer number of seconds since the Unix + /// epoch. + #[inline] + pub fn serialize( + timestamp: &crate::Timestamp, + se: S, + ) -> Result { + se.serialize_i64(timestamp.as_second()) + } + + /// Deserialize a required integer number of seconds since the + /// Unix epoch. + #[inline] + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + de: D, + ) -> Result { + de.deserialize_i64(super::Visitor) + } + } + + /// (De)serialize an optional integer number of seconds from the Unix + /// epoch. + pub mod optional { + /// Serialize an optional integer number of seconds since the Unix + /// epoch. + #[inline] + pub fn serialize( + timestamp: &Option, + se: S, + ) -> Result { + match *timestamp { + None => se.serialize_none(), + Some(ts) => se.serialize_i64(ts.as_second()), + } + } + + /// Deserialize an optional integer number of seconds since the + /// Unix epoch. + #[inline] + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + de: D, + ) -> Result, D::Error> { + de.deserialize_option(super::super::OptionalVisitor( + super::Visitor, + )) + } + } + } + + /// (De)serialize an integer number of milliseconds from the Unix epoch. + pub mod millisecond { + use serde::de; + + struct Visitor; + + impl<'de> de::Visitor<'de> for Visitor { + type Value = crate::Timestamp; + + fn expecting( + &self, + f: &mut core::fmt::Formatter, + ) -> core::fmt::Result { + f.write_str( + "an integer number of milliseconds from the Unix epoch", + ) + } + + #[inline] + fn visit_i8( + self, + v: i8, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_u8( + self, + v: u8, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_i16( + self, + v: i16, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_u16( + self, + v: u16, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_i32( + self, + v: i32, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_u32( + self, + v: u32, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_i64( + self, + v: i64, + ) -> Result { + crate::Timestamp::from_millisecond(v) + .map_err(de::Error::custom) + } + + #[inline] + fn visit_u64( + self, + v: u64, + ) -> Result { + let v = i64::try_from(v).map_err(|_| { + de::Error::custom(alloc::format!( + "got unsigned integer {v} milliseconds, \ + which is too big to fit in a Jiff `Timestamp`", + )) + })?; + self.visit_i64(v) + } + + #[inline] + fn visit_i128( + self, + v: i128, + ) -> Result { + let v = i64::try_from(v).map_err(|_| { + de::Error::custom(alloc::format!( + "got signed integer {v} milliseconds, \ + which is too big to fit in a Jiff `Timestamp`", + )) + })?; + self.visit_i64(v) + } + + #[inline] + fn visit_u128( + self, + v: u128, + ) -> Result { + let v = i64::try_from(v).map_err(|_| { + de::Error::custom(alloc::format!( + "got unsigned integer {v} milliseconds, \ + which is too big to fit in a Jiff `Timestamp`", + )) + })?; + self.visit_i64(v) + } + } + + /// (De)serialize a required integer number of milliseconds from the + /// Unix epoch. + pub mod required { + /// Serialize a required integer number of milliseconds since the + /// Unix epoch. + #[inline] + pub fn serialize( + timestamp: &crate::Timestamp, + se: S, + ) -> Result { + se.serialize_i64(timestamp.as_millisecond()) + } + + /// Deserialize a required integer number of milliseconds since the + /// Unix epoch. + #[inline] + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + de: D, + ) -> Result { + de.deserialize_i64(super::Visitor) + } + } + + /// (De)serialize an optional integer number of milliseconds from the + /// Unix epoch. + pub mod optional { + /// Serialize an optional integer number of milliseconds since the + /// Unix epoch. + #[inline] + pub fn serialize( + timestamp: &Option, + se: S, + ) -> Result { + match *timestamp { + None => se.serialize_none(), + Some(ts) => se.serialize_i64(ts.as_millisecond()), + } + } + + /// Deserialize an optional integer number of milliseconds since + /// the Unix epoch. + #[inline] + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + de: D, + ) -> Result, D::Error> { + de.deserialize_option(super::super::OptionalVisitor( + super::Visitor, + )) + } + } + } + + /// (De)serialize an integer number of microseconds from the Unix epoch. + pub mod microsecond { + use serde::de; + + struct Visitor; + + impl<'de> de::Visitor<'de> for Visitor { + type Value = crate::Timestamp; + + fn expecting( + &self, + f: &mut core::fmt::Formatter, + ) -> core::fmt::Result { + f.write_str( + "an integer number of microseconds from the Unix epoch", + ) + } + + #[inline] + fn visit_i8( + self, + v: i8, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_u8( + self, + v: u8, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_i16( + self, + v: i16, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_u16( + self, + v: u16, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_i32( + self, + v: i32, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_u32( + self, + v: u32, + ) -> Result { + self.visit_i64(i64::from(v)) + } + + #[inline] + fn visit_i64( + self, + v: i64, + ) -> Result { + crate::Timestamp::from_microsecond(v) + .map_err(de::Error::custom) + } + + #[inline] + fn visit_u64( + self, + v: u64, + ) -> Result { + let v = i64::try_from(v).map_err(|_| { + de::Error::custom(alloc::format!( + "got unsigned integer {v} microseconds, \ + which is too big to fit in a Jiff `Timestamp`", + )) + })?; + self.visit_i64(v) + } + + #[inline] + fn visit_i128( + self, + v: i128, + ) -> Result { + let v = i64::try_from(v).map_err(|_| { + de::Error::custom(alloc::format!( + "got signed integer {v} microseconds, \ + which is too big to fit in a Jiff `Timestamp`", + )) + })?; + self.visit_i64(v) + } + + #[inline] + fn visit_u128( + self, + v: u128, + ) -> Result { + let v = i64::try_from(v).map_err(|_| { + de::Error::custom(alloc::format!( + "got unsigned integer {v} microseconds, \ + which is too big to fit in a Jiff `Timestamp`", + )) + })?; + self.visit_i64(v) + } + } + + /// (De)serialize a required integer number of microseconds from the + /// Unix epoch. + pub mod required { + /// Serialize a required integer number of microseconds since the + /// Unix epoch. + #[inline] + pub fn serialize( + timestamp: &crate::Timestamp, + se: S, + ) -> Result { + se.serialize_i64(timestamp.as_microsecond()) + } + + /// Deserialize a required integer number of microseconds since the + /// Unix epoch. + #[inline] + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + de: D, + ) -> Result { + de.deserialize_i64(super::Visitor) + } + } + + /// (De)serialize an optional integer number of microseconds from the + /// Unix epoch. + pub mod optional { + /// Serialize an optional integer number of microseconds since the + /// Unix epoch. + #[inline] + pub fn serialize( + timestamp: &Option, + se: S, + ) -> Result { + match *timestamp { + None => se.serialize_none(), + Some(ts) => se.serialize_i64(ts.as_microsecond()), + } + } + + /// Deserialize an optional integer number of microseconds since + /// the Unix epoch. + #[inline] + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + de: D, + ) -> Result, D::Error> { + de.deserialize_option(super::super::OptionalVisitor( + super::Visitor, + )) + } + } + } + + /// (De)serialize an integer number of nanoseconds from the Unix epoch. + pub mod nanosecond { + use serde::de; + + struct Visitor; + + impl<'de> de::Visitor<'de> for Visitor { + type Value = crate::Timestamp; + + fn expecting( + &self, + f: &mut core::fmt::Formatter, + ) -> core::fmt::Result { + f.write_str( + "an integer number of nanoseconds from the Unix epoch", + ) + } + + #[inline] + fn visit_i64( + self, + v: i64, + ) -> Result { + self.visit_i128(i128::from(v)) + } + + #[inline] + fn visit_u64( + self, + v: u64, + ) -> Result { + self.visit_u128(u128::from(v)) + } + + #[inline] + fn visit_i128( + self, + v: i128, + ) -> Result { + crate::Timestamp::from_nanosecond(v).map_err(de::Error::custom) + } + + #[inline] + fn visit_u128( + self, + v: u128, + ) -> Result { + let v = i128::try_from(v).map_err(|_| { + de::Error::custom(alloc::format!( + "got unsigned integer {v} nanoseconds, \ + which is too big to fit in a Jiff `Timestamp`", + )) + })?; + self.visit_i128(v) + } + } + + /// (De)serialize a required integer number of nanoseconds from the + /// Unix epoch. + pub mod required { + /// Serialize a required integer number of nanoseconds since the + /// Unix epoch. + #[inline] + pub fn serialize( + timestamp: &crate::Timestamp, + se: S, + ) -> Result { + se.serialize_i128(timestamp.as_nanosecond()) + } + + /// Deserialize a required integer number of nanoseconds since the + /// Unix epoch. + #[inline] + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + de: D, + ) -> Result { + de.deserialize_i128(super::Visitor) + } + } + + /// (De)serialize an optional integer number of nanoseconds from the + /// Unix epoch. + pub mod optional { + /// Serialize an optional integer number of nanoseconds since the + /// Unix epoch. + #[inline] + pub fn serialize( + timestamp: &Option, + se: S, + ) -> Result { + match *timestamp { + None => se.serialize_none(), + Some(ts) => se.serialize_i128(ts.as_nanosecond()), + } + } + + /// Deserialize an optional integer number of nanoseconds since the + /// Unix epoch. + #[inline] + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + de: D, + ) -> Result, D::Error> { + de.deserialize_option(super::super::OptionalVisitor( + super::Visitor, + )) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::Timestamp; + + #[test] + fn timestamp_second_required() { + #[derive(Debug, serde::Deserialize, serde::Serialize)] + struct Data { + #[serde(with = "crate::fmt::serde::timestamp::second::required")] + ts: Timestamp, + } + + let json = r#"{"ts":1517644800}"#; + let got: Data = serde_json::from_str(&json).unwrap(); + assert_eq!(got.ts, Timestamp::from_second(1517644800).unwrap()); + assert_eq!(serde_json::to_string(&got).unwrap(), json); + } + + #[test] + fn timestamp_second_optional() { + #[derive(Debug, serde::Deserialize, serde::Serialize)] + struct Data { + #[serde(with = "crate::fmt::serde::timestamp::second::optional")] + ts: Option, + } + + let json = r#"{"ts":1517644800}"#; + let got: Data = serde_json::from_str(&json).unwrap(); + assert_eq!(got.ts, Some(Timestamp::from_second(1517644800).unwrap())); + assert_eq!(serde_json::to_string(&got).unwrap(), json); + } + + #[test] + fn timestamp_millisecond_required() { + #[derive(Debug, serde::Deserialize, serde::Serialize)] + struct Data { + #[serde( + with = "crate::fmt::serde::timestamp::millisecond::required" + )] + ts: Timestamp, + } + + let json = r#"{"ts":1517644800000}"#; + let got: Data = serde_json::from_str(&json).unwrap(); + assert_eq!( + got.ts, + Timestamp::from_millisecond(1517644800_000).unwrap() + ); + assert_eq!(serde_json::to_string(&got).unwrap(), json); + + let json = r#"{"ts":1517644800123}"#; + let got: Data = serde_json::from_str(&json).unwrap(); + assert_eq!( + got.ts, + Timestamp::from_millisecond(1517644800_123).unwrap() + ); + assert_eq!(serde_json::to_string(&got).unwrap(), json); + } + + #[test] + fn timestamp_millisecond_optional() { + #[derive(Debug, serde::Deserialize, serde::Serialize)] + struct Data { + #[serde( + with = "crate::fmt::serde::timestamp::millisecond::optional" + )] + ts: Option, + } + + let json = r#"{"ts":1517644800000}"#; + let got: Data = serde_json::from_str(&json).unwrap(); + assert_eq!( + got.ts, + Some(Timestamp::from_millisecond(1517644800_000).unwrap()) + ); + assert_eq!(serde_json::to_string(&got).unwrap(), json); + + let json = r#"{"ts":1517644800123}"#; + let got: Data = serde_json::from_str(&json).unwrap(); + assert_eq!( + got.ts, + Some(Timestamp::from_millisecond(1517644800_123).unwrap()) + ); + assert_eq!(serde_json::to_string(&got).unwrap(), json); + } + + #[test] + fn timestamp_microsecond_required() { + #[derive(Debug, serde::Deserialize, serde::Serialize)] + struct Data { + #[serde( + with = "crate::fmt::serde::timestamp::microsecond::required" + )] + ts: Timestamp, + } + + let json = r#"{"ts":1517644800000000}"#; + let got: Data = serde_json::from_str(&json).unwrap(); + assert_eq!( + got.ts, + Timestamp::from_microsecond(1517644800_000000).unwrap() + ); + assert_eq!(serde_json::to_string(&got).unwrap(), json); + + let json = r#"{"ts":1517644800123456}"#; + let got: Data = serde_json::from_str(&json).unwrap(); + assert_eq!( + got.ts, + Timestamp::from_microsecond(1517644800_123456).unwrap() + ); + assert_eq!(serde_json::to_string(&got).unwrap(), json); + } + + #[test] + fn timestamp_microsecond_optional() { + #[derive(Debug, serde::Deserialize, serde::Serialize)] + struct Data { + #[serde( + with = "crate::fmt::serde::timestamp::microsecond::optional" + )] + ts: Option, + } + + let json = r#"{"ts":1517644800000000}"#; + let got: Data = serde_json::from_str(&json).unwrap(); + assert_eq!( + got.ts, + Some(Timestamp::from_microsecond(1517644800_000000).unwrap()) + ); + assert_eq!(serde_json::to_string(&got).unwrap(), json); + + let json = r#"{"ts":1517644800123456}"#; + let got: Data = serde_json::from_str(&json).unwrap(); + assert_eq!( + got.ts, + Some(Timestamp::from_microsecond(1517644800_123456).unwrap()) + ); + assert_eq!(serde_json::to_string(&got).unwrap(), json); + } + + #[test] + fn timestamp_nanosecond_required() { + #[derive(Debug, serde::Deserialize, serde::Serialize)] + struct Data { + #[serde( + with = "crate::fmt::serde::timestamp::nanosecond::required" + )] + ts: Timestamp, + } + + let json = r#"{"ts":1517644800000000000}"#; + let got: Data = serde_json::from_str(&json).unwrap(); + assert_eq!( + got.ts, + Timestamp::from_nanosecond(1517644800_000000000).unwrap() + ); + assert_eq!(serde_json::to_string(&got).unwrap(), json); + + let json = r#"{"ts":1517644800123456789}"#; + let got: Data = serde_json::from_str(&json).unwrap(); + assert_eq!( + got.ts, + Timestamp::from_nanosecond(1517644800_123456789).unwrap() + ); + assert_eq!(serde_json::to_string(&got).unwrap(), json); + } + + #[test] + fn timestamp_nanosecond_optional() { + #[derive(Debug, serde::Deserialize, serde::Serialize)] + struct Data { + #[serde( + with = "crate::fmt::serde::timestamp::nanosecond::optional" + )] + ts: Option, + } + + let json = r#"{"ts":1517644800000000000}"#; + let got: Data = serde_json::from_str(&json).unwrap(); + assert_eq!( + got.ts, + Some(Timestamp::from_nanosecond(1517644800_000000000).unwrap()) + ); + assert_eq!(serde_json::to_string(&got).unwrap(), json); + + let json = r#"{"ts":1517644800123456789}"#; + let got: Data = serde_json::from_str(&json).unwrap(); + assert_eq!( + got.ts, + Some(Timestamp::from_nanosecond(1517644800_123456789).unwrap()) + ); + assert_eq!(serde_json::to_string(&got).unwrap(), json); + } +} diff --git a/src/lib.rs b/src/lib.rs index 78fb33c7..a1d19b04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -196,6 +196,7 @@ subsequent runs shouldn't have to re-compile the dependencies. * [Parsing a span](#parsing-a-span) * [Parsing an RFC 2822 datetime string](#parsing-an-rfc-2822-datetime-string) * [Using `strftime` and `strptime` for formatting and parsing](#using-strftime-and-strptime-for-formatting-and-parsing) +* [Serializing and deserializing integer timestamps with Serde](#serializing-and-deserializing-integer-timestamps-with-serde) ### Get the current time in your system's time zone @@ -529,6 +530,39 @@ specifiers and other APIs. [`strftime`]: https://pubs.opengroup.org/onlinepubs/009695399/functions/strftime.html [`strptime`]: https://pubs.opengroup.org/onlinepubs/009695399/functions/strptime.html +### Serializing and deserializing integer timestamps with Serde + +Sometimes you need to interact with external services that use integer timestamps +instead of something more civilized like RFC 3339. Since [`Timestamp`]'s +Serde integration uses RFC 3339, you'll need to override the default. While +you could hand-write this, Jiff provides convenience routines that do this +for you. But you do need to wire it up via [Serde's `with` attribute]: + +``` +use jiff::Timestamp; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct Record { + #[serde(with = "jiff::fmt::serde::timestamp::second::required")] + timestamp: Timestamp, +} + +let json = r#"{"timestamp":1517644800}"#; +let got: Record = serde_json::from_str(&json)?; +assert_eq!(got.timestamp, Timestamp::from_second(1517644800)?); +assert_eq!(serde_json::to_string(&got)?, json); + +# Ok::<(), Box>(()) +``` + +If you need to support optional timestamps via `Option`, then use +`jiff::fmt::serde::timestamp::second::optional` instead. + +For more, see the [`fmt::serde`] sub-module. (This requires enabling Jiff's +`serde` crate feature.) + +[Serde's `with` attribute]: https://serde.rs/field-attrs.html#with + # Crate features ### Ecosystem features @@ -594,7 +628,7 @@ specifiers and other APIs. // Lots of rustdoc links break when disabling default features because docs // aren't written conditionally. #![cfg_attr( - all(feature = "std", feature = "tzdb-zoneinfo"), + all(feature = "std", feature = "serde", feature = "tzdb-zoneinfo"), deny(rustdoc::broken_intra_doc_links) )] // These are just too annoying to squash otherwise.