diff --git a/Cargo.lock b/Cargo.lock index e3919189..59450fc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,7 @@ dependencies = [ "getrandom 0.3.2", "hex", "indexmap 2.10.0", + "jiff", "js-sys", "once_cell", "pretty_assertions", @@ -519,6 +520,30 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -623,6 +648,21 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 307fc803..c540ae22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,8 @@ default = ["compat-3-0-0"] compat-3-0-0 = [] # if enabled, include API for interfacing with chrono 0.4 chrono-0_4 = ["dep:chrono"] +# if enabled, include API for interfacing with jiff 0.2 +jiff-0_2 = ["dep:jiff"] # enable the large-dates feature for the time crate large_dates = ["time/large-dates"] # if enabled, include API for interfacing with uuid 1.x @@ -56,6 +58,7 @@ name = "bson" [dependencies] ahash = "0.8.0" chrono = { version = "0.4.15", features = ["std"], default-features = false, optional = true } +jiff = { version = "0.2", default-features = false, optional = true } rand = "0.9" serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0", features = ["preserve_order"], optional = true } @@ -87,6 +90,7 @@ serde_bytes = "0.11" serde_path_to_error = "0.1.16" serde_json = "1" chrono = { version = "0.4", features = ["serde", "clock", "std"], default-features = false } +jiff = { version = "0.2", default-features = false, features = ["std"] } [package.metadata.docs.rs] all-features = true diff --git a/src/bson.rs b/src/bson.rs index 7a0105e6..b131e5cb 100644 --- a/src/bson.rs +++ b/src/bson.rs @@ -408,6 +408,13 @@ impl From> for Bson { } } +#[cfg(feature = "jiff-0_2")] +impl From for Bson { + fn from(a: jiff::Timestamp) -> Bson { + Bson::DateTime(crate::DateTime::from(a)) + } +} + #[cfg(feature = "uuid-1")] impl From for Bson { fn from(uuid: uuid::Uuid) -> Self { diff --git a/src/datetime.rs b/src/datetime.rs index c6430016..4175c1a6 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -12,7 +12,7 @@ use std::{ use chrono::{LocalResult, TimeZone, Utc}; #[cfg(all( feature = "serde_with-3", - any(feature = "chrono-0_4", feature = "time-0_3") + any(feature = "chrono-0_4", feature = "time-0_3", feature = "jiff-0_2") ))] use serde::{Deserialize, Deserializer, Serialize}; use time::format_description::well_known::Rfc3339; @@ -215,6 +215,13 @@ impl crate::DateTime { Self::from_millis(dt.timestamp_millis()) } + /// Convert the given [`jiff::Timestamp`] into a [`bson::DateTime`](DateTime), truncating it to + /// millisecond precision. + #[cfg(feature = "jiff-0_2")] + pub fn from_jiff(ts: jiff::Timestamp) -> Self { + Self::from_millis(ts.as_millisecond()) + } + /// Returns a builder used to construct a [`DateTime`] from a given year, month, /// day, and optionally, an hour, minute, second and millisecond, which default to /// 0 if not explicitly set. @@ -253,6 +260,32 @@ impl crate::DateTime { } } + /// Convert this [`DateTime`] to a [`jiff::Timestamp`]. + /// + /// Note: Not every BSON datetime can be represented as a [`jiff::Timestamp`]. For such dates, + /// [`jiff::Timestamp::MIN`] or [`jiff::Timestamp::MAX`] will be returned, whichever + /// is closer. + /// + /// ``` + /// let bson_dt = bson::DateTime::now(); + /// let jiff_ts = bson_dt.to_jiff(); + /// assert_eq!(bson_dt.timestamp_millis(), jiff_ts.as_millisecond()); + /// + /// let big = bson::DateTime::from_millis(i64::MAX); + /// let jiff_big = big.to_jiff(); + /// assert_eq!(jiff_big, jiff::Timestamp::MAX) + /// ``` + #[cfg(feature = "jiff-0_2")] + pub fn to_jiff(self) -> jiff::Timestamp { + jiff::Timestamp::from_millisecond(self.0).unwrap_or({ + if self.0 < 0 { + jiff::Timestamp::MIN + } else { + jiff::Timestamp::MAX + } + }) + } + fn from_time_private(dt: time::OffsetDateTime) -> Self { let millis = dt.unix_timestamp_nanos() / 1_000_000; match millis.try_into() { @@ -488,6 +521,45 @@ impl serde_with::SerializeAs> for crate::DateTime { } } +#[cfg(feature = "jiff-0_2")] +impl From for jiff::Timestamp { + fn from(bson_dt: DateTime) -> Self { + bson_dt.to_jiff() + } +} + +#[cfg(feature = "jiff-0_2")] +impl From for crate::DateTime { + fn from(x: jiff::Timestamp) -> Self { + Self::from_jiff(x) + } +} + +#[cfg(all(feature = "jiff-0_2", feature = "serde_with-3"))] +impl<'de> serde_with::DeserializeAs<'de, jiff::Timestamp> for crate::DateTime { + fn deserialize_as(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let dt = DateTime::deserialize(deserializer)?; + Ok(dt.to_jiff()) + } +} + +#[cfg(all(feature = "jiff-0_2", feature = "serde_with-3"))] +impl serde_with::SerializeAs for crate::DateTime { + fn serialize_as( + source: &jiff::Timestamp, + serializer: S, + ) -> std::result::Result + where + S: serde::Serializer, + { + let dt = DateTime::from_jiff(*source); + dt.serialize(serializer) + } +} + #[cfg(feature = "time-0_3")] impl From for time::OffsetDateTime { fn from(bson_dt: DateTime) -> Self { diff --git a/src/lib.rs b/src/lib.rs index ce6af55c..6ef05641 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,6 +62,7 @@ //! | Feature | Description | Default | //! |:-------------|:-----------------------------------------------------------------------------------------------------|:--------| //! | `chrono-0_4` | Enable support for v0.4 of the [`chrono`](https://docs.rs/chrono/0.4) crate in the public API. | no | +//! | `jiff-0_2` | Enable support for v0.2 of the [`jiff`](https://docs.rs/jiff/0.2) crate in the public API. | no | //! | `uuid-1` | Enable support for v1.x of the [`uuid`](https://docs.rs/uuid/1.x) crate in the public API. | no | //! | `time-0_3` | Enable support for v0.3 of the [`time`](https://docs.rs/time/0.3) crate in the public API. | no | //! | `serde_with-3` | Enable [`serde_with`](https://docs.rs/serde_with/3.x) 3.x integrations for [`DateTime`] and [`Uuid`]. | no | diff --git a/src/serde_helpers.rs b/src/serde_helpers.rs index 24774e86..79b0620e 100644 --- a/src/serde_helpers.rs +++ b/src/serde_helpers.rs @@ -81,6 +81,8 @@ pub mod object_id { /// - [`datetime::FromI64`] — converts an `i64` to and from a [`crate::DateTime`]. /// - [`datetime::FromChrono04DateTime`] — converts a [`chrono::DateTime`] to and from a /// [`crate::DateTime`]. +/// - [`datetime::FromJiff02Timestamp`] — converts a [`jiff::Timestamp`] to and from a +/// [`crate::DateTime`]. /// - [`datetime::FromTime03OffsetDateTime`] — converts a [`time::OffsetDateTime`] to and from a /// [`crate::DateTime`]. #[cfg(feature = "serde_with-3")] @@ -200,6 +202,33 @@ pub mod datetime { } ); + #[cfg(feature = "jiff-0_2")] + serde_conv_doc!( + /// Converts a [`jiff::Timestamp`] to and from a [`DateTime`]. + /// ```rust + /// # #[cfg(all(feature = "jiff-0_2", feature = "serde_with-3"))] + /// # { + /// use bson::serde_helpers::datetime; + /// use serde::{Serialize, Deserialize}; + /// use serde_with::serde_as; + /// #[serde_as] + /// #[derive(Serialize, Deserialize)] + /// struct Event { + /// #[serde_as(as = "datetime::FromJiff02Timestamp")] + /// pub date: jiff::Timestamp, + /// } + /// # } + /// ``` + pub FromJiff02Timestamp, + jiff::Timestamp, + |jiff_ts: &jiff::Timestamp| -> Result { + Ok(DateTime::from_jiff(*jiff_ts)) + }, + |bson_date: DateTime| -> Result { + Ok(bson_date.to_jiff()) + } + ); + #[cfg(feature = "time-0_3")] serde_conv_doc!( /// Converts a [`time::OffsetDateTime`] to and from a [`DateTime`]. diff --git a/src/tests/modules/bson.rs b/src/tests/modules/bson.rs index f9d3636f..71f6eb7c 100644 --- a/src/tests/modules/bson.rs +++ b/src/tests/modules/bson.rs @@ -274,6 +274,15 @@ fn from_external_datetime() { let from_chrono = DateTime::from(now); assert_millisecond_precision(from_chrono); } + #[cfg(feature = "jiff-0_2")] + { + let now = jiff::Timestamp::now(); + let bson = Bson::from(now); + assert_millisecond_precision(bson.as_datetime().unwrap().to_owned()); + + let from_jiff = DateTime::from(now); + assert_millisecond_precision(from_jiff); + } let no_subsec_millis = datetime!(2014-11-28 12:00:09 UTC); let dt = DateTime::from_time_0_3(no_subsec_millis); @@ -298,6 +307,17 @@ fn from_external_datetime() { assert_millisecond_precision(bson.as_datetime().unwrap().to_owned()); assert_subsec_millis(bson.as_datetime().unwrap().to_owned(), 0); } + #[cfg(feature = "jiff-0_2")] + { + let no_subsec_millis: jiff::Timestamp = "2014-11-28T12:00:09Z".parse().unwrap(); + let dt = DateTime::from(no_subsec_millis); + assert_millisecond_precision(dt); + assert_subsec_millis(dt, 0); + + let bson = Bson::from(dt); + assert_millisecond_precision(bson.as_datetime().unwrap().to_owned()); + assert_subsec_millis(bson.as_datetime().unwrap().to_owned(), 0); + } for s in &[ "2014-11-28T12:00:09.123Z", @@ -327,6 +347,17 @@ fn from_external_datetime() { assert_millisecond_precision(bson.as_datetime().unwrap().to_owned()); assert_subsec_millis(bson.as_datetime().unwrap().to_owned(), 123); } + #[cfg(feature = "jiff-0_2")] + { + let jiff_ts: jiff::Timestamp = s.parse().unwrap(); + let dt = DateTime::from(jiff_ts); + assert_millisecond_precision(dt); + assert_subsec_millis(dt, 123); + + let bson = Bson::from(jiff_ts); + assert_millisecond_precision(bson.as_datetime().unwrap().to_owned()); + assert_subsec_millis(bson.as_datetime().unwrap().to_owned(), 123); + } } #[cfg(feature = "time-0_3")] @@ -373,6 +404,26 @@ fn from_external_datetime() { let bdt = DateTime::MIN; assert_eq!(bdt.to_chrono(), chrono::DateTime::::MIN_UTC); } + #[cfg(feature = "jiff-0_2")] + { + let bdt = DateTime::from(jiff::Timestamp::MAX); + assert_eq!( + bdt.to_jiff().as_millisecond(), + jiff::Timestamp::MAX.as_millisecond() + ); + + let bdt = DateTime::from(jiff::Timestamp::MIN); + assert_eq!( + bdt.to_jiff().as_millisecond(), + jiff::Timestamp::MIN.as_millisecond() + ); + + let bdt = DateTime::MAX; + assert_eq!(bdt.to_jiff(), jiff::Timestamp::MAX); + + let bdt = DateTime::MIN; + assert_eq!(bdt.to_jiff(), jiff::Timestamp::MIN); + } } #[test] diff --git a/src/tests/modules/serializer_deserializer.rs b/src/tests/modules/serializer_deserializer.rs index b58980fa..16b4ba37 100644 --- a/src/tests/modules/serializer_deserializer.rs +++ b/src/tests/modules/serializer_deserializer.rs @@ -326,7 +326,10 @@ fn test_serialize_utc_date_time() { #[allow(unused)] let src = time::OffsetDateTime::from_unix_timestamp(1_286_705_410).unwrap(); #[cfg(feature = "chrono-0_4")] + #[allow(unused)] let src = chrono::Utc.timestamp_opt(1_286_705_410, 0).unwrap(); + #[cfg(feature = "jiff-0_2")] + let src = jiff::Timestamp::from_second(1_286_705_410).unwrap(); let dst = vec![ 18, 0, 0, 0, 9, 107, 101, 121, 0, 208, 111, 158, 149, 43, 1, 0, 0, 0, ]; diff --git a/src/tests/serde.rs b/src/tests/serde.rs index 403bcbff..3996fcee 100644 --- a/src/tests/serde.rs +++ b/src/tests/serde.rs @@ -1081,6 +1081,78 @@ fn test_datetime_chrono04_datetime_helper() { ); } +#[test] +#[cfg(all(feature = "jiff-0_2", feature = "serde_with-3"))] +fn test_datetime_jiff02_timestamp_helper() { + let _guard = LOCK.run_concurrently(); + + use std::str::FromStr; + + #[serde_as] + #[derive(Deserialize, Serialize, Debug, PartialEq)] + struct A { + #[serde_as(as = "datetime::FromJiff02Timestamp")] + pub date: jiff::Timestamp, + + #[serde_as(as = "Option")] + pub date_optional_none: Option, + + #[serde_as(as = "Option")] + pub date_optional_some: Option, + + #[serde_as(as = "Vec")] + pub date_vector: Vec, + } + + let iso = "1996-12-20T00:39:57Z"; + let date: jiff::Timestamp = jiff::Timestamp::from_str(iso).unwrap(); + let a: A = A { + date, + date_optional_none: None, + date_optional_some: Some(date), + date_vector: vec![date], + }; + + // Serialize the struct to BSON + let doc = serialize_to_document(&a).unwrap(); + + // Validate serialized data + assert_eq!( + doc.get_datetime("date").unwrap().to_jiff(), + date, + "Expected serialized date to match original date." + ); + + assert_eq!( + doc.get("date_optional_none"), + Some(&Bson::Null), + "Expected serialized date_optional_none to be None." + ); + + assert_eq!( + doc.get("date_optional_some"), + Some(&Bson::DateTime(DateTime::from_jiff(date))), + "Expected serialized date_optional_some to match original." + ); + + let date_vector = doc + .get_array("date_vector") + .expect("Expected serialized date_vector to be a BSON array."); + let expected_date_vector: Vec = vec![Bson::DateTime(date.into())]; + assert_eq!( + date_vector, &expected_date_vector, + "Expected each serialized element in date_vector to be a BSON DateTime matching the \ + original." + ); + + // Validate deserialized data + let a_deserialized: A = deserialize_from_document(doc).unwrap(); + assert_eq!( + a_deserialized, a, + "Deserialized struct does not match original." + ); +} + #[test] #[cfg(all(feature = "time-0_3", feature = "serde_with-3"))] fn test_datetime_time03_offset_datetime_helper() {