Skip to content

RUST-1748 Convert unsigned serde helpers to use serde_conv #573

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
a54433e
Refactor BSON DateTime serde converters using serde_conv_doc macro
JamieTsai1024 Jul 8, 2025
a51f29e
Refactor OffsetDateTime and i64 to DateTime helpers to use serde_conv
JamieTsai1024 Jul 8, 2025
03e9a51
Refactor type converters betweem timestamp and u32 with serde_conv
JamieTsai1024 Jul 9, 2025
f1577fd
Refactor u32 and u64 to f64 converters to use serde_conv
JamieTsai1024 Jul 9, 2025
b8ad904
Rename u32 module to timestamp
JamieTsai1024 Jul 9, 2025
a1ad2de
Clean up remaining u32_as_timestamp converter
JamieTsai1024 Jul 9, 2025
5529ec8
Fix struct capitalization
JamieTsai1024 Jul 10, 2025
a7c182a
Refactor unsigned (u32, u64) to signed (i32, i64) converters using se…
JamieTsai1024 Jul 10, 2025
693faa8
Consolidate module names under u32 and u64
JamieTsai1024 Jul 10, 2025
a9873d3
Consolidate module names under u32 and u64
JamieTsai1024 Jul 10, 2025
b9b08cf
Refactor uuid to binary converter to use serde_conv
JamieTsai1024 Jul 11, 2025
1ccea32
Reorder tests based on serde_helpers ordering
JamieTsai1024 Jul 11, 2025
1db7343
Refactor uuid to legacy binary converters using serde_conv
JamieTsai1024 Jul 11, 2025
2298c43
Remove bson from datetime helpers module name
JamieTsai1024 Jul 14, 2025
7cdc3d9
Add RFC 3339 format to error messages
JamieTsai1024 Jul 14, 2025
76349a0
Fix typo
JamieTsai1024 Jul 14, 2025
e9b14b8
Refactor DateTime FromI64 and FromTime03OffsetDateTime using serde_co…
JamieTsai1024 Jul 14, 2025
212ea0b
Add rustdoc comments to module and structs
JamieTsai1024 Jul 14, 2025
03027d1
Switch from clone through to_owned() to dereference
JamieTsai1024 Jul 14, 2025
7077c0b
Rename bson_datetime module to datetime
JamieTsai1024 Jul 14, 2025
ebe3a99
Remove extra rustdocs ///
JamieTsai1024 Jul 14, 2025
9f41ee6
Add DateTime import to fix lint on rustdocs
JamieTsai1024 Jul 14, 2025
e6b69c5
Merge branch 'RUST-1748-serde_conv-2' into RUST-1748-serde_conv-3
JamieTsai1024 Jul 14, 2025
2d26b59
Remove DateTime import for rustdocs to avoid clippy warning
JamieTsai1024 Jul 14, 2025
d3d9a95
Add missing conditional compilation to OffsetDateTime struct
JamieTsai1024 Jul 14, 2025
e1e4dae
Merge branch 2 into branch 3
JamieTsai1024 Jul 14, 2025
ae2b0a4
Fix rustdoc imports
JamieTsai1024 Jul 14, 2025
3a0e345
Update rustdocs phrasing and improve struct naming for Chrono04DateTime
JamieTsai1024 Jul 14, 2025
f527aff
Split tests for DateTime
JamieTsai1024 Jul 14, 2025
c14f554
Merge branch 'RUST-1748-serde_conv-2' into RUST-1748-serde_conv-3
JamieTsai1024 Jul 14, 2025
f834214
Add back deleted u32 tests from merge
JamieTsai1024 Jul 14, 2025
ec82af4
Fix datetime rustdoc comments from merge
JamieTsai1024 Jul 14, 2025
47cbddd
Update rustdoc comments
JamieTsai1024 Jul 14, 2025
fe5b5e4
clippy test: revert FromTime03OffsetDateTime to FromTimeOffsetDateTime
JamieTsai1024 Jul 14, 2025
ca6f68f
Rename FromTime03OffsetDateTime
JamieTsai1024 Jul 14, 2025
b85d7d0
Merge branch 'RUST-1748-serde_conv-2' into RUST-1748-serde_conv-3
JamieTsai1024 Jul 14, 2025
53090e9
Revert all UUID changes (separate to another PR)
JamieTsai1024 Jul 14, 2025
f0c3833
Fix rustdoc import
JamieTsai1024 Jul 14, 2025
00a4f9c
Copy documentation and test changes for ObjectId
JamieTsai1024 Jul 14, 2025
3103c27
Try uncommenting conditional compilation on rustdoc comments
JamieTsai1024 Jul 15, 2025
6f74ead
Undo uncommenting rustdocs
JamieTsai1024 Jul 15, 2025
a3dd1b8
Fix clippy attempt - move conditional compilation attribute
JamieTsai1024 Jul 15, 2025
f81dfd2
Revert all ObjectId changes (separate to another PR)
JamieTsai1024 Jul 15, 2025
60b6c4f
Move #[cfg(feature = serde_with-3)] attribute to test function
JamieTsai1024 Jul 15, 2025
d91ba76
Break tests into smaller functions
JamieTsai1024 Jul 15, 2025
de0b400
Remove whitespace diffs
JamieTsai1024 Jul 15, 2025
844b50c
Merge branch 'RUST-1748-serde_conv-2' into RUST-1748-serde_conv-3
JamieTsai1024 Jul 15, 2025
1e9903a
Fix rustdocs whitespace
JamieTsai1024 Jul 15, 2025
9169665
Remove whitespace
JamieTsai1024 Jul 15, 2025
9938011
Remove Result imports
JamieTsai1024 Jul 15, 2025
11b0ddd
Update rustdocs
JamieTsai1024 Jul 15, 2025
71cab69
Update rustdocs imports
JamieTsai1024 Jul 15, 2025
48234dc
Move cfg_attr into macro
JamieTsai1024 Jul 15, 2025
31a23d6
Merge branch 'RUST-1748-serde_conv-2' into RUST-1748-serde_conv-3
JamieTsai1024 Jul 16, 2025
648bfaa
Merge branch 'main' into RUST-1748-serde_conv-3
JamieTsai1024 Jul 16, 2025
59bf3fa
Merge main
JamieTsai1024 Jul 16, 2025
65d935a
Merge branch 'RUST-1748-serde_conv-3' of https://github.com/JamieTsai…
JamieTsai1024 Jul 16, 2025
dd78cf6
Fix merge from main
JamieTsai1024 Jul 16, 2025
103011c
Reorder structs and tests
JamieTsai1024 Jul 16, 2025
19329dd
Revert last 6 commits
JamieTsai1024 Jul 17, 2025
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
4 changes: 1 addition & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ serde_path_to_error = ["dep:serde_path_to_error"]
# should be used in conjunction with chrono-0_4 or uuid-0_8.
serde_with-3 = ["dep:serde_with", "dep:serde"]
serde = ["dep:serde"]
serde_json-1 = ["dep:serde_json"]

