diff --git a/Cargo.toml b/Cargo.toml index 1e7e43db..307fc803 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ 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" @@ -57,7 +58,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"] } +serde_json = { version = "1.0", features = ["preserve_order"], optional = true } indexmap = "2.1.0" hex = "0.4.2" base64 = "0.22.1" @@ -84,6 +85,7 @@ 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] diff --git a/README.md b/README.md index e9d98592..eafc2ab7 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,11 @@ 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 diff --git a/serde-tests/Cargo.toml b/serde-tests/Cargo.toml index 12a09e8c..05878dee 100644 --- a/serde-tests/Cargo.toml +++ b/serde-tests/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" default = [] [dependencies] -bson = { path = "..", features = ["uuid-1", "chrono-0_4", "serde", "serde_with-3"] } +bson = { path = "..", features = ["uuid-1", "chrono-0_4", "serde", "serde_with-3", "serde_json-1"] } serde = { version = "1.0", features = ["derive"] } pretty_assertions = "0.6.1" hex = "0.4.2" diff --git a/src/bson.rs b/src/bson.rs index 7bcac566..69ec9d6d 100644 --- a/src/bson.rs +++ b/src/bson.rs @@ -28,10 +28,8 @@ use std::{ ops::Index, }; -use serde_json::{json, Value}; - pub use crate::document::Document; -use crate::{base64, oid, raw::CString, spec::ElementType, Binary, Decimal128}; +use crate::{oid, raw::CString, spec::ElementType, Binary, Decimal128}; /// Possible BSON value types. #[derive(Clone, Default, PartialEq)] @@ -450,143 +448,7 @@ 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 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 { @@ -668,7 +530,7 @@ impl Bson { } else { doc! { "$binary": { - "base64": base64::encode(bytes), + "base64": crate::base64::encode(bytes), "subType": hex::encode([tval]), } } diff --git a/src/error.rs b/src/error.rs index 7b9d5918..9dd62185 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,5 @@ +//! Contains the error-related types for the `bson` crate. + mod decimal128; mod oid; mod uuid; diff --git a/src/extjson.rs b/src/extjson.rs index a23ca419..f661cf4a 100644 --- a/src/extjson.rs +++ b/src/extjson.rs @@ -1,92 +1,3 @@ -//! Deserialization and serialization of [MongoDB Extended JSON v2](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/) -//! -//! ## Overview of Extended JSON -//! -//! MongoDB Extended JSON (abbreviated extJSON) is format of JSON that allows for the encoding of -//! BSON type information. Normal JSON cannot unambiguously represent all BSON types losslessly, so -//! an extension was designed to include conventions for representing those types. -//! -//! For example, a BSON binary is represented by the following format: -//! ```text -//! { -//! "$binary": { -//! "base64": , -//! "subType": , -//! } -//! } -//! ``` -//! For more information on extJSON and the complete list of translations, see the [official MongoDB documentation](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). -//! -//! All MongoDB drivers and BSON libraries interpret and produce extJSON, so it can serve as a -//! useful tool for communicating between applications where raw BSON bytes cannot be used (e.g. via -//! JSON REST APIs). It's also useful for representing BSON data as a string. -//! -//! ### Canonical and Relaxed Modes -//! -//! There are two modes of extJSON: "Canonical" and "Relaxed". They are the same except for the -//! following differences: -//! - In relaxed mode, all BSON numbers are represented by the JSON number type, rather than the -//! object notation. -//! - In relaxed mode, the string in the datetime object notation is RFC 3339 (ISO-8601) formatted -//! (if the date is after 1970). -//! -//! e.g. -//! ```rust -//! # use bson::bson; -//! let doc = bson!({ "x": 5, "d": bson::DateTime::now() }); -//! -//! println!("relaxed: {}", doc.clone().into_relaxed_extjson()); -//! // relaxed: "{"x":5,"d":{"$date":"2020-06-01T22:19:13.075Z"}}" -//! -//! println!("canonical: {}", doc.into_canonical_extjson()); -//! // canonical: {"x":{"$numberInt":"5"},"d":{"$date":{"$numberLong":"1591050020711"}}} -//! ``` -//! -//! Canonical mode is useful when BSON values need to be round tripped without losing any type -//! information. Relaxed mode is more useful when debugging or logging BSON data. -//! -//! ## Deserializing Extended JSON -//! -//! Extended JSON can be deserialized using [`Bson`](../enum.Bson.html)'s -//! `TryFrom` implementation. This implementation accepts both canonical and -//! relaxed extJSON, and the two modes can even be mixed within a single representation. -//! -//! e.g. -//! ```rust -//! # use bson::Bson; -//! # use serde_json::json; -//! # use std::convert::{TryFrom, TryInto}; -//! let json_doc = json!({ "x": 5i32, "y": { "$numberInt": "5" }, "z": { "subdoc": "hello" } }); -//! let bson: Bson = json_doc.try_into().unwrap(); // Bson::Document(...) -//! -//! let json_date = json!({ "$date": { "$numberLong": "1590972160292" } }); -//! let bson_date: Bson = json_date.try_into().unwrap(); // Bson::DateTime(...) -//! -//! let invalid_ext_json = json!({ "$numberLong": 5 }); -//! Bson::try_from(invalid_ext_json).expect_err("5 should be a string"); -//! ``` -//! -//! ## Serializing to Extended JSON -//! -//! Extended JSON can be created via [`Bson`](../enum.Bson.html)'s `Into` -//! implementation (which will create relaxed extJSON), -//! [`Bson::into_relaxed_extjson`](../enum.Bson.html#method.into_relaxed_extjson), and -//! [`Bson::into_canonical_extjson`](../enum.Bson.html#method.into_canonical_extjson). -//! -//! e.g. -//! ```rust -//! # use bson::{bson, oid}; -//! let doc = bson!({ "x": 5i32, "_id": oid::ObjectId::new() }); -//! -//! let relaxed_extjson: serde_json::Value = doc.clone().into(); -//! println!("{}", relaxed_extjson); // { "x": 5, "_id": { "$oid": } } -//! -//! let relaxed_extjson = doc.clone().into_relaxed_extjson(); -//! println!("{}", relaxed_extjson); // { "x": 5, "_id": { "$oid": } } -//! -//! let canonical_extjson = doc.into_canonical_extjson(); -//! println!("{}", canonical_extjson); // { "x": { "$numberInt": "5" }, "_id": { "$oid": } } -//! ``` - -pub mod de; +#[cfg(feature = "serde_json-1")] +pub(crate) mod json; pub(crate) mod models; diff --git a/src/extjson/de.rs b/src/extjson/de.rs deleted file mode 100644 index f5f1af3b..00000000 --- a/src/extjson/de.rs +++ /dev/null @@ -1,187 +0,0 @@ -//! Deserializing [MongoDB Extended JSON v2](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/) -//! -//! ## Usage -//! -//! Extended JSON can be deserialized using [`Bson`](../../enum.Bson.html)'s -//! `TryFrom` implementation. This implementation accepts both canonical and -//! relaxed extJSON, and the two modes can even be mixed within a single representation. -//! -//! e.g. -//! ```rust -//! # use bson::Bson; -//! # use serde_json::json; -//! # use std::convert::{TryFrom, TryInto}; -//! let json_doc = json!({ "x": 5i32, "y": { "$numberInt": "5" }, "z": { "subdoc": "hello" } }); -//! let bson: Bson = json_doc.try_into().unwrap(); // Bson::Document(...) -//! -//! let json_date = json!({ "$date": { "$numberLong": "1590972160292" } }); -//! let bson_date: Bson = json_date.try_into().unwrap(); // Bson::DateTime(...) -//! -//! let invalid_ext_json = json!({ "$numberLong": 5 }); -//! Bson::try_from(invalid_ext_json).expect_err("5 should be a string"); -//! ``` - -use std::convert::{TryFrom, TryInto}; - -use serde::de::{Error as _, Unexpected}; - -use crate::{ - error::{Error, Result}, - extjson::models, - Bson, - Document, -}; - -impl From for Error { - fn from(error: serde_json::Error) -> Self { - Self::deserialization(error) - } -} - -/// This converts from the input JSON object as if it were [MongoDB Extended JSON v2](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). -impl TryFrom> for Bson { - type Error = Error; - - fn try_from(obj: serde_json::Map) -> Result { - if obj.contains_key("$oid") { - let oid: models::ObjectId = serde_json::from_value(obj.into())?; - return Ok(Bson::ObjectId(oid.parse()?)); - } - - if obj.contains_key("$symbol") { - let symbol: models::Symbol = serde_json::from_value(obj.into())?; - return Ok(Bson::Symbol(symbol.value)); - } - - if obj.contains_key("$regularExpression") { - let regex: models::Regex = serde_json::from_value(obj.into())?; - return Ok(regex.parse()?.into()); - } - - if obj.contains_key("$numberInt") { - let int: models::Int32 = serde_json::from_value(obj.into())?; - return Ok(Bson::Int32(int.parse()?)); - } - - if obj.contains_key("$numberLong") { - let int: models::Int64 = serde_json::from_value(obj.into())?; - return Ok(Bson::Int64(int.parse()?)); - } - - if obj.contains_key("$numberDouble") { - let double: models::Double = serde_json::from_value(obj.into())?; - return Ok(Bson::Double(double.parse()?)); - } - - if obj.contains_key("$numberDecimal") { - let decimal: models::Decimal128 = serde_json::from_value(obj.into())?; - return Ok(Bson::Decimal128(decimal.parse()?)); - } - - if obj.contains_key("$binary") { - let binary: models::Binary = serde_json::from_value(obj.into())?; - return Ok(Bson::Binary(binary.parse()?)); - } - - if obj.contains_key("$uuid") { - let uuid: models::Uuid = serde_json::from_value(obj.into())?; - return Ok(Bson::Binary(uuid.parse()?)); - } - - if obj.contains_key("$code") { - let code_w_scope: models::JavaScriptCodeWithScope = serde_json::from_value(obj.into())?; - return match code_w_scope.scope { - Some(scope) => Ok(crate::JavaScriptCodeWithScope { - code: code_w_scope.code, - scope: scope.try_into()?, - } - .into()), - None => Ok(Bson::JavaScriptCode(code_w_scope.code)), - }; - } - - if obj.contains_key("$timestamp") { - let ts: models::Timestamp = serde_json::from_value(obj.into())?; - return Ok(ts.parse().into()); - } - - if obj.contains_key("$date") { - let extjson_datetime: models::DateTime = serde_json::from_value(obj.into())?; - return Ok(Bson::DateTime(extjson_datetime.parse()?)); - } - - if obj.contains_key("$minKey") { - let min_key: models::MinKey = serde_json::from_value(obj.into())?; - return min_key.parse(); - } - - if obj.contains_key("$maxKey") { - let max_key: models::MaxKey = serde_json::from_value(obj.into())?; - return max_key.parse(); - } - - if obj.contains_key("$dbPointer") { - let db_ptr: models::DbPointer = serde_json::from_value(obj.into())?; - return Ok(db_ptr.parse()?.into()); - } - - if obj.contains_key("$undefined") { - let undefined: models::Undefined = serde_json::from_value(obj.into())?; - return undefined.parse(); - } - - Ok(Bson::Document(obj.try_into()?)) - } -} - -/// This converts from the input JSON as if it were [MongoDB Extended JSON v2](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). -impl TryFrom for Bson { - type Error = Error; - - fn try_from(value: serde_json::Value) -> Result { - match value { - serde_json::Value::Number(x) => x - .as_i64() - .map(|i| { - if i >= i32::MIN as i64 && i <= i32::MAX as i64 { - Bson::Int32(i as i32) - } else { - Bson::Int64(i) - } - }) - .or_else(|| x.as_f64().map(Bson::from)) - .ok_or_else(|| { - Error::invalid_value( - Unexpected::Other(format!("{}", x).as_str()), - &"a number that could fit in i32, i64, or f64", - ) - }), - serde_json::Value::String(x) => Ok(x.into()), - serde_json::Value::Bool(x) => Ok(x.into()), - serde_json::Value::Array(x) => Ok(Bson::Array( - x.into_iter() - .map(Bson::try_from) - .collect::>>()?, - )), - serde_json::Value::Null => Ok(Bson::Null), - serde_json::Value::Object(map) => map.try_into(), - } - } -} - -/// This converts from the input JSON as if it were [MongoDB Extended JSON v2](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). -impl TryFrom> for Document { - type Error = Error; - - fn try_from(obj: serde_json::Map) -> Result { - Ok(obj - .into_iter() - .map(|(k, v)| -> Result<(String, Bson)> { - let value: Bson = v.try_into()?; - Ok((k, value)) - }) - .collect::>>()? - .into_iter() - .collect()) - } -} diff --git a/src/extjson/json.rs b/src/extjson/json.rs new file mode 100644 index 00000000..1743a4cb --- /dev/null +++ b/src/extjson/json.rs @@ -0,0 +1,316 @@ +use std::convert::{TryFrom, TryInto}; + +use serde::de::{Error as _, Unexpected}; +use serde_json::{json, Value}; + +use crate::{ + error::{Error, Result}, + extjson::models, + Binary, + Bson, + DbPointer, + Document, + JavaScriptCodeWithScope, + Regex, + Timestamp, +}; + +impl From for Error { + fn from(error: serde_json::Error) -> Self { + Self::deserialization(error) + } +} + +/// Converts the [`serde_json::Map`] into [`Bson`]. This conversion can interpret both canonical +/// and relaxed [extended JSON](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). +impl TryFrom> for Bson { + type Error = Error; + + fn try_from(obj: serde_json::Map) -> Result { + if obj.contains_key("$oid") { + let oid: models::ObjectId = serde_json::from_value(obj.into())?; + return Ok(Bson::ObjectId(oid.parse()?)); + } + + if obj.contains_key("$symbol") { + let symbol: models::Symbol = serde_json::from_value(obj.into())?; + return Ok(Bson::Symbol(symbol.value)); + } + + if obj.contains_key("$regularExpression") { + let regex: models::Regex = serde_json::from_value(obj.into())?; + return Ok(regex.parse()?.into()); + } + + if obj.contains_key("$numberInt") { + let int: models::Int32 = serde_json::from_value(obj.into())?; + return Ok(Bson::Int32(int.parse()?)); + } + + if obj.contains_key("$numberLong") { + let int: models::Int64 = serde_json::from_value(obj.into())?; + return Ok(Bson::Int64(int.parse()?)); + } + + if obj.contains_key("$numberDouble") { + let double: models::Double = serde_json::from_value(obj.into())?; + return Ok(Bson::Double(double.parse()?)); + } + + if obj.contains_key("$numberDecimal") { + let decimal: models::Decimal128 = serde_json::from_value(obj.into())?; + return Ok(Bson::Decimal128(decimal.parse()?)); + } + + if obj.contains_key("$binary") { + let binary: models::Binary = serde_json::from_value(obj.into())?; + return Ok(Bson::Binary(binary.parse()?)); + } + + if obj.contains_key("$uuid") { + let uuid: models::Uuid = serde_json::from_value(obj.into())?; + return Ok(Bson::Binary(uuid.parse()?)); + } + + if obj.contains_key("$code") { + let code_w_scope: models::JavaScriptCodeWithScope = serde_json::from_value(obj.into())?; + return match code_w_scope.scope { + Some(scope) => Ok(crate::JavaScriptCodeWithScope { + code: code_w_scope.code, + scope: scope.try_into()?, + } + .into()), + None => Ok(Bson::JavaScriptCode(code_w_scope.code)), + }; + } + + if obj.contains_key("$timestamp") { + let ts: models::Timestamp = serde_json::from_value(obj.into())?; + return Ok(ts.parse().into()); + } + + if obj.contains_key("$date") { + let extjson_datetime: models::DateTime = serde_json::from_value(obj.into())?; + return Ok(Bson::DateTime(extjson_datetime.parse()?)); + } + + if obj.contains_key("$minKey") { + let min_key: models::MinKey = serde_json::from_value(obj.into())?; + return min_key.parse(); + } + + if obj.contains_key("$maxKey") { + let max_key: models::MaxKey = serde_json::from_value(obj.into())?; + return max_key.parse(); + } + + if obj.contains_key("$dbPointer") { + let db_ptr: models::DbPointer = serde_json::from_value(obj.into())?; + return Ok(db_ptr.parse()?.into()); + } + + if obj.contains_key("$undefined") { + let undefined: models::Undefined = serde_json::from_value(obj.into())?; + return undefined.parse(); + } + + Ok(Bson::Document(obj.try_into()?)) + } +} + +/// Converts the [`serde_json::Value`] into [`Bson`]. This conversion can interpret both canonical +/// and relaxed [extended JSON](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). +impl TryFrom for Bson { + type Error = Error; + + fn try_from(value: serde_json::Value) -> Result { + match value { + serde_json::Value::Number(x) => x + .as_i64() + .map(|i| { + if i >= i32::MIN as i64 && i <= i32::MAX as i64 { + Bson::Int32(i as i32) + } else { + Bson::Int64(i) + } + }) + .or_else(|| x.as_f64().map(Bson::from)) + .ok_or_else(|| { + Error::invalid_value( + Unexpected::Other(format!("{}", x).as_str()), + &"a number that could fit in i32, i64, or f64", + ) + }), + serde_json::Value::String(x) => Ok(x.into()), + serde_json::Value::Bool(x) => Ok(x.into()), + serde_json::Value::Array(x) => Ok(Bson::Array( + x.into_iter() + .map(Bson::try_from) + .collect::>>()?, + )), + serde_json::Value::Null => Ok(Bson::Null), + serde_json::Value::Object(map) => map.try_into(), + } + } +} + +/// Converts the [`serde_json::Map`] into a [`Document`]. This conversion can interpret both +/// canonical and relaxed [extended JSON](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). +impl TryFrom> for Document { + type Error = Error; + + fn try_from(obj: serde_json::Map) -> Result { + Ok(obj + .into_iter() + .map(|(k, v)| -> Result<(String, Bson)> { + let value: Bson = v.try_into()?; + Ok((k, value)) + }) + .collect::>>()? + .into_iter() + .collect()) + } +} + +/// Converts [`Bson`] into a [`serde_json::Value`] in relaxed +/// [extended JSON](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). +impl From for Value { + fn from(bson: Bson) -> Self { + bson.into_relaxed_extjson() + } +} + +impl Bson { + /// Converts this value into a [`serde_json::Value`] in relaxed + /// [extended JSON](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/) + /// format. + 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": crate::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 this value into a [`serde_json::Value`] in canonical + /// [extended JSON](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/) + /// format. + 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(), + } + } +} diff --git a/src/extjson/models.rs b/src/extjson/models.rs index 88d3a2ca..d7107d4e 100644 --- a/src/extjson/models.rs +++ b/src/extjson/models.rs @@ -1,5 +1,7 @@ //! A module defining serde models for the extended JSON representations of the various BSON types. +#![cfg_attr(not(feature = "serde_json-1"), allow(unused))] + use serde::{ de::{Error as _, Unexpected}, Deserialize, @@ -196,6 +198,7 @@ impl Uuid { } } +#[cfg(feature = "serde_json-1")] #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub(crate) struct JavaScriptCodeWithScope { diff --git a/src/lib.rs b/src/lib.rs index a4972cc4..ae9aae3f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,6 +68,7 @@ //! | `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. | no | //! | `compat-3-0-0` | Required for future compatibility if default features are disabled. | 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. | 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. | no | //! //! ## BSON values //! @@ -222,6 +223,94 @@ //! failing field. This feature does incur a small CPU and memory overhead during (de)serialization //! and should be enabled with care in performance-sensitive environments. //! +//! ## Working with Extended JSON +//! +//! MongoDB Extended JSON (extJSON) is a format of JSON that allows for the encoding +//! of BSON type information. Normal JSON cannot unambiguously represent all BSON types losslessly, +//! so an extension was designed to include conventions for representing those types. +//! +//! For example, a BSON binary is represented by the following format: +//! ```text +//! { +//! "$binary": { +//! "base64": , +//! "subType": , +//! } +//! } +//! ``` +//! For more information on extJSON and the complete list of translations, see the [official MongoDB documentation](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). +//! +//! All MongoDB drivers and BSON libraries interpret and produce extJSON, so it can serve as a +//! useful tool for communicating between applications where raw BSON bytes cannot be used (e.g. via +//! JSON REST APIs). It's also useful for representing BSON data as a string. +//! +//! ### Canonical and Relaxed Modes +//! +//! There are two modes of extJSON: "Canonical" and "Relaxed". They are the same except for the +//! following differences: +//! - In relaxed mode, all BSON numbers are represented by the JSON number type, rather than the +//! object notation. +//! - In relaxed mode, the string in the datetime object notation is RFC 3339 (ISO-8601) formatted +//! (if the date is after 1970). +//! +//! e.g. +//! ```rust +//! # use bson::bson; +//! let doc = bson!({ "x": 5, "d": bson::DateTime::now() }); +//! +//! println!("relaxed: {}", doc.clone().into_relaxed_extjson()); +//! // relaxed: "{"x":5,"d":{"$date":"2020-06-01T22:19:13.075Z"}}" +//! +//! println!("canonical: {}", doc.into_canonical_extjson()); +//! // canonical: {"x":{"$numberInt":"5"},"d":{"$date":{"$numberLong":"1591050020711"}}} +//! ``` +//! +//! Canonical mode is useful when BSON values need to be round tripped without losing any type +//! information. Relaxed mode is more useful when debugging or logging BSON data. +//! +//! ### Deserializing from Extended JSON +//! +//! Extended JSON can be deserialized into a [`Bson`] value using the +//! [`TryFrom`](https://docs.rs/bson/latest/bson/enum.Bson.html#impl-TryFrom%3CValue%3E-for-Bson) +//! implementation for [`serde_json::Value`]. This implementation accepts both canonical and relaxed +//! extJSON, and the two modes can be mixed within a single representation. +//! +//! e.g. +//! ```rust +//! # use bson::Bson; +//! # use serde_json::json; +//! # use std::convert::{TryFrom, TryInto}; +//! let json_doc = json!({ "x": 5i32, "y": { "$numberInt": "5" }, "z": { "subdoc": "hello" } }); +//! let bson: Bson = json_doc.try_into().unwrap(); // Bson::Document(...) +//! +//! let json_date = json!({ "$date": { "$numberLong": "1590972160292" } }); +//! let bson_date: Bson = json_date.try_into().unwrap(); // Bson::DateTime(...) +//! +//! let invalid_ext_json = json!({ "$numberLong": 5 }); +//! Bson::try_from(invalid_ext_json).expect_err("5 should be a string"); +//! ``` +//! +//! ### Serializing to Extended JSON +//! +//! A [`Bson`] value can be serialized into extJSON using the [`Bson::into_relaxed_extjson`] and +//! [`Bson::into_canonical_extjson`] methods. The `Into` implementation for +//! [`Bson`] produces relaxed extJSON. +//! +//! e.g. +//! ```rust +//! # use bson::{bson, oid}; +//! let doc = bson!({ "x": 5i32, "_id": oid::ObjectId::new() }); +//! +//! let relaxed_extjson: serde_json::Value = doc.clone().into(); +//! println!("{}", relaxed_extjson); // { "x": 5, "_id": { "$oid": } } +//! +//! let relaxed_extjson = doc.clone().into_relaxed_extjson(); +//! println!("{}", relaxed_extjson); // { "x": 5, "_id": { "$oid": } } +//! +//! let canonical_extjson = doc.into_canonical_extjson(); +//! println!("{}", canonical_extjson); // { "x": { "$numberInt": "5" }, "_id": { "$oid": } } +//! ``` +//! //! ## Working with datetimes //! //! The BSON format includes a datetime type, which is modeled in this crate by the @@ -340,7 +429,7 @@ pub mod decimal128; pub mod document; pub mod error; #[cfg(feature = "serde")] -pub mod extjson; +mod extjson; pub mod oid; pub mod raw; #[cfg(feature = "serde")] diff --git a/src/tests/modules/bson.rs b/src/tests/modules/bson.rs index 7dd366a6..f9d3636f 100644 --- a/src/tests/modules/bson.rs +++ b/src/tests/modules/bson.rs @@ -19,9 +19,8 @@ use crate::{ Timestamp, }; -use serde_json::Value; - #[test] +#[cfg(feature = "serde_json-1")] fn to_json() { let _guard = LOCK.run_concurrently(); let mut doc = Document::new(); @@ -32,7 +31,7 @@ fn to_json() { doc.insert("first", Bson::Int32(1)); doc.insert("second", Bson::String("foo".to_owned())); doc.insert("alphanumeric", Bson::String("bar".to_owned())); - let data: Value = Bson::Document(doc).into(); + let data: serde_json::Value = Bson::Document(doc).into(); assert!(data.is_object()); let obj = data.as_object().unwrap();