diff --git a/README.md b/README.md index 64904200..17faf4aa 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ Encoding and decoding support for BSON in Rust - [BSON Values](#bson-values) - [BSON Documents](#bson-documents) - [Modeling BSON with strongly typed data structures](#modeling-bson-with-strongly-typed-data-structures) -- [Breaking Changes](#breaking-changes) - [Contributing](#contributing) - [Running the Tests](#running-the-tests) - [Continuous Integration](#continuous-integration) @@ -185,32 +184,6 @@ separate the "business logic" that operates over the data from the (de)serializa translates the data to/from its serialized form. This can lead to more clear and concise code that is also less error prone. -## Breaking Changes - -In the BSON specification, _unsigned integer types_ are unsupported; for example, `u32`. In the older version of this crate (< `v0.8.0`), if you uses `serde` to serialize _unsigned integer types_ into BSON, it will store them with `Bson::Double` type. From `v0.8.0`, we removed this behavior and simply returned an error when you want to serialize _unsigned integer types_ to BSON. [#72](https://github.com/zonyitoo/bson-rs/pull/72) - -For backward compatibility, we've provided a mod `bson::compat::u2f` to explicitly serialize _unsigned integer types_ into BSON's floating point value as follows: - -```rust -#[test] -fn test_compat_u2f() { - #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] - struct Foo { - #[serde(with = "bson::compat::u2f")] - x: u32 - } - - let foo = Foo { x: 20 }; - let b = bson::to_bson(&foo).unwrap(); - assert_eq!(b, Bson::Document(doc! { "x": Bson::Double(20.0) })); - - let de_foo = bson::from_bson::(b).unwrap(); - assert_eq!(de_foo, foo); -} -``` - -In this example, we added an attribute `#[serde(with = "bson::compat::u2f")]` on field `x`, which will tell `serde` to use the `bson::compat::u2f::serialize` and `bson::compat::u2f::deserialize` methods to process this field. - ## Contributing We encourage and would happily accept contributions in the form of GitHub pull requests. Before opening one, be sure to run the tests locally; check out the [testing section](#running-the-tests) for information on how to do that. Once you open a pull request, your branch will be run against the same testing matrix that we use for our [continuous integration](#continuous-integration) system, so it is usually sufficient to only run the integration tests locally against a standalone. Remember to always run the linter tests before opening a pull request. diff --git a/src/compat/mod.rs b/src/compat/mod.rs deleted file mode 100644 index e2665e0c..00000000 --- a/src/compat/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Backward compatibility - -pub mod u2f; diff --git a/src/compat/u2f.rs b/src/compat/u2f.rs deleted file mode 100644 index 4cec8e5f..00000000 --- a/src/compat/u2f.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! Convert unsigned types to/from `Bson::Double` - -use serde::{Deserialize, Deserializer, Serializer}; - -/// Converts primitive unsigned types to `f64` -pub trait ToF64 { - /// Converts to `f64` value - fn to_f64(&self) -> f64; -} - -impl ToF64 for u8 { - fn to_f64(&self) -> f64 { - *self as f64 - } -} - -impl ToF64 for u16 { - fn to_f64(&self) -> f64 { - *self as f64 - } -} - -impl ToF64 for u32 { - fn to_f64(&self) -> f64 { - *self as f64 - } -} - -impl ToF64 for u64 { - fn to_f64(&self) -> f64 { - *self as f64 - } -} - -/// Serialize unsigned types to `Bson::Double` -pub fn serialize(v: &T, s: S) -> Result -where - T: ToF64, - S: Serializer, -{ - s.serialize_f64(v.to_f64()) -} - -/// Converts from `f64` value -pub trait FromF64 { - /// Converts from `f64` value - fn from_f64(v: f64) -> Self; -} - -impl FromF64 for u8 { - fn from_f64(v: f64) -> u8 { - v as u8 - } -} - -impl FromF64 for u16 { - fn from_f64(v: f64) -> u16 { - v as u16 - } -} - -impl FromF64 for u32 { - fn from_f64(v: f64) -> u32 { - v as u32 - } -} - -impl FromF64 for u64 { - fn from_f64(v: f64) -> u64 { - v as u64 - } -} - -/// Deserialize unsigned types to `Bson::Double` -pub fn deserialize<'de, T, D>(d: D) -> Result -where - D: Deserializer<'de>, - T: FromF64, -{ - f64::deserialize(d).map(T::from_f64) -} diff --git a/src/lib.rs b/src/lib.rs index 8abdc01f..2eae3007 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -204,7 +204,6 @@ pub use self::{ #[macro_use] mod macros; mod bson; -pub mod compat; pub mod de; pub mod decimal128; pub mod document; diff --git a/src/serde_helpers.rs b/src/serde_helpers.rs index 07de26d4..b3aac2cd 100644 --- a/src/serde_helpers.rs +++ b/src/serde_helpers.rs @@ -21,13 +21,18 @@ pub use iso_string_as_bson_datetime::{ serialize as serialize_iso_string_as_bson_datetime, }; pub use timestamp_as_u32::{ - deserialize as deserialize_timestamp_from_u32, serialize as serialize_timestamp_as_u32, + deserialize as deserialize_timestamp_from_u32, + serialize as serialize_timestamp_as_u32, }; +pub use u32_as_f64::{deserialize as deserialize_u32_from_f64, serialize as serialize_u32_as_f64}; pub use u32_as_timestamp::{ - deserialize as deserialize_u32_from_timestamp, serialize as serialize_u32_as_timestamp, + deserialize as deserialize_u32_from_timestamp, + serialize as serialize_u32_as_timestamp, }; +pub use u64_as_f64::{deserialize as deserialize_u64_from_f64, serialize as serialize_u64_as_f64}; pub use uuid_as_binary::{ - deserialize as deserialize_uuid_from_binary, serialize as serialize_uuid_as_binary, + deserialize as deserialize_uuid_from_binary, + serialize as serialize_uuid_as_binary, }; /// Attempts to serialize a u32 as an i32. Errors if an exact conversion is not possible. @@ -59,6 +64,87 @@ pub fn serialize_u64_as_i64(val: &u64, serializer: S) -> Result(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let f = f64::deserialize(deserializer)?; + if (f - f as u32 as f64).abs() <= f64::EPSILON { + Ok(f as u32) + } else { + Err(de::Error::custom(format!( + "cannot convert f64 (BSON double) {} to u32", + f + ))) + } + } + + /// Serializes a u32 as an f64 (BSON double). + pub fn serialize(val: &u32, serializer: S) -> Result { + serializer.serialize_f64(*val as f64) + } +} + +/// Contains functions to serialize a u64 as an f64 (BSON double) and deserialize a +/// u64 from an f64 (BSON double). +/// +/// ```rust +/// # use serde::{Serialize, Deserialize}; +/// # use bson::serde_helpers::u64_as_f64; +/// #[derive(Serialize, Deserialize)] +/// struct FileInfo { +/// #[serde(with = "u64_as_f64")] +/// pub size_bytes: u64, +/// } +/// ``` +pub mod u64_as_f64 { + use serde::{de, ser, Deserialize, Deserializer, Serializer}; + + /// Deserializes a u64 from an f64 (BSON double). Errors if an exact conversion is not possible. + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let f = f64::deserialize(deserializer)?; + if (f - f as u64 as f64).abs() <= f64::EPSILON { + Ok(f as u64) + } else { + Err(de::Error::custom(format!( + "cannot convert f64 (BSON double) {} to u64", + f + ))) + } + } + + /// Serializes a u64 as an f64 (BSON double). Errors if an exact conversion is not possible. + pub fn serialize(val: &u64, serializer: S) -> Result { + if val < &u64::MAX && *val == *val as f64 as u64 { + serializer.serialize_f64(*val as f64) + } else { + Err(ser::Error::custom(format!( + "cannot convert u64 {} to f64 (BSON double)", + val + ))) + } + } +} + /// Contains functions to serialize a chrono::DateTime as a bson::DateTime and deserialize a /// chrono::DateTime from a bson::DateTime. /// diff --git a/src/tests/serde.rs b/src/tests/serde.rs index 7fec1b8f..11429fb1 100644 --- a/src/tests/serde.rs +++ b/src/tests/serde.rs @@ -2,16 +2,15 @@ use crate::{ bson, - compat::u2f, doc, from_bson, from_document, oid::ObjectId, serde_helpers, serde_helpers::{ - hex_string_as_object_id, bson_datetime_as_iso_string, chrono_datetime_as_bson_datetime, + hex_string_as_object_id, iso_string_as_bson_datetime, timestamp_as_u32, u32_as_timestamp, @@ -274,23 +273,6 @@ fn test_ser_datetime() { assert_eq!(xfoo, foo); } -#[test] -fn test_compat_u2f() { - let _guard = LOCK.run_concurrently(); - #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] - struct Foo { - #[serde(with = "u2f")] - x: u32, - } - - let foo = Foo { x: 20 }; - let b = to_bson(&foo).unwrap(); - assert_eq!(b, Bson::Document(doc! { "x": (Bson::Double(20.0)) })); - - let de_foo = from_bson::(b).unwrap(); - assert_eq!(de_foo, foo); -} - #[test] fn test_binary_generic_roundtrip() { let _guard = LOCK.run_concurrently(); @@ -664,6 +646,40 @@ fn test_unsigned_helpers() { }; let doc_result = to_document(&b); assert!(doc_result.is_err()); + + #[derive(Deserialize, Serialize, Debug, PartialEq)] + struct F { + #[serde(with = "serde_helpers::u32_as_f64")] + num_1: u32, + #[serde(with = "serde_helpers::u64_as_f64")] + num_2: u64, + } + + let f = F { + num_1: 101, + num_2: 12345, + }; + let doc = to_document(&f).unwrap(); + assert!((doc.get_f64("num_1").unwrap() - 101.0).abs() < f64::EPSILON); + assert!((doc.get_f64("num_2").unwrap() - 12345.0).abs() < f64::EPSILON); + + let back: F = from_document(doc).unwrap(); + assert_eq!(back, f); + + let f = F { + num_1: 1, + // f64 cannot represent many large integers exactly, u64::MAX included + num_2: u64::MAX, + }; + let doc_result = to_document(&f); + assert!(doc_result.is_err()); + + let f = F { + num_1: 1, + num_2: u64::MAX - 255, + }; + let doc_result = to_document(&f); + assert!(doc_result.is_err()); } #[test] @@ -735,9 +751,11 @@ fn test_oid_helpers() { } let oid = ObjectId::new(); - let a = A { oid: oid.to_string() }; + let a = A { + oid: oid.to_string(), + }; let doc = to_document(&a).unwrap(); - assert_eq!(doc.get_object_id("oid").unwrap(), oid); + assert_eq!(doc.get_object_id("oid").unwrap(), oid); let a: A = from_document(doc).unwrap(); assert_eq!(a.oid, oid.to_string()); }