[lib]
name = "bson"
Expand All @@ -58,7 +57,7 @@ ahash = "0.8.0"
chrono = { version = "0.4.15", features = ["std"], 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 }
serde_json = { version = "1.0", features = ["preserve_order"] }
indexmap = "2.1.0"
hex = "0.4.2"
base64 = "0.22.1"
Expand All @@ -85,7 +84,6 @@ pretty_assertions = "0.6.1"
proptest = "1.0.0"
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 }

[package.metadata.docs.rs]
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,10 @@ Note that if you are using `bson` through the `mongodb` crate, you do not need t
| `chrono-0_4` | Enable support for v0.4 of the [`chrono`](https://docs.rs/chrono/0.4) crate in the public API. | n/a | no |
| `uuid-1` | Enable support for v1.x of the [`uuid`](https://docs.rs/uuid/1.0) crate in the public API. | n/a | no |
| `time-0_3` | Enable support for v0.3 of the [`time`](https://docs.rs/time/0.3) crate in the public API. | n/a | no |
| `serde_with-3` | Enable [`serde_with`](https://docs.rs/serde_with/3.x) 3.x integrations for `bson::DateTime` and `bson::Uuid`.| `serde_with` | no |
| `serde_path_to_error` | Enable support for error paths via integration with [`serde_path_to_error`](https://docs.rs/serde_path_to_err/latest). This is an unstable feature and any breaking changes to `serde_path_to_error` may affect usage of it via this feature. | `serde_path_to_error` | no |
| `serde_with-3` | Enable [`serde_with`](https://docs.rs/serde_with/3.x) 3.x integrations for `bson::DateTime` and `bson::Uuid`.| serde_with | no |
| `serde_path_to_error` | Enable support for error paths via integration with [`serde_path_to_error`](https://docs.rs/serde_path_to_err/latest). This is an unstable feature and any breaking changes to `serde_path_to_error` may affect usage of it via this feature. | serde_path_to_error | no |
| `compat-3-0-0` | Required for future compatibility if default features are disabled. | n/a | no |
| `large_dates` | Increase the supported year range for some `bson::DateTime` utilities from +/-9,999 (inclusive) to +/-999,999 (inclusive). Note that enabling this feature can impact performance and introduce parsing ambiguities. | n/a | no |
| `serde_json-1` | Enable support for v1.x of the [`serde_json`](https://docs.rs/serde_json/1.x) crate in the public API. | `serde_json` | no |

## Overview of the BSON Format

Expand Down
2 changes: 1 addition & 1 deletion serde-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ edition = "2018"
default = []

[dependencies]
bson = { path = "..", features = ["uuid-1", "chrono-0_4", "serde", "serde_with-3", "serde_json-1"] }
bson = { path = "..", features = ["uuid-1", "chrono-0_4", "serde", "serde_with-3"] }
serde = { version = "1.0", features = ["derive"] }
pretty_assertions = "0.6.1"
hex = "0.4.2"
Expand Down
142 changes: 140 additions & 2 deletions src/bson.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ use std::{
ops::Index,
};

use serde_json::{json, Value};

pub use crate::document::Document;
use crate::{oid, raw::CString, spec::ElementType, Binary, Decimal128};
use crate::{base64, oid, raw::CString, spec::ElementType, Binary, Decimal128};

/// Possible BSON value types.
#[derive(Clone, Default, PartialEq)]
Expand Down Expand Up @@ -448,7 +450,143 @@ where
}
}

/// This will create the [relaxed Extended JSON v2](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/) representation of the provided [`Bson`](../enum.Bson.html).
impl From<Bson> for Value {
fn from(bson: Bson) -> Self {
bson.into_relaxed_extjson()
}
}

impl Bson {
/// Converts the Bson value into its [relaxed extended JSON representation](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/).
pub fn into_relaxed_extjson(self) -> Value {
match self {
Bson::Double(v) if v.is_nan() => {
let s = if v.is_sign_negative() { "-NaN" } else { "NaN" };

json!({ "$numberDouble": s })
}
Bson::Double(v) if v.is_infinite() => {
let s = if v.is_sign_negative() {
"-Infinity"
} else {
"Infinity"
};

json!({ "$numberDouble": s })
}
Bson::Double(v) => json!(v),
Bson::String(v) => json!(v),
Bson::Array(v) => Value::Array(v.into_iter().map(Bson::into_relaxed_extjson).collect()),
Bson::Document(v) => Value::Object(
v.into_iter()
.map(|(k, v)| (k, v.into_relaxed_extjson()))
.collect(),
),
Bson::Boolean(v) => json!(v),
Bson::Null => Value::Null,
Bson::RegularExpression(Regex { pattern, options }) => {
let mut chars: Vec<_> = options.as_str().chars().collect();
chars.sort_unstable();

let options: String = chars.into_iter().collect();

json!({
"$regularExpression": {
"pattern": pattern.into_string(),
"options": options,
}
})
}
Bson::JavaScriptCode(code) => json!({ "$code": code }),
Bson::JavaScriptCodeWithScope(JavaScriptCodeWithScope { code, scope }) => json!({
"$code": code,
"$scope": Bson::Document(scope).into_relaxed_extjson(),
}),
Bson::Int32(v) => v.into(),
Bson::Int64(v) => v.into(),
Bson::Timestamp(Timestamp { time, increment }) => json!({
"$timestamp": {
"t": time,
"i": increment,
}
}),
Bson::Binary(Binary { subtype, ref bytes }) => {
let tval: u8 = From::from(subtype);
json!({
"$binary": {
"base64": base64::encode(bytes),
"subType": hex::encode([tval]),
}
})
}
Bson::ObjectId(v) => json!({"$oid": v.to_hex()}),
Bson::DateTime(v) if v.timestamp_millis() >= 0 && v.to_time_0_3().year() <= 9999 => {
json!({
// Unwrap safety: timestamps in the guarded range can always be formatted.
"$date": v.try_to_rfc3339_string().unwrap(),
})
}
Bson::DateTime(v) => json!({
"$date": { "$numberLong": v.timestamp_millis().to_string() },
}),
Bson::Symbol(v) => json!({ "$symbol": v }),
Bson::Decimal128(v) => json!({ "$numberDecimal": v.to_string() }),
Bson::Undefined => json!({ "$undefined": true }),
Bson::MinKey => json!({ "$minKey": 1 }),
Bson::MaxKey => json!({ "$maxKey": 1 }),
Bson::DbPointer(DbPointer {
ref namespace,
ref id,
}) => json!({
"$dbPointer": {
"$ref": namespace,
"$id": {
"$oid": id.to_hex()
}
}
}),
}
}

/// Converts the Bson value into its [canonical extended JSON representation](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/).
pub fn into_canonical_extjson(self) -> Value {
match self {
Bson::Int32(i) => json!({ "$numberInt": i.to_string() }),
Bson::Int64(i) => json!({ "$numberLong": i.to_string() }),
Bson::Double(f) if f.is_normal() => {
let mut s = f.to_string();
if f.fract() == 0.0 {
s.push_str(".0");
}

json!({ "$numberDouble": s })
}
Bson::Double(f) if f == 0.0 => {
let s = if f.is_sign_negative() { "-0.0" } else { "0.0" };

json!({ "$numberDouble": s })
}
Bson::DateTime(date) => {
json!({ "$date": { "$numberLong": date.timestamp_millis().to_string() } })
}
Bson::Array(arr) => {
Value::Array(arr.into_iter().map(Bson::into_canonical_extjson).collect())
}
Bson::Document(arr) => Value::Object(
arr.into_iter()
.map(|(k, v)| (k, v.into_canonical_extjson()))
.collect(),
),
Bson::JavaScriptCodeWithScope(JavaScriptCodeWithScope { code, scope }) => json!({
"$code": code,
"$scope": Bson::Document(scope).into_canonical_extjson(),
}),

other => other.into_relaxed_extjson(),
}
}

/// Get the [`ElementType`] of this value.
pub fn element_type(&self) -> ElementType {
match *self {
Expand Down Expand Up @@ -530,7 +668,7 @@ impl Bson {
} else {
doc! {
"$binary": {
"base64": crate::base64::encode(bytes),
"base64": base64::encode(bytes),
"subType": hex::encode([tval]),
}
}
Expand Down
36 changes: 17 additions & 19 deletions src/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,16 @@ use crate::error::{Error, Result};
///
/// ### `serde_helpers`
/// The `bson` crate provides a number of useful helpers for serializing and deserializing
/// various datetime types to and from different formats using the [`serde_with`](https://docs.rs/serde_with/1.11.0/serde_with/)
/// crate.
///
/// > **Note:** All helpers in this module require use of the [`#[serde_as]`](https://docs.rs/serde_with/latest/serde_with/attr.serde_as.html)
/// > attribute on the struct. This enables the enhanced serialization behavior provided by
/// > `serde_with-3`.
///
/// For example, to serialize a [`chrono::DateTime`] as a BSON datetime, you can use
/// various datetime types to and from different formats. For example, to serialize a
/// [`chrono::DateTime`] as a BSON datetime, you can use
/// [`crate::serde_helpers::datetime::FromChrono04DateTime`].
/// Similarly, to serialize a BSON [`DateTime`] to a string, you can use
/// [`crate::serde_helpers::datetime::AsRfc3339String`]. Check out the
/// [`crate::serde_helpers`] module documentation for a list of all of the helpers
/// offered by the crate.
/// [`crate::serde_helpers`] module documentation for a list of all of the helpers offered by the
/// crate.
///
/// ```rust
/// # #[cfg(all(feature = "chrono-0_4", feature = "serde_with-3"))]
/// # #[cfg(feature = "chrono-0_4")]
/// # {
/// use serde::{Serialize, Deserialize};
/// use serde_with::serde_as;
Expand All @@ -143,26 +138,29 @@ use crate::error::{Error, Result};
/// chrono_as_bson: chrono::DateTime<chrono::Utc>,
///
/// // serializes as an RFC 3339 / ISO-8601 string.
/// // this requires the "serde_with-3" feature flag
/// #[serde_as(as = "datetime::AsRfc3339String")]
/// bson_as_string: bson::DateTime,
/// }
/// # }
/// ```
/// The main benefit of using the [`serde_with`](https://docs.rs/serde_with/1.11.0/serde_with/) crate
/// is that it can handle nested [`chrono::DateTime`] values (e.g. in [`Option`] or [`Vec`]).
/// ### The `serde_with-3` feature flag
///
/// The `serde_with-3` feature can be enabled to support more ergonomic serde attributes for
/// (de)serializing [`chrono::DateTime`] from/to BSON via the [`serde_with`](https://docs.rs/serde_with/1.11.0/serde_with/)
/// crate. The main benefit of this compared to the regular `serde_helpers` is that `serde_with-3`
/// can handle nested [`chrono::DateTime`] values (e.g. in [`Option`]), whereas the former only
/// works on fields that are exactly [`chrono::DateTime`].
/// ```
/// # #[cfg(all(feature = "chrono-0_4", feature = "serde_with-3"))]
/// # {
/// use serde::{Deserialize, Serialize};
/// use serde_with::serde_as;
/// use bson::{doc, serde_helpers::datetime};
/// use bson::doc;
///
/// #[serde_as]
/// #[serde_with::serde_as]
/// #[derive(Deserialize, Serialize, PartialEq, Debug)]
/// struct Foo {
/// /// serializes as a BSON datetime rather than using [`chrono::DateTime`]'s serialization
/// #[serde_as(as = "Option<datetime::FromChrono04DateTime>")]
/// /// Serializes as a BSON datetime rather than using [`chrono::DateTime`]'s serialization
/// #[serde_as(as = "Option<bson::DateTime>")]
/// as_bson: Option<chrono::DateTime<chrono::Utc>>,
/// }
///
Expand Down
5 changes: 2 additions & 3 deletions src/de/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ impl<'de> serde::de::MapAccess<'de> for DocumentAccess<'de> {
match &self.elem {
None => Ok(None),
Some(elem) => seed
.deserialize(BorrowedStrDeserializer::new(elem.key().as_str()))
.deserialize(BorrowedStrDeserializer::new(elem.key()))
.map(Some),
}
}
Expand Down Expand Up @@ -421,8 +421,7 @@ impl<'de> serde::de::EnumAccess<'de> for DocumentAccess<'de> {
Some(e) => e,
None => return Err(Error::end_of_stream()),
};
let de: BorrowedStrDeserializer<'_, Error> =
BorrowedStrDeserializer::new(elem.key().as_str());
let de: BorrowedStrDeserializer<'_, Error> = BorrowedStrDeserializer::new(elem.key());
let key = seed.deserialize(de)?;
Ok((key, self))
}
Expand Down
25 changes: 9 additions & 16 deletions src/decimal128.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,7 @@ impl std::str::FromStr for ParsedDecimal128 {
exp_str = post;
}
}
// Initially parse the exponent as an i128 to handle corner cases with zero
// coefficients and very large exponents.
let mut wide_exp = exp_str.parse::<i128>().map_err(|e| {
let mut exp = exp_str.parse::<i16>().map_err(|e| {
Error::decimal128(Decimal128ErrorKind::InvalidExponent {}).with_message(e)
})?;

Expand All @@ -369,7 +367,7 @@ impl std::str::FromStr for ParsedDecimal128 {
.len()
.try_into()
.map_err(|_| Error::decimal128(Decimal128ErrorKind::Underflow {}))?;
wide_exp = wide_exp
exp = exp
.checked_sub(exp_adj)
.ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Underflow {}))?;
joined_str = format!("{}{}", pre, post);
Expand All @@ -388,17 +386,16 @@ impl std::str::FromStr for ParsedDecimal128 {
let exp_adj = (len - decimal_str.len())
.try_into()
.map_err(|_| Error::decimal128(Decimal128ErrorKind::Overflow {}))?;
wide_exp = wide_exp
exp = exp
.checked_add(exp_adj)
.ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Overflow {}))?;
}
}

// Check exponent limits
const TINY: i128 = Exponent::TINY as i128;
if wide_exp < TINY {
if exp < Exponent::TINY {
if decimal_str != "0" {
let delta = (TINY - wide_exp)
let delta = (Exponent::TINY - exp)
.try_into()
.map_err(|_| Error::decimal128(Decimal128ErrorKind::Overflow {}))?;
let new_precision = decimal_str
Expand All @@ -407,13 +404,12 @@ impl std::str::FromStr for ParsedDecimal128 {
.ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Overflow {}))?;
decimal_str = round_decimal_str(decimal_str, new_precision)?;
}
wide_exp = Exponent::TINY.into();
exp = Exponent::TINY;
}
let padded_str;
const MAX: i128 = Exponent::MAX as i128;
if wide_exp > MAX {
if exp > Exponent::MAX {
if decimal_str != "0" {
let delta = (wide_exp - MAX)
let delta = (exp - Exponent::MAX)
.try_into()
.map_err(|_| Error::decimal128(Decimal128ErrorKind::Overflow {}))?;
if decimal_str
Expand All @@ -427,13 +423,10 @@ impl std::str::FromStr for ParsedDecimal128 {
padded_str = format!("{}{}", decimal_str, "0".repeat(delta));
decimal_str = &padded_str;
}
wide_exp = Exponent::MAX.into();
exp = Exponent::MAX;
}

// Assemble the final value
let exp: i16 = wide_exp
.try_into()
.map_err(|_| Error::decimal128(Decimal128ErrorKind::Overflow {}))?;
let exponent = Exponent::from_native(exp);
let coeff: u128 = decimal_str.parse().map_err(|e: ParseIntError| {
Error::decimal128(Decimal128ErrorKind::InvalidCoefficient {}).with_message(e)
Expand Down
2 changes: 0 additions & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
//! Contains the error-related types for the `bson` crate.
mod decimal128;
mod oid;
mod uuid;
Expand Down
Loading