From e03e95e05a8f8da4d71f088c5e06c62488ba77ce Mon Sep 17 00:00:00 2001 From: Isabel Atkinson Date: Wed, 6 Jan 2021 12:27:56 -0500 Subject: [PATCH] RUST-506 Add serde helper functions (#222) --- Cargo.toml | 1 + src/lib.rs | 1 + src/serde_helpers.rs | 309 +++++++++++++++++++++++++++++++++++++++++++ src/tests/serde.rs | 215 +++++++++++++++++++++++++++++- 4 files changed, 524 insertions(+), 2 deletions(-) create mode 100644 src/serde_helpers.rs diff --git a/Cargo.toml b/Cargo.toml index 9dac3c90..782e7ac8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ hex = "0.4.2" decimal = { version = "2.0.4", default_features = false, optional = true } base64 = "0.12.1" lazy_static = "1.4.0" +uuid = "0.8.1" [dev-dependencies] assert_matches = "1.2" diff --git a/src/lib.rs b/src/lib.rs index adf55f4a..8abdc01f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -211,6 +211,7 @@ pub mod document; pub mod extjson; pub mod oid; pub mod ser; +pub mod serde_helpers; pub mod spec; #[cfg(test)] diff --git a/src/serde_helpers.rs b/src/serde_helpers.rs new file mode 100644 index 00000000..a010dd86 --- /dev/null +++ b/src/serde_helpers.rs @@ -0,0 +1,309 @@ +//! Collection of helper functions for serializing to and deserializing from BSON using Serde + +use std::{convert::TryFrom, result::Result}; + +use serde::{ser, Serialize, Serializer}; + +use crate::oid::ObjectId; + +pub use bson_datetime_as_iso_string::{ + deserialize as deserialize_bson_datetime_from_iso_string, + serialize as serialize_bson_datetime_as_iso_string, +}; +pub use chrono_datetime_as_bson_datetime::{ + deserialize as deserialize_chrono_datetime_from_bson_datetime, + serialize as serialize_chrono_datetime_as_bson_datetime, +}; +pub use iso_string_as_bson_datetime::{ + deserialize as deserialize_iso_string_from_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, +}; +pub use u32_as_timestamp::{ + deserialize as deserialize_u32_from_timestamp, + serialize as serialize_u32_as_timestamp, +}; +pub use 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. +pub fn serialize_u32_as_i32(val: &u32, serializer: S) -> Result { + match i32::try_from(*val) { + Ok(val) => serializer.serialize_i32(val), + Err(_) => Err(ser::Error::custom(format!("cannot convert {} to i32", val))), + } +} + +/// Serializes a u32 as an i64. +pub fn serialize_u32_as_i64(val: &u32, serializer: S) -> Result { + serializer.serialize_i64(*val as i64) +} + +/// Attempts to serialize a u64 as an i32. Errors if an exact conversion is not possible. +pub fn serialize_u64_as_i32(val: &u64, serializer: S) -> Result { + match i32::try_from(*val) { + Ok(val) => serializer.serialize_i32(val), + Err(_) => Err(ser::Error::custom(format!("cannot convert {} to i32", val))), + } +} + +/// Attempts to serialize a u64 as an i64. Errors if an exact conversion is not possible. +pub fn serialize_u64_as_i64(val: &u64, serializer: S) -> Result { + match i64::try_from(*val) { + Ok(val) => serializer.serialize_i64(val), + Err(_) => Err(ser::Error::custom(format!("cannot convert {} to i64", val))), + } +} + +/// Contains functions to serialize a chrono::DateTime as a bson::DateTime and deserialize a +/// chrono::DateTime from a bson::DateTime. +/// +/// ```rust +/// # use serde::{Serialize, Deserialize}; +/// # use bson::serde_helpers::chrono_datetime_as_bson_datetime; +/// #[derive(Serialize, Deserialize)] +/// struct Event { +/// #[serde(with = "chrono_datetime_as_bson_datetime")] +/// pub date: chrono::DateTime, +/// } +/// ``` +pub mod chrono_datetime_as_bson_datetime { + use crate::DateTime; + use chrono::Utc; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::result::Result; + + /// Deserializes a chrono::DateTime from a bson::DateTime. + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let datetime = DateTime::deserialize(deserializer)?; + Ok(datetime.into()) + } + + /// Serializes a chrono::DateTime as a bson::DateTime. + pub fn serialize( + val: &chrono::DateTime, + serializer: S, + ) -> Result { + let datetime = DateTime::from(val.to_owned()); + datetime.serialize(serializer) + } +} + +/// Contains functions to serialize an ISO string as a bson::DateTime and deserialize an ISO string +/// from a bson::DateTime. +/// +/// ```rust +/// # use serde::{Serialize, Deserialize}; +/// # use bson::serde_helpers::iso_string_as_bson_datetime; +/// #[derive(Serialize, Deserialize)] +/// struct Event { +/// #[serde(with = "iso_string_as_bson_datetime")] +/// pub date: String, +/// } +/// ``` +pub mod iso_string_as_bson_datetime { + use crate::{Bson, DateTime}; + use serde::{ser, Deserialize, Deserializer, Serialize, Serializer}; + use std::{result::Result, str::FromStr}; + + /// Deserializes an ISO string from a DateTime. + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let date = DateTime::deserialize(deserializer)?; + Ok(date.to_string()) + } + + /// Serializes an ISO string as a DateTime. + pub fn serialize(val: &str, serializer: S) -> Result { + let date = chrono::DateTime::from_str(val).map_err(|_| { + ser::Error::custom(format!("cannot convert {} to chrono::DateTime", val)) + })?; + Bson::DateTime(date).serialize(serializer) + } +} + +/// Contains functions to serialize a bson::DateTime as an ISO string and deserialize a +/// bson::DateTime from an ISO string. +/// +/// ```rust +/// # use serde::{Serialize, Deserialize}; +/// # use bson::serde_helpers::bson_datetime_as_iso_string; +/// #[derive(Serialize, Deserialize)] +/// struct Event { +/// #[serde(with = "bson_datetime_as_iso_string")] +/// pub date: bson::DateTime, +/// } +/// ``` +pub mod bson_datetime_as_iso_string { + use crate::DateTime; + use serde::{de, Deserialize, Deserializer, Serializer}; + use std::{result::Result, str::FromStr}; + + /// Deserializes a bson::DateTime from an ISO string. + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let iso = String::deserialize(deserializer)?; + let date = chrono::DateTime::from_str(&iso).map_err(|_| { + de::Error::custom(format!("cannot convert {} to chrono::DateTime", iso)) + })?; + Ok(DateTime::from(date)) + } + + /// Serializes a bson::DateTime as an ISO string. + pub fn serialize(val: &DateTime, serializer: S) -> Result { + serializer.serialize_str(&val.to_string()) + } +} + +/// Serializes a hex string as an ObjectId. +pub fn serialize_hex_string_as_object_id( + val: &str, + serializer: S, +) -> Result { + match ObjectId::with_string(val) { + Ok(oid) => oid.serialize(serializer), + Err(_) => Err(ser::Error::custom(format!( + "cannot convert {} to ObjectId", + val + ))), + } +} + +/// Contains functions to serialize a Uuid as a bson::Binary and deserialize a Uuid from a +/// bson::Binary. +/// +/// ```rust +/// # use serde::{Serialize, Deserialize}; +/// # use uuid::Uuid; +/// # use bson::serde_helpers::uuid_as_binary; +/// #[derive(Serialize, Deserialize)] +/// struct Item { +/// #[serde(with = "uuid_as_binary")] +/// pub id: Uuid, +/// } +/// ``` +pub mod uuid_as_binary { + use crate::{spec::BinarySubtype, Binary}; + use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + use std::result::Result; + use uuid::Uuid; + + /// Serializes a Uuid as a Binary. + pub fn serialize(val: &Uuid, serializer: S) -> Result { + let binary = Binary { + subtype: BinarySubtype::Uuid, + bytes: val.as_bytes().to_vec(), + }; + binary.serialize(serializer) + } + + /// Deserializes a Uuid from a Binary. + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let binary = Binary::deserialize(deserializer)?; + if binary.subtype == BinarySubtype::Uuid { + if binary.bytes.len() == 16 { + let mut bytes = [0u8; 16]; + bytes.copy_from_slice(&binary.bytes); + Ok(Uuid::from_bytes(bytes)) + } else { + Err(de::Error::custom( + "cannot convert Binary to Uuid: incorrect bytes length", + )) + } + } else { + Err(de::Error::custom( + "cannot convert Binary to Uuid: incorrect binary subtype", + )) + } + } +} + +/// Contains functions to serialize a u32 as a bson::Timestamp and deserialize a u32 from a +/// bson::Timestamp. The u32 should represent seconds since the Unix epoch. +/// +/// ```rust +/// # use serde::{Serialize, Deserialize}; +/// # use bson::serde_helpers::u32_as_timestamp; +/// #[derive(Serialize, Deserialize)] +/// struct Event { +/// #[serde(with = "u32_as_timestamp")] +/// pub time: u32, +/// } +/// ``` +pub mod u32_as_timestamp { + use crate::{Bson, Timestamp}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::result::Result; + + /// Serializes a u32 as a bson::Timestamp. + pub fn serialize(val: &u32, serializer: S) -> Result { + let timestamp = Bson::Timestamp(Timestamp { + time: *val, + increment: 0, + }); + timestamp.serialize(serializer) + } + + /// Deserializes a u32 from a bson::Timestamp. + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let timestamp = Timestamp::deserialize(deserializer)?; + Ok(timestamp.time) + } +} + +/// Contains functions to serialize a bson::Timestamp as a u32 and deserialize a bson::Timestamp +/// from a u32. The u32 should represent seconds since the Unix epoch. Serialization will return an +/// error if the Timestamp has a non-zero increment. +/// +/// ```rust +/// # use serde::{Serialize, Deserialize}; +/// # use bson::{serde_helpers::timestamp_as_u32, Timestamp}; +/// #[derive(Serialize, Deserialize)] +/// struct Item { +/// #[serde(with = "timestamp_as_u32")] +/// pub timestamp: Timestamp, +/// } +/// ``` +pub mod timestamp_as_u32 { + use crate::Timestamp; + use serde::{ser, Deserialize, Deserializer, Serializer}; + use std::result::Result; + + /// Serializes a bson::Timestamp as a u32. Returns an error if the conversion is lossy (i.e. the + /// Timestamp has a non-zero increment). + pub fn serialize(val: &Timestamp, serializer: S) -> Result { + if val.increment != 0 { + return Err(ser::Error::custom( + "Cannot convert Timestamp with a non-zero increment to u32", + )); + } + serializer.serialize_u32(val.time) + } + + /// Deserializes a bson::Timestamp from a u32. + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let time = u32::deserialize(deserializer)?; + Ok(Timestamp { time, increment: 0 }) + } +} diff --git a/src/tests/serde.rs b/src/tests/serde.rs index de7bb364..10e5e7ee 100644 --- a/src/tests/serde.rs +++ b/src/tests/serde.rs @@ -5,20 +5,35 @@ use crate::{ compat::u2f, doc, from_bson, + from_document, oid::ObjectId, + serde_helpers, + serde_helpers::{ + bson_datetime_as_iso_string, + chrono_datetime_as_bson_datetime, + iso_string_as_bson_datetime, + timestamp_as_u32, + u32_as_timestamp, + uuid_as_binary, + }, spec::BinarySubtype, tests::LOCK, to_bson, + to_document, Binary, Bson, + DateTime, Deserializer, Document, Serializer, + Timestamp, }; + use serde::{Deserialize, Serialize}; -use serde_json::json; +use serde_json::{json, Value}; +use uuid::Uuid; -use std::{collections::BTreeMap, convert::TryFrom}; +use std::{collections::BTreeMap, convert::TryFrom, str::FromStr}; #[test] fn test_ser_vec() { @@ -594,3 +609,199 @@ fn test_serialize_deserialize_unsigned_numbers() { let doc_result: Result = serde_json::from_str(&json); assert!(doc_result.is_err()); } + +#[test] +fn test_unsigned_helpers() { + let _guard = LOCK.run_concurrently(); + + #[derive(Serialize)] + struct A { + #[serde(serialize_with = "serde_helpers::serialize_u32_as_i32")] + num_1: u32, + #[serde(serialize_with = "serde_helpers::serialize_u64_as_i32")] + num_2: u64, + } + + let a = A { num_1: 1, num_2: 2 }; + let doc = to_document(&a).unwrap(); + assert!(doc.get_i32("num_1").unwrap() == 1); + assert!(doc.get_i32("num_2").unwrap() == 2); + + let a = A { + num_1: u32::MAX, + num_2: 1, + }; + let doc_result = to_document(&a); + assert!(doc_result.is_err()); + + let a = A { + num_1: 1, + num_2: u64::MAX, + }; + let doc_result = to_document(&a); + assert!(doc_result.is_err()); + + #[derive(Serialize)] + struct B { + #[serde(serialize_with = "serde_helpers::serialize_u32_as_i64")] + num_1: u32, + #[serde(serialize_with = "serde_helpers::serialize_u64_as_i64")] + num_2: u64, + } + + let b = B { + num_1: u32::MAX, + num_2: i64::MAX as u64, + }; + let doc = to_document(&b).unwrap(); + assert!(doc.get_i64("num_1").unwrap() == u32::MAX as i64); + assert!(doc.get_i64("num_2").unwrap() == i64::MAX); + + let b = B { + num_1: 1, + num_2: i64::MAX as u64 + 1, + }; + let doc_result = to_document(&b); + assert!(doc_result.is_err()); +} + +#[test] +fn test_datetime_helpers() { + let _guard = LOCK.run_concurrently(); + + #[derive(Deserialize, Serialize)] + struct A { + #[serde(with = "bson_datetime_as_iso_string")] + pub date: DateTime, + } + + let iso = "1996-12-20 00:39:57 UTC"; + let date = chrono::DateTime::from_str(iso).unwrap(); + let a = A { date: date.into() }; + 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, date.into()); + + #[derive(Deserialize, Serialize)] + struct B { + #[serde(with = "chrono_datetime_as_bson_datetime")] + pub date: chrono::DateTime, + } + + let date = r#" + { + "date": { + "$date": { + "$numberLong": "1591700287095" + } + } + }"#; + let json: Value = serde_json::from_str(&date).unwrap(); + let b: B = serde_json::from_value(json).unwrap(); + let expected: chrono::DateTime = + chrono::DateTime::from_str("2020-06-09 10:58:07.095 UTC").unwrap(); + assert_eq!(b.date, expected); + let doc = to_document(&b).unwrap(); + assert_eq!(doc.get_datetime("date").unwrap(), &expected); + let b: B = from_document(doc).unwrap(); + assert_eq!(b.date, expected); + + #[derive(Deserialize, Serialize)] + struct C { + #[serde(with = "iso_string_as_bson_datetime")] + pub date: String, + } + + let date = "2020-06-09 10:58:07.095 UTC"; + let c = C { + date: date.to_string(), + }; + let doc = to_document(&c).unwrap(); + assert!(doc.get_datetime("date").is_ok()); + let c: C = from_document(doc).unwrap(); + assert_eq!(c.date.as_str(), date); +} + +#[test] +fn test_oid_helpers() { + let _guard = LOCK.run_concurrently(); + + #[derive(Serialize)] + struct A { + #[serde(serialize_with = "serde_helpers::serialize_hex_string_as_object_id")] + oid: String, + } + + let oid = ObjectId::new(); + let a = A { + oid: oid.to_string(), + }; + let doc = to_document(&a).unwrap(); + assert_eq!(doc.get_object_id("oid").unwrap(), &oid); +} + +#[test] +fn test_uuid_helpers() { + let _guard = LOCK.run_concurrently(); + + #[derive(Serialize, Deserialize)] + struct A { + #[serde(with = "uuid_as_binary")] + uuid: Uuid, + } + + let uuid = Uuid::parse_str("936DA01F9ABD4d9d80C702AF85C822A8").unwrap(); + let a = A { uuid }; + let doc = to_document(&a).unwrap(); + match doc.get("uuid").unwrap() { + Bson::Binary(bin) => { + assert_eq!(bin.subtype, BinarySubtype::Uuid); + assert_eq!(bin.bytes, uuid.as_bytes()); + } + _ => panic!("expected Bson::Binary"), + } + let a: A = from_document(doc).unwrap(); + assert_eq!(a.uuid, uuid); +} + +#[test] +fn test_timestamp_helpers() { + let _guard = LOCK.run_concurrently(); + + #[derive(Deserialize, Serialize)] + struct A { + #[serde(with = "u32_as_timestamp")] + pub time: u32, + } + + let time = 12345; + let a = A { time }; + let doc = to_document(&a).unwrap(); + let timestamp = doc.get_timestamp("time").unwrap(); + assert_eq!(timestamp.time, time); + assert_eq!(timestamp.increment, 0); + let a: A = from_document(doc).unwrap(); + assert_eq!(a.time, time); + + #[derive(Deserialize, Serialize)] + struct B { + #[serde(with = "timestamp_as_u32")] + pub timestamp: Timestamp, + } + + let time = 12345; + let timestamp = Timestamp { time, increment: 0 }; + let b = B { timestamp }; + let val = serde_json::to_value(b).unwrap(); + assert_eq!(val["timestamp"], time); + let b: B = serde_json::from_value(val).unwrap(); + assert_eq!(b.timestamp, timestamp); + + let timestamp = Timestamp { + time: 12334, + increment: 1, + }; + let b = B { timestamp }; + assert!(serde_json::to_value(b).is_err()); +}