diff --git a/.evergreen/config.yml b/.evergreen/config.yml index e46e31e4..c11f96c8 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -181,9 +181,9 @@ axes: - id: "extra-rust-versions" values: - id: "min" - display_name: "1.48 (minimum supported version)" + display_name: "1.53 (minimum supported version)" variables: - RUST_VERSION: "1.48.0" + RUST_VERSION: "1.53.0" - id: "nightly" display_name: "nightly" variables: diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 6d56e8f6..f4991f6b 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -4,7 +4,7 @@ set -o errexit . ~/.cargo/env RUST_BACKTRACE=1 cargo test -RUST_BACKTRACE=1 cargo test --features chrono-0_4,uuid-0_8 +RUST_BACKTRACE=1 cargo test --all-features cd serde-tests RUST_BACKTRACE=1 cargo test diff --git a/Cargo.toml b/Cargo.toml index d575c3db..04579c38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,9 +33,11 @@ exclude = [ [features] default = [] # if enabled, include API for interfacing with chrono 0.4 -chrono-0_4 = [] +chrono-0_4 = ["chrono"] # if enabled, include API for interfacing with uuid 0.8 uuid-0_8 = [] +# if enabled, include API for interfacing with time 0.3 +time-0_3 = [] # if enabled, include serde_with interop. # should be used in conjunction with chrono-0_4 or uuid-0_8. # it's commented out here because Cargo implicitly adds a feature flag for @@ -47,7 +49,7 @@ name = "bson" [dependencies] ahash = "0.7.2" -chrono = { version = "0.4.15", features = ["std"], default-features = false } +chrono = { version = "0.4.15", features = ["std"], default-features = false, optional = true } rand = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } @@ -58,6 +60,7 @@ lazy_static = "1.4.0" uuid = { version = "0.8.1", features = ["serde", "v4"] } serde_bytes = "0.11.5" serde_with = { version = "1", optional = true } +time = { version = "0.3.9", features = ["formatting", "parsing", "macros", "large-dates"] } [dev-dependencies] assert_matches = "1.2" diff --git a/src/bson.rs b/src/bson.rs index bc9f2156..0bb825e0 100644 --- a/src/bson.rs +++ b/src/bson.rs @@ -26,7 +26,6 @@ use std::{ fmt::{self, Debug, Display, Formatter}, }; -use chrono::Datelike; use serde_json::{json, Value}; pub use crate::document::Document; @@ -311,6 +310,14 @@ impl From for Bson { } } +#[cfg(feature = "time-0_3")] +#[cfg_attr(docsrs, doc(cfg(feature = "time-0_3")))] +impl From for Bson { + fn from(a: time::OffsetDateTime) -> Bson { + Bson::DateTime(crate::DateTime::from(a)) + } +} + #[cfg(feature = "chrono-0_4")] #[cfg_attr(docsrs, doc(cfg(feature = "chrono-0_4")))] impl From> for Bson { @@ -430,9 +437,10 @@ impl Bson { }) } Bson::ObjectId(v) => json!({"$oid": v.to_hex()}), - Bson::DateTime(v) if v.timestamp_millis() >= 0 && v.to_chrono().year() <= 99999 => { + Bson::DateTime(v) if v.timestamp_millis() >= 0 && v.to_time_0_3().year() <= 9999 => { json!({ - "$date": v.to_rfc3339_string(), + // Unwrap safety: timestamps in the guarded range can always be formatted. + "$date": v.try_to_rfc3339_string().unwrap(), }) } Bson::DateTime(v) => json!({ @@ -592,9 +600,10 @@ impl Bson { Bson::DateTime(v) if rawbson => doc! { "$date": v.timestamp_millis(), }, - Bson::DateTime(v) if v.timestamp_millis() >= 0 && v.to_chrono().year() <= 9999 => { + Bson::DateTime(v) if v.timestamp_millis() >= 0 && v.to_time_0_3().year() <= 9999 => { doc! { - "$date": v.to_rfc3339_string(), + // Unwrap safety: timestamps in the guarded range can always be formatted. + "$date": v.try_to_rfc3339_string().unwrap(), } } Bson::DateTime(v) => doc! { @@ -776,8 +785,8 @@ impl Bson { } if let Ok(date) = doc.get_str("$date") { - if let Ok(date) = chrono::DateTime::parse_from_rfc3339(date) { - return Bson::DateTime(crate::DateTime::from_chrono(date)); + if let Ok(dt) = crate::DateTime::parse_rfc3339_str(date) { + return Bson::DateTime(dt); } } } diff --git a/src/datetime.rs b/src/datetime.rs index 9cc85b58..33cb55db 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -1,16 +1,25 @@ use std::{ + convert::TryInto, error, fmt::{self, Display}, result, time::{Duration, SystemTime}, }; -#[cfg(all(feature = "serde_with", feature = "chrono-0_4"))] -use serde::{Deserialize, Deserializer, Serialize}; -#[cfg(all(feature = "serde_with", feature = "chrono-0_4"))] -use serde_with::{DeserializeAs, SerializeAs}; +use time::format_description::well_known::Rfc3339; +#[cfg(feature = "chrono-0_4")] use chrono::{LocalResult, TimeZone, Utc}; +#[cfg(all( + feature = "serde_with", + any(feature = "chrono-0_4", feature = "time-0_3") +))] +use serde::{Deserialize, Deserializer, Serialize}; +#[cfg(all( + feature = "serde_with", + any(feature = "chrono-0_4", feature = "time-0_3") +))] +use serde_with::{DeserializeAs, SerializeAs}; /// Struct representing a BSON datetime. /// Note: BSON datetimes have millisecond precision. @@ -114,11 +123,6 @@ impl crate::DateTime { Self::from_system_time(SystemTime::now()) } - #[cfg(not(feature = "chrono-0_4"))] - pub(crate) fn from_chrono(dt: chrono::DateTime) -> Self { - Self::from_millis(dt.timestamp_millis()) - } - /// Convert the given `chrono::DateTime` into a `bson::DateTime`, truncating it to millisecond /// precision. #[cfg(feature = "chrono-0_4")] @@ -127,7 +131,23 @@ impl crate::DateTime { Self::from_millis(dt.timestamp_millis()) } - fn to_chrono_private(self) -> chrono::DateTime { + /// Convert this [`DateTime`] to a [`chrono::DateTime`]. + /// + /// Note: Not every BSON datetime can be represented as a [`chrono::DateTime`]. For such dates, + /// [`chrono::MIN_DATETIME`] or [`chrono::MAX_DATETIME`] will be returned, whichever is closer. + /// + /// ``` + /// let bson_dt = bson::DateTime::now(); + /// let chrono_dt = bson_dt.to_chrono(); + /// assert_eq!(bson_dt.timestamp_millis(), chrono_dt.timestamp_millis()); + /// + /// let big = bson::DateTime::from_millis(i64::MAX); + /// let chrono_big = big.to_chrono(); + /// assert_eq!(chrono_big, chrono::MAX_DATETIME) + /// ``` + #[cfg(feature = "chrono-0_4")] + #[cfg_attr(docsrs, doc(cfg(feature = "chrono-0_4")))] + pub fn to_chrono(self) -> chrono::DateTime { match Utc.timestamp_millis_opt(self.0) { LocalResult::Single(dt) => dt, _ => { @@ -140,30 +160,78 @@ impl crate::DateTime { } } - #[cfg(not(feature = "chrono-0_4"))] + fn from_time_private(dt: time::OffsetDateTime) -> Self { + let millis = dt.unix_timestamp_nanos() / 1_000_000; + match millis.try_into() { + Ok(ts) => Self::from_millis(ts), + _ => { + if millis > 0 { + Self::MAX + } else { + Self::MIN + } + } + } + } + + #[cfg(not(feature = "time-0_3"))] #[allow(unused)] - pub(crate) fn to_chrono(self) -> chrono::DateTime { - self.to_chrono_private() + pub(crate) fn from_time_0_3(dt: time::OffsetDateTime) -> Self { + Self::from_time_private(dt) } - /// Convert this [`DateTime`] to a [`chrono::DateTime`]. + /// Convert the given `time::OffsetDateTime` into a `bson::DateTime`, truncating it to + /// millisecond precision. /// - /// Note: Not every BSON datetime can be represented as a [`chrono::DateTime`]. For such dates, - /// [`chrono::MIN_DATETIME`] or [`chrono::MAX_DATETIME`] will be returned, whichever is closer. + /// If the provided time is too far in the future or too far in the past to be represented + /// by a BSON datetime, either [`DateTime::MAX`] or [`DateTime::MIN`] will be + /// returned, whichever is closer. + #[cfg(feature = "time-0_3")] + pub fn from_time_0_3(dt: time::OffsetDateTime) -> Self { + Self::from_time_private(dt) + } + + fn to_time_private(self) -> time::OffsetDateTime { + match self.to_time_opt() { + Some(dt) => dt, + None => if self.0 < 0 { + time::PrimitiveDateTime::MIN + } else { + time::PrimitiveDateTime::MAX + } + .assume_utc(), + } + } + + pub(crate) fn to_time_opt(self) -> Option { + time::OffsetDateTime::UNIX_EPOCH.checked_add(time::Duration::milliseconds(self.0)) + } + + #[cfg(not(feature = "time-0_3"))] + #[allow(unused)] + pub(crate) fn to_time_0_3(self) -> time::OffsetDateTime { + self.to_time_private() + } + + /// Convert this [`DateTime`] to a [`time::OffsetDateTime`]. + /// + /// Note: Not every BSON datetime can be represented as a [`time::OffsetDateTime`]. For such + /// dates, [`time::PrimitiveDateTime::MIN`] or [`time::PrimitiveDateTime::MAX`] will be + /// returned, whichever is closer. /// /// ``` /// let bson_dt = bson::DateTime::now(); - /// let chrono_dt = bson_dt.to_chrono(); - /// assert_eq!(bson_dt.timestamp_millis(), chrono_dt.timestamp_millis()); + /// let time_dt = bson_dt.to_time_0_3(); + /// assert_eq!(bson_dt.timestamp_millis() / 1000, time_dt.unix_timestamp()); /// - /// let big = bson::DateTime::from_millis(i64::MAX); - /// let chrono_big = big.to_chrono(); - /// assert_eq!(chrono_big, chrono::MAX_DATETIME) + /// let big = bson::DateTime::from_millis(i64::MIN); + /// let time_big = big.to_time_0_3(); + /// assert_eq!(time_big, time::PrimitiveDateTime::MIN.assume_utc()) /// ``` - #[cfg(feature = "chrono-0_4")] - #[cfg_attr(docsrs, doc(cfg(feature = "chrono-0_4")))] - pub fn to_chrono(self) -> chrono::DateTime { - self.to_chrono_private() + #[cfg(feature = "time-0_3")] + #[cfg_attr(docsrs, doc(cfg(feature = "time-0_3")))] + pub fn to_time_0_3(self) -> time::OffsetDateTime { + self.to_time_private() } /// Convert the given [`std::time::SystemTime`] to a [`DateTime`]. @@ -208,28 +276,39 @@ impl crate::DateTime { self.0 } - /// Convert this [`DateTime`] to an RFC 3339 formatted string. + #[deprecated(since = "2.3.0", note = "Use try_to_rfc3339_string instead.")] + /// Convert this [`DateTime`] to an RFC 3339 formatted string. Panics if it could not be + /// represented in that format. pub fn to_rfc3339_string(self) -> String { - self.to_chrono() - .to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true) + self.try_to_rfc3339_string().unwrap() + } + + /// Convert this [`DateTime`] to an RFC 3339 formatted string. + pub fn try_to_rfc3339_string(self) -> Result { + self.to_time_0_3() + .format(&Rfc3339) + .map_err(|e| Error::CannotFormat { + message: e.to_string(), + }) } /// Convert the given RFC 3339 formatted string to a [`DateTime`], truncating it to millisecond /// precision. pub fn parse_rfc3339_str(s: impl AsRef) -> Result { - let date = chrono::DateTime::::parse_from_rfc3339(s.as_ref()) - .map_err(|e| Error::InvalidTimestamp { + let odt = time::OffsetDateTime::parse(s.as_ref(), &Rfc3339).map_err(|e| { + Error::InvalidTimestamp { message: e.to_string(), - })?; - Ok(Self::from_chrono(date)) + } + })?; + Ok(Self::from_time_0_3(odt)) } } impl fmt::Debug for crate::DateTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut tup = f.debug_tuple("DateTime"); - match Utc.timestamp_millis_opt(self.0) { - LocalResult::Single(ref dt) => tup.field(dt), + match self.to_time_opt() { + Some(dt) => tup.field(&dt), _ => tup.field(&self.0), }; tup.finish() @@ -238,8 +317,8 @@ impl fmt::Debug for crate::DateTime { impl Display for crate::DateTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match Utc.timestamp_millis_opt(self.0) { - LocalResult::Single(ref dt) => Display::fmt(dt, f), + match self.to_time_opt() { + Some(dt) => Display::fmt(&dt, f), _ => Display::fmt(&self.0, f), } } @@ -300,6 +379,49 @@ impl SerializeAs> for crate::DateTime { } } +#[cfg(feature = "time-0_3")] +#[cfg_attr(docsrs, doc(cfg(feature = "time-0_3")))] +impl From for time::OffsetDateTime { + fn from(bson_dt: DateTime) -> Self { + bson_dt.to_time_0_3() + } +} + +#[cfg(feature = "time-0_3")] +#[cfg_attr(docsrs, doc(cfg(feature = "time-0_3")))] +impl From for crate::DateTime { + fn from(x: time::OffsetDateTime) -> Self { + Self::from_time_0_3(x) + } +} + +#[cfg(all(feature = "time-0_3", feature = "serde_with"))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "time-0_3", feature = "serde_with"))))] +impl<'de> DeserializeAs<'de, time::OffsetDateTime> for crate::DateTime { + fn deserialize_as(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let dt = DateTime::deserialize(deserializer)?; + Ok(dt.to_time_0_3()) + } +} + +#[cfg(all(feature = "time-0_3", feature = "serde_with"))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "time-0_3", feature = "chrono-0_4"))))] +impl SerializeAs for crate::DateTime { + fn serialize_as( + source: &time::OffsetDateTime, + serializer: S, + ) -> std::result::Result + where + S: serde::Serializer, + { + let dt = DateTime::from_time_0_3(*source); + dt.serialize(serializer) + } +} + /// Errors that can occur during [`DateTime`] construction and generation. #[derive(Clone, Debug)] #[non_exhaustive] @@ -307,6 +429,9 @@ pub enum Error { /// Error returned when an invalid datetime format is provided to a conversion method. #[non_exhaustive] InvalidTimestamp { message: String }, + /// Error returned when a `DateTime` cannot be represented in a particular format. + #[non_exhaustive] + CannotFormat { message: String }, } /// Alias for `Result` @@ -315,7 +440,7 @@ pub type Result = result::Result; impl fmt::Display for Error { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { match self { - Error::InvalidTimestamp { message } => { + Error::InvalidTimestamp { message } | Error::CannotFormat { message } => { write!(fmt, "{}", message) } } diff --git a/src/extjson/models.rs b/src/extjson/models.rs index 0b98f0f3..667123c3 100644 --- a/src/extjson/models.rs +++ b/src/extjson/models.rs @@ -1,6 +1,5 @@ //! A module defining serde models for the extended JSON representations of the various BSON types. -use chrono::Utc; use serde::{ de::{Error, Unexpected}, Deserialize, @@ -256,16 +255,13 @@ impl DateTime { Ok(crate::DateTime::from_millis(date)) } DateTimeBody::Relaxed(date) => { - let datetime: chrono::DateTime = - chrono::DateTime::parse_from_rfc3339(date.as_str()) - .map_err(|_| { - extjson::de::Error::invalid_value( - Unexpected::Str(date.as_str()), - &"rfc3339 formatted utc datetime", - ) - })? - .into(); - Ok(crate::DateTime::from_chrono(datetime)) + let datetime = crate::DateTime::parse_rfc3339_str(date.as_str()).map_err(|_| { + extjson::de::Error::invalid_value( + Unexpected::Str(date.as_str()), + &"rfc3339 formatted utc datetime", + ) + })?; + Ok(datetime) } } } diff --git a/src/lib.rs b/src/lib.rs index 21a298cf..425c5422 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,7 @@ //! //! ## Installation //! ### Requirements -//! - Rust 1.48+ +//! - Rust 1.53+ //! //! ### Importing //! This crate is available on [crates.io](https://crates.io/crates/bson). To use it in your application, @@ -260,7 +260,7 @@ //! //! ## Minimum supported Rust version (MSRV) //! -//! The MSRV for this crate is currently 1.48.0. This will be rarely be increased, and if it ever is, +//! The MSRV for this crate is currently 1.53.0. This will be rarely be increased, and if it ever is, //! it will only happen in a minor or major version release. #![allow(clippy::cognitive_complexity)] diff --git a/src/oid.rs b/src/oid.rs index 4a13d2b8..8a884792 100644 --- a/src/oid.rs +++ b/src/oid.rs @@ -283,7 +283,7 @@ fn test_counter_overflow_usize_max() { #[cfg(test)] mod test { - use chrono::{offset::TimeZone, Utc}; + use time::macros::datetime; #[test] fn test_display() { @@ -310,30 +310,27 @@ mod test { fn test_timestamp() { let id = super::ObjectId::parse_str("000000000000000000000000").unwrap(); // "Jan 1st, 1970 00:00:00 UTC" - assert_eq!( - Utc.ymd(1970, 1, 1).and_hms(0, 0, 0), - id.timestamp().to_chrono() - ); + assert_eq!(datetime!(1970-01-01 0:00 UTC), id.timestamp().to_time_0_3()); let id = super::ObjectId::parse_str("7FFFFFFF0000000000000000").unwrap(); // "Jan 19th, 2038 03:14:07 UTC" assert_eq!( - Utc.ymd(2038, 1, 19).and_hms(3, 14, 7), - id.timestamp().to_chrono() + datetime!(2038-01-19 3:14:07 UTC), + id.timestamp().to_time_0_3() ); let id = super::ObjectId::parse_str("800000000000000000000000").unwrap(); // "Jan 19th, 2038 03:14:08 UTC" assert_eq!( - Utc.ymd(2038, 1, 19).and_hms(3, 14, 8), - id.timestamp().to_chrono() + datetime!(2038-01-19 3:14:08 UTC), + id.timestamp().to_time_0_3() ); let id = super::ObjectId::parse_str("FFFFFFFF0000000000000000").unwrap(); // "Feb 7th, 2106 06:28:15 UTC" assert_eq!( - Utc.ymd(2106, 2, 7).and_hms(6, 28, 15), - id.timestamp().to_chrono() + datetime!(2106-02-07 6:28:15 UTC), + id.timestamp().to_time_0_3() ); } } diff --git a/src/raw/test/mod.rs b/src/raw/test/mod.rs index 22a065c4..5bcec28e 100644 --- a/src/raw/test/mod.rs +++ b/src/raw/test/mod.rs @@ -13,7 +13,6 @@ use crate::{ Regex, Timestamp, }; -use chrono::{TimeZone, Utc}; #[test] fn string_from_document() { @@ -227,9 +226,11 @@ fn boolean() { #[test] fn datetime() { + use time::macros::datetime; + let rawdoc = rawdoc! { "boolean": true, - "datetime": DateTime::from_chrono(Utc.ymd(2000,10,31).and_hms(12, 30, 45)), + "datetime": DateTime::from_time_0_3(datetime!(2000-10-31 12:30:45 UTC)), }; let datetime = rawdoc .get("datetime") @@ -237,7 +238,10 @@ fn datetime() { .expect("no key datetime") .as_datetime() .expect("result was not datetime"); - assert_eq!(datetime.to_rfc3339_string(), "2000-10-31T12:30:45Z"); + assert_eq!( + datetime.try_to_rfc3339_string().unwrap(), + "2000-10-31T12:30:45Z" + ); } #[test] diff --git a/src/serde_helpers.rs b/src/serde_helpers.rs index 7c48c920..29ce15d8 100644 --- a/src/serde_helpers.rs +++ b/src/serde_helpers.rs @@ -27,6 +27,12 @@ pub use rfc3339_string_as_bson_datetime::{ deserialize as deserialize_rfc3339_string_from_bson_datetime, serialize as serialize_rfc3339_string_as_bson_datetime, }; +#[cfg(feature = "time-0_3")] +#[doc(inline)] +pub use time_0_3_offsetdatetime_as_bson_datetime::{ + deserialize as deserialize_time_0_3_offsetdatetime_from_bson_datetime, + serialize as serialize_time_0_3_offsetdatetime_as_bson_datetime, +}; #[doc(inline)] pub use timestamp_as_u32::{ deserialize as deserialize_timestamp_from_u32, @@ -185,6 +191,49 @@ pub mod u64_as_f64 { } } +/// Contains functions to serialize a [`time::OffsetDateTime`] as a [`crate::DateTime`] and +/// deserialize a [`time::OffsetDateTime`] from a [`crate::DateTime`]. +/// +/// ```rust +/// # #[cfg(feature = "time-0_3")] +/// # { +/// # use serde::{Serialize, Deserialize}; +/// # use bson::serde_helpers::time_0_3_offsetdatetime_as_bson_datetime; +/// #[derive(Serialize, Deserialize)] +/// struct Event { +/// #[serde(with = "time_0_3_offsetdatetime_as_bson_datetime")] +/// pub date: time::OffsetDateTime, +/// } +/// # } +/// ``` +#[cfg(feature = "time-0_3")] +#[cfg_attr(docsrs, doc(cfg(feature = "time-0_3")))] +pub mod time_0_3_offsetdatetime_as_bson_datetime { + use crate::DateTime; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::result::Result; + + /// Deserializes a [`time::OffsetDateTime`] from a [`crate::DateTime`]. + #[cfg_attr(docsrs, doc(cfg(feature = "time-0_3")))] + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let datetime = DateTime::deserialize(deserializer)?; + Ok(datetime.to_time_0_3()) + } + + /// Serializes a [`time::OffsetDateTime`] as a [`crate::DateTime`]. + #[cfg_attr(docsrs, doc(cfg(feature = "time-0_3")))] + pub fn serialize( + val: &time::OffsetDateTime, + serializer: S, + ) -> Result { + let datetime = DateTime::from_time_0_3(val.to_owned()); + datetime.serialize(serializer) + } +} + /// Contains functions to serialize a [`chrono::DateTime`] as a [`crate::DateTime`] and deserialize /// a [`chrono::DateTime`] from a [`crate::DateTime`]. /// @@ -243,7 +292,7 @@ pub mod chrono_datetime_as_bson_datetime { /// ``` pub mod rfc3339_string_as_bson_datetime { use crate::{Bson, DateTime}; - use serde::{ser, Deserialize, Deserializer, Serialize, Serializer}; + use serde::{de, ser, Deserialize, Deserializer, Serialize, Serializer}; use std::result::Result; /// Deserializes an ISO string from a DateTime. @@ -252,16 +301,15 @@ pub mod rfc3339_string_as_bson_datetime { D: Deserializer<'de>, { let date = DateTime::deserialize(deserializer)?; - Ok(date.to_rfc3339_string()) + date.try_to_rfc3339_string() + .map_err(|e| de::Error::custom(format!("cannot format {} as RFC 3339: {}", date, e))) } /// Serializes an ISO string as a DateTime. pub fn serialize(val: &str, serializer: S) -> Result { - let date = - chrono::DateTime::::parse_from_rfc3339(val).map_err(|_| { - ser::Error::custom(format!("cannot convert {} to chrono::DateTime", val)) - })?; - Bson::DateTime(crate::DateTime::from_chrono(date)).serialize(serializer) + let date = crate::DateTime::parse_rfc3339_str(val) + .map_err(|_| ser::Error::custom(format!("cannot convert {} to DateTime", val)))?; + Bson::DateTime(date).serialize(serializer) } } @@ -279,7 +327,7 @@ pub mod rfc3339_string_as_bson_datetime { /// ``` pub mod bson_datetime_as_rfc3339_string { use crate::DateTime; - use serde::{de, Deserialize, Deserializer, Serializer}; + use serde::{de, ser, Deserialize, Deserializer, Serializer}; use std::result::Result; /// Deserializes a [`crate::DateTime`] from an RFC 3339 formatted string. @@ -288,16 +336,18 @@ pub mod bson_datetime_as_rfc3339_string { D: Deserializer<'de>, { let iso = String::deserialize(deserializer)?; - let date = - chrono::DateTime::::parse_from_rfc3339(&iso).map_err(|_| { - de::Error::custom(format!("cannot parse RFC 3339 datetime from \"{}\"", iso)) - })?; - Ok(DateTime::from_chrono(date)) + let date = crate::DateTime::parse_rfc3339_str(&iso).map_err(|_| { + de::Error::custom(format!("cannot parse RFC 3339 datetime from \"{}\"", iso)) + })?; + Ok(date) } /// Serializes a [`crate::DateTime`] as an RFC 3339 (ISO 8601) formatted string. pub fn serialize(val: &DateTime, serializer: S) -> Result { - serializer.serialize_str(&val.to_rfc3339_string()) + let formatted = val + .try_to_rfc3339_string() + .map_err(|e| ser::Error::custom(format!("cannot format {} as RFC 3339: {}", val, e)))?; + serializer.serialize_str(&formatted) } } diff --git a/src/tests/datetime.rs b/src/tests/datetime.rs index f0a38289..8f4719c3 100644 --- a/src/tests/datetime.rs +++ b/src/tests/datetime.rs @@ -1,15 +1,15 @@ use crate::tests::LOCK; -use std::str::FromStr; #[test] fn rfc3339_to_datetime() { let _guard = LOCK.run_concurrently(); let rfc = "2020-06-09T10:58:07.095Z"; - let date = chrono::DateTime::::from_str(rfc).unwrap(); + let date = + time::OffsetDateTime::parse(rfc, &time::format_description::well_known::Rfc3339).unwrap(); let parsed = crate::DateTime::parse_rfc3339_str(rfc).unwrap(); - assert_eq!(parsed, crate::DateTime::from_chrono(date)); - assert_eq!(crate::DateTime::to_rfc3339_string(parsed), rfc); + assert_eq!(parsed, crate::DateTime::from_time_0_3(date)); + assert_eq!(crate::DateTime::try_to_rfc3339_string(parsed).unwrap(), rfc); } #[test] @@ -23,3 +23,18 @@ fn invalid_rfc3339_to_datetime() { assert!(crate::DateTime::parse_rfc3339_str(b).is_err()); assert!(crate::DateTime::parse_rfc3339_str(c).is_err()); } + +#[test] +fn datetime_to_rfc3339() { + assert_eq!( + crate::DateTime::from_millis(0) + .try_to_rfc3339_string() + .unwrap(), + "1970-01-01T00:00:00Z" + ); +} + +#[test] +fn invalid_datetime_to_rfc3339() { + assert!(crate::DateTime::MAX.try_to_rfc3339_string().is_err()); +} diff --git a/src/tests/modules/bson.rs b/src/tests/modules/bson.rs index 2f48863c..8f6de566 100644 --- a/src/tests/modules/bson.rs +++ b/src/tests/modules/bson.rs @@ -231,22 +231,33 @@ fn timestamp_ordering() { } #[test] -fn from_chrono_datetime() { +fn from_external_datetime() { + use time::macros::datetime; + let _guard = LOCK.run_concurrently(); fn assert_millisecond_precision(dt: DateTime) { - assert!(dt.to_chrono().timestamp_subsec_micros() % 1000 == 0); + assert!(dt.to_time_0_3().microsecond() % 1000 == 0); } fn assert_subsec_millis(dt: DateTime, millis: u32) { - assert_eq!(dt.to_chrono().timestamp_subsec_millis(), millis) + assert_eq!(dt.to_time_0_3().millisecond() as u32, millis) } - let now = chrono::Utc::now(); - let dt = DateTime::from_chrono(now); + let now = time::OffsetDateTime::now_utc(); + let dt = DateTime::from_time_0_3(now); assert_millisecond_precision(dt); + #[cfg(feature = "time-0_3")] + { + let bson = Bson::from(now); + assert_millisecond_precision(bson.as_datetime().unwrap().to_owned()); + + let from_time = DateTime::from(now); + assert_millisecond_precision(from_time); + } #[cfg(feature = "chrono-0_4")] { + let now = chrono::Utc::now(); let bson = Bson::from(now); assert_millisecond_precision(bson.as_datetime().unwrap().to_owned()); @@ -254,13 +265,21 @@ fn from_chrono_datetime() { assert_millisecond_precision(from_chrono); } - let no_subsec_millis: chrono::DateTime = "2014-11-28T12:00:09Z".parse().unwrap(); - let dt = DateTime::from_chrono(no_subsec_millis); + let no_subsec_millis = datetime!(2014-11-28 12:00:09 UTC); + let dt = DateTime::from_time_0_3(no_subsec_millis); assert_millisecond_precision(dt); assert_subsec_millis(dt, 0); + #[cfg(feature = "time-0_3")] + { + let bson = Bson::from(dt); + assert_millisecond_precision(bson.as_datetime().unwrap().to_owned()); + assert_subsec_millis(bson.as_datetime().unwrap().to_owned(), 0); + } #[cfg(feature = "chrono-0_4")] { + let no_subsec_millis: chrono::DateTime = + "2014-11-28T12:00:09Z".parse().unwrap(); let dt = DateTime::from(no_subsec_millis); assert_millisecond_precision(dt); assert_subsec_millis(dt, 0); @@ -275,13 +294,21 @@ fn from_chrono_datetime() { "2014-11-28T12:00:09.123456Z", "2014-11-28T12:00:09.123456789Z", ] { - let chrono_dt: chrono::DateTime = s.parse().unwrap(); - let dt = DateTime::from_chrono(chrono_dt); + let time_dt = + time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339).unwrap(); + let dt = DateTime::from_time_0_3(time_dt); assert_millisecond_precision(dt); assert_subsec_millis(dt, 123); + #[cfg(feature = "time-0_3")] + { + let bson = Bson::from(time_dt); + assert_millisecond_precision(bson.as_datetime().unwrap().to_owned()); + assert_subsec_millis(bson.as_datetime().unwrap().to_owned(), 123); + } #[cfg(feature = "chrono-0_4")] { + let chrono_dt: chrono::DateTime = s.parse().unwrap(); let dt = DateTime::from(chrono_dt); assert_millisecond_precision(dt); assert_subsec_millis(dt, 123); @@ -292,6 +319,28 @@ fn from_chrono_datetime() { } } + #[cfg(feature = "time-0_3")] + { + let max = time::PrimitiveDateTime::MAX.assume_utc(); + let bdt = DateTime::from(max); + assert_eq!( + bdt.to_time_0_3().unix_timestamp_nanos() / 1_000_000, // truncate to millis + max.unix_timestamp_nanos() / 1_000_000 + ); + + let min = time::PrimitiveDateTime::MIN.assume_utc(); + let bdt = DateTime::from(min); + assert_eq!( + bdt.to_time_0_3().unix_timestamp_nanos() / 1_000_000, + min.unix_timestamp_nanos() / 1_000_000 + ); + + let bdt = DateTime::MAX; + assert_eq!(bdt.to_time_0_3(), max); + + let bdt = DateTime::MIN; + assert_eq!(bdt.to_time_0_3(), min); + } #[cfg(feature = "chrono-0_4")] { let bdt = DateTime::from(chrono::MAX_DATETIME); diff --git a/src/tests/modules/document.rs b/src/tests/modules/document.rs index 6c7669a1..85fc62ff 100644 --- a/src/tests/modules/document.rs +++ b/src/tests/modules/document.rs @@ -9,7 +9,7 @@ use crate::{ Document, Timestamp, }; -use chrono::Utc; +use time::OffsetDateTime; #[test] fn ordered_insert() { @@ -50,8 +50,8 @@ fn ordered_insert_shorthand() { #[test] fn test_getters() { let _guard = LOCK.run_concurrently(); - let datetime = Utc::now(); - let cloned_dt = crate::DateTime::from_chrono(datetime); + let datetime = OffsetDateTime::now_utc(); + let cloned_dt = crate::DateTime::from_time_0_3(datetime); let binary = vec![0, 1, 2, 3, 4]; let mut doc = doc! { "floating_point": 10.0, @@ -125,7 +125,7 @@ fn test_getters() { doc.get_timestamp("timestamp") ); - let dt = crate::DateTime::from_chrono(datetime); + let dt = crate::DateTime::from_time_0_3(datetime); assert_eq!(Some(&Bson::DateTime(dt)), doc.get("datetime")); assert_eq!(Ok(&dt), doc.get_datetime("datetime")); diff --git a/src/tests/modules/macros.rs b/src/tests/modules/macros.rs index 9206dd5c..ada1ccc5 100644 --- a/src/tests/modules/macros.rs +++ b/src/tests/modules/macros.rs @@ -9,7 +9,6 @@ use crate::{ Regex, Timestamp, }; -use chrono::offset::Utc; use pretty_assertions::assert_eq; #[test] @@ -21,7 +20,7 @@ fn standard_format() { bytes[..12].clone_from_slice(&string_bytes[..12]); let id = ObjectId::from_bytes(bytes); - let date = Utc::now(); + let date = time::OffsetDateTime::now_utc(); let doc = doc! { "float": 2.4, @@ -43,7 +42,7 @@ fn standard_format() { "binary": Binary { subtype: BinarySubtype::Md5, bytes: "thingies".to_owned().into_bytes() }, "encrypted": Binary { subtype: BinarySubtype::Encrypted, bytes: "secret".to_owned().into_bytes() }, "_id": id, - "date": Bson::DateTime(crate::DateTime::from_chrono(date)), + "date": Bson::DateTime(crate::DateTime::from_time_0_3(date)), }; let rawdoc = rawdoc! { @@ -66,9 +65,12 @@ fn standard_format() { "binary": Binary { subtype: BinarySubtype::Md5, bytes: "thingies".to_owned().into_bytes() }, "encrypted": Binary { subtype: BinarySubtype::Encrypted, bytes: "secret".to_owned().into_bytes() }, "_id": id, - "date": crate::DateTime::from_chrono(date), + "date": crate::DateTime::from_time_0_3(date), }; + let ts_nanos = date.unix_timestamp_nanos(); + let ts_millis = ts_nanos - (ts_nanos % 1_000_000); + let date_trunc = time::OffsetDateTime::from_unix_timestamp_nanos(ts_millis).unwrap(); let expected = format!( "{{ \"float\": 2.4, \"string\": \"hello\", \"array\": [\"testing\", 1, true, [1, 2]], \ \"doc\": {{ \"fish\": \"in\", \"a\": \"barrel\", \"!\": 1 }}, \"bool\": true, \"null\": \ @@ -79,7 +81,7 @@ fn standard_format() { base64::encode("thingies"), base64::encode("secret"), hex::encode(id_string), - date.format("%Y-%m-%d %H:%M:%S%.3f %Z") + date_trunc, ); assert_eq!(expected, format!("{}", doc)); diff --git a/src/tests/modules/serializer_deserializer.rs b/src/tests/modules/serializer_deserializer.rs index cc5f222c..b5f5ba7f 100644 --- a/src/tests/modules/serializer_deserializer.rs +++ b/src/tests/modules/serializer_deserializer.rs @@ -21,7 +21,6 @@ use crate::{ Regex, Timestamp, }; -use chrono::{offset::TimeZone, Utc}; use serde_json::json; #[test] @@ -312,11 +311,18 @@ fn test_serialize_deserialize_object_id() { #[test] fn test_serialize_utc_date_time() { + #[cfg(feature = "chrono-0_4")] + use chrono::offset::TimeZone; let _guard = LOCK.run_concurrently(); - #[cfg(not(feature = "chrono-0_4"))] - let src = crate::DateTime::from_chrono(Utc.timestamp(1_286_705_410, 0)); + #[cfg(not(any(feature = "chrono-0_4", feature = "time-0_3")))] + let src = crate::DateTime::from_time_0_3( + time::OffsetDateTime::from_unix_timestamp(1_286_705_410).unwrap(), + ); + #[cfg(feature = "time-0_3")] + #[allow(unused)] + let src = time::OffsetDateTime::from_unix_timestamp(1_286_705_410).unwrap(); #[cfg(feature = "chrono-0_4")] - let src = Utc.timestamp(1_286_705_410, 0); + let src = chrono::Utc.timestamp(1_286_705_410, 0); let dst = vec![ 18, 0, 0, 0, 9, 107, 101, 121, 0, 208, 111, 158, 149, 43, 1, 0, 0, 0, ]; @@ -367,8 +373,7 @@ fn test_deserialize_utc_date_time_overflows() { let deserialized = Document::from_reader(&mut Cursor::new(raw)).unwrap(); - let expected = - doc! { "A": crate::DateTime::from_chrono(Utc.timestamp(1_530_492_218, 999 * 1_000_000))}; + let expected = doc! { "A": crate::DateTime::from_time_0_3(time::OffsetDateTime::from_unix_timestamp(1_530_492_218).unwrap() + time::Duration::nanoseconds(999 * 1_000_000))}; assert_eq!(deserialized, expected); } diff --git a/src/tests/serde.rs b/src/tests/serde.rs index e31428e1..13a716d0 100644 --- a/src/tests/serde.rs +++ b/src/tests/serde.rs @@ -34,7 +34,6 @@ use serde_json::json; use std::{ collections::BTreeMap, convert::{TryFrom, TryInto}, - str::FromStr, }; #[test] @@ -727,6 +726,8 @@ fn test_unsigned_helpers() { #[test] fn test_datetime_helpers() { + use time::{format_description::well_known::Rfc3339, OffsetDateTime}; + let _guard = LOCK.run_concurrently(); #[derive(Deserialize, Serialize)] @@ -736,17 +737,46 @@ fn test_datetime_helpers() { } let iso = "1996-12-20T00:39:57Z"; - let date = chrono::DateTime::::from_str(iso).unwrap(); + let date = OffsetDateTime::parse(iso, &Rfc3339).unwrap(); let a = A { - date: crate::DateTime::from_chrono(date), + date: crate::DateTime::from_time_0_3(date), }; let doc = to_document(&a).unwrap(); assert_eq!(doc.get_str("date").unwrap(), iso); let a: A = from_document(doc).unwrap(); - assert_eq!(a.date.to_chrono(), date); + assert_eq!(a.date.to_time_0_3(), date); + + #[cfg(feature = "time-0_3")] + { + use time::macros::datetime; + + #[derive(Deserialize, Serialize)] + struct B { + #[serde(with = "serde_helpers::time_0_3_offsetdatetime_as_bson_datetime")] + pub date: time::OffsetDateTime, + } + + let date = r#" + { + "date": { + "$date": { + "$numberLong": "1591700287095" + } + } + }"#; + let json: serde_json::Value = serde_json::from_str(date).unwrap(); + let b: B = serde_json::from_value(json).unwrap(); + let expected = datetime!(2020-06-09 10:58:07.095 UTC); + assert_eq!(b.date, expected); + let doc = to_document(&b).unwrap(); + assert_eq!(doc.get_datetime("date").unwrap().to_time_0_3(), expected); + let b: B = from_document(doc).unwrap(); + assert_eq!(b.date, expected); + } #[cfg(feature = "chrono-0_4")] { + use std::str::FromStr; #[derive(Deserialize, Serialize)] struct B { #[serde(with = "serde_helpers::chrono_datetime_as_bson_datetime")]