Skip to content

RUST-2256 feat: Add integration with jiff::Timestamp #587

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/bson.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,13 @@ impl<T: chrono::TimeZone> From<chrono::DateTime<T>> for Bson {
}
}

#[cfg(feature = "jiff-0_2")]
impl From<jiff::Timestamp> for Bson {
fn from(a: jiff::Timestamp) -> Bson {
Bson::DateTime(crate::DateTime::from(a))
}
}

#[cfg(feature = "uuid-1")]
impl From<uuid::Uuid> for Bson {
fn from(uuid: uuid::Uuid) -> Self {
Expand Down
74 changes: 73 additions & 1 deletion src/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -488,6 +521,45 @@ impl serde_with::SerializeAs<chrono::DateTime<Utc>> for crate::DateTime {
}
}

#[cfg(feature = "jiff-0_2")]
impl From<crate::DateTime> for jiff::Timestamp {
fn from(bson_dt: DateTime) -> Self {
bson_dt.to_jiff()
}
}

#[cfg(feature = "jiff-0_2")]
impl From<jiff::Timestamp> 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<D>(deserializer: D) -> std::result::Result<jiff::Timestamp, D::Error>
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<jiff::Timestamp> for crate::DateTime {
fn serialize_as<S>(
source: &jiff::Timestamp,
serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let dt = DateTime::from_jiff(*source);
dt.serialize(serializer)
}
}

#[cfg(feature = "time-0_3")]
impl From<crate::DateTime> for time::OffsetDateTime {
fn from(bson_dt: DateTime) -> Self {
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
29 changes: 29 additions & 0 deletions src/serde_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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<DateTime, String> {
Ok(DateTime::from_jiff(*jiff_ts))
},
|bson_date: DateTime| -> Result<jiff::Timestamp, String> {
Ok(bson_date.to_jiff())
}
);

#[cfg(feature = "time-0_3")]
serde_conv_doc!(
/// Converts a [`time::OffsetDateTime`] to and from a [`DateTime`].
Expand Down
51 changes: 51 additions & 0 deletions src/tests/modules/bson.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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",
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -373,6 +404,26 @@ fn from_external_datetime() {
let bdt = DateTime::MIN;
assert_eq!(bdt.to_chrono(), chrono::DateTime::<Utc>::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]
Expand Down
3 changes: 3 additions & 0 deletions src/tests/modules/serializer_deserializer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
];
Expand Down
Loading