diff --git a/.github/test.sh b/.github/test.sh index 2fdbf58b9..44610393d 100755 --- a/.github/test.sh +++ b/.github/test.sh @@ -28,6 +28,9 @@ cargo test --features de_strict_order 'roundtrip::test_hash_map' cargo test --features de_strict_order 'roundtrip::test_btree_map' ########## features = ["bson"] group cargo test --features bson,derive 'roundtrip::requires_derive_category::test_bson_object_ids' +########## features = ["serde_json_value"] group +cargo test --features serde_json_value 'roundtrip::test_serde_json_value' +cargo test --features serde_json_value,unstable__schema 'schema::test_serde_json_value' ########## features = ["bytes"] group cargo test --features bytes,derive 'roundtrip::requires_derive_category::test_ultimate_many_features_combined' @@ -44,6 +47,9 @@ cargo test --no-default-features --features ascii,unstable__schema 'schema::test ########## features = ["rc"] group cargo test --no-default-features --features rc 'roundtrip::test_rc' cargo test --no-default-features --features rc,unstable__schema 'schema::test_rc' +########## features = ["serde_json_value"] group +cargo test --features serde_json_value 'roundtrip::test_serde_json_value' +cargo test --features serde_json_value,unstable__schema 'schema::test_serde_json_value' ########## features = ["hashbrown"] group cargo test --no-default-features --features hashbrown cargo test --no-default-features --features hashbrown,derive diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 53d65d66d..2af0f9b7d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -82,7 +82,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: run cargo doc - run: RUSTDOCFLAGS="-D warnings" cargo doc --features derive,unstable__schema + run: RUSTDOCFLAGS="-D warnings" cargo doc --features derive,unstable__schema,rc,bytes,bson,ascii,serde_json_value release-plz: runs-on: ubuntu-latest diff --git a/borsh-derive/src/internals/generics.rs b/borsh-derive/src/internals/generics.rs index 72080f206..7914b3b00 100644 --- a/borsh-derive/src/internals/generics.rs +++ b/borsh-derive/src/internals/generics.rs @@ -259,7 +259,9 @@ impl FindTyParams { )] match bound { TypeParamBound::Trait(bound) => self.visit_path(&bound.path), - TypeParamBound::Lifetime(_) | TypeParamBound::Verbatim(_) => {} + TypeParamBound::Lifetime(_) + | TypeParamBound::Verbatim(_) + | TypeParamBound::PreciseCapture(_) => {} _ => {} } } diff --git a/borsh/Cargo.toml b/borsh/Cargo.toml index e0a9465fe..9c13c6afd 100644 --- a/borsh/Cargo.toml +++ b/borsh/Cargo.toml @@ -36,12 +36,13 @@ borsh-derive = { path = "../borsh-derive", version = "~1.5.1", optional = true } hashbrown = { version = ">=0.11,<0.15.0", optional = true } bytes = { version = "1", optional = true } bson = { version = "2", optional = true } +serde_json = { version = "1", optional = true } [dev-dependencies] insta = "1.29.0" [package.metadata.docs.rs] -features = ["derive", "unstable__schema", "rc"] +features = ["derive", "unstable__schema", "rc", "bytes", "bson", "ascii", "serde_json_value"] targets = ["x86_64-unknown-linux-gnu"] [features] @@ -54,3 +55,5 @@ std = [] # Be sure that this is what you want before enabling this feature. rc = [] de_strict_order = [] +# Implements BorshSerialize,BorshDeserialize and BorshSchema for serde_json::Value. +serde_json_value = ["dep:serde_json"] diff --git a/borsh/src/de/mod.rs b/borsh/src/de/mod.rs index 0abb36e3e..d61f0068e 100644 --- a/borsh/src/de/mod.rs +++ b/borsh/src/de/mod.rs @@ -453,6 +453,91 @@ impl BorshDeserialize for bson::oid::ObjectId { } } +#[cfg(feature = "serde_json_value")] +impl BorshDeserialize for serde_json::Value { + #[inline] + fn deserialize_reader(reader: &mut R) -> Result { + let flag: u8 = BorshDeserialize::deserialize_reader(reader)?; + match flag { + 0 => Ok(Self::Null), + 1 => { + let b: bool = BorshDeserialize::deserialize_reader(reader)?; + Ok(Self::Bool(b)) + } + 2 => { + let n: serde_json::Number = BorshDeserialize::deserialize_reader(reader)?; + Ok(Self::Number(n)) + } + 3 => { + let s: String = BorshDeserialize::deserialize_reader(reader)?; + Ok(Self::String(s)) + } + 4 => { + let a: Vec = BorshDeserialize::deserialize_reader(reader)?; + Ok(Self::Array(a)) + } + 5 => { + let o: serde_json::Map<_, _> = BorshDeserialize::deserialize_reader(reader)?; + Ok(Self::Object(o)) + } + _ => { + let msg = format!( + "Invalid JSON value representation: {}. The first byte must be 0-5", + flag + ); + + Err(Error::new(ErrorKind::InvalidData, msg)) + } + } + } +} + +#[cfg(feature = "serde_json_value")] +impl BorshDeserialize for serde_json::Number { + #[inline] + fn deserialize_reader(reader: &mut R) -> Result { + let flag: u8 = BorshDeserialize::deserialize_reader(reader)?; + match flag { + 0 => { + let u: u64 = BorshDeserialize::deserialize_reader(reader)?; + Ok(u.into()) + } + 1 => { + let i: i64 = BorshDeserialize::deserialize_reader(reader)?; + Ok(i.into()) + } + 2 => { + let f: f64 = BorshDeserialize::deserialize_reader(reader)?; + // This returns None if the number is a NaN or +/-Infinity, + // which are not valid JSON numbers. + Self::from_f64(f).ok_or_else(|| { + let msg = format!("Invalid JSON number: {}", f); + + Error::new(ErrorKind::InvalidData, msg) + }) + } + _ => { + let msg = format!( + "Invalid JSON number representation: {}. The first byte must be 0-2", + flag + ); + + Err(Error::new(ErrorKind::InvalidData, msg)) + } + } + } +} + +#[cfg(feature = "serde_json_value")] +impl BorshDeserialize for serde_json::Map { + #[inline] + fn deserialize_reader(reader: &mut R) -> Result { + // The implementation here is identical to that of BTreeMap. + let vec = >::deserialize_reader(reader)?; + Ok(vec.into_iter().collect()) + } +} + impl BorshDeserialize for Cow<'_, T> where T: ToOwned + ?Sized, diff --git a/borsh/src/lib.rs b/borsh/src/lib.rs index eafad58a4..964e21025 100644 --- a/borsh/src/lib.rs +++ b/borsh/src/lib.rs @@ -58,6 +58,9 @@ and [Ord] for btree ones. Deserialization emits error otherwise. If this feature is not enabled, it is possible that two different byte slices could deserialize into the same `HashMap`/`HashSet` object. +* **serde_json_value** - + Gates implementation of [BorshSerialize], [BorshDeserialize], [BorshSchema] for + [serde_json::Value] and [serde_json] types it depends on. ### Config aliases diff --git a/borsh/src/ser/mod.rs b/borsh/src/ser/mod.rs index b7da5ebd7..d9860ccc8 100644 --- a/borsh/src/ser/mod.rs +++ b/borsh/src/ser/mod.rs @@ -317,6 +317,93 @@ impl BorshSerialize for bson::oid::ObjectId { } } +#[cfg(feature = "serde_json_value")] +impl BorshSerialize for serde_json::Value { + #[inline] + fn serialize(&self, writer: &mut W) -> Result<()> { + match self { + Self::Null => 0_u8.serialize(writer), + Self::Bool(b) => { + 1_u8.serialize(writer)?; + b.serialize(writer) + } + Self::Number(n) => { + 2_u8.serialize(writer)?; + n.serialize(writer) + } + Self::String(s) => { + 3_u8.serialize(writer)?; + s.serialize(writer) + } + Self::Array(a) => { + 4_u8.serialize(writer)?; + a.serialize(writer) + } + Self::Object(o) => { + 5_u8.serialize(writer)?; + o.serialize(writer) + } + } + } +} + +#[cfg(feature = "serde_json_value")] +impl BorshSerialize for serde_json::Number { + #[inline] + fn serialize(&self, writer: &mut W) -> Result<()> { + // A JSON number can either be a non-negative integer (represented in + // serde_json by a u64), a negative integer (by an i64), or a non-integer + // (by an f64). + // We identify these cases with the following single-byte discriminants: + // 0 - u64 + // 1 - i64 + // 2 - f64 + if let Some(u) = self.as_u64() { + 0_u8.serialize(writer)?; + return u.serialize(writer); + } + + if let Some(i) = self.as_i64() { + 1_u8.serialize(writer)?; + return i.serialize(writer); + } + + if let Some(f) = self.as_f64() { + 2_u8.serialize(writer)?; + return f.serialize(writer); + } + + unreachable!("number is neither a u64, i64, nor f64"); + } +} + +#[cfg(feature = "serde_json_value")] +/// Module is available if borsh is built with `features = ["serde_json_value"]`. +/// +/// Module defines [BorshSerialize] implementation for [serde_json::Map], +pub mod serde_json_value { + use super::BorshSerialize; + use crate::io::{ErrorKind, Result, Write}; + use core::convert::TryFrom; + + impl BorshSerialize for serde_json::Map { + #[inline] + fn serialize(&self, writer: &mut W) -> Result<()> { + // The implementation here is identical to that of BTreeMap. + u32::try_from(self.len()) + .map_err(|_| ErrorKind::InvalidData)? + .serialize(writer)?; + + for (key, value) in self { + key.serialize(writer)?; + value.serialize(writer)?; + } + + Ok(()) + } + } +} + impl BorshSerialize for VecDeque where T: BorshSerialize, diff --git a/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_serde_json_value__json_value.snap b/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_serde_json_value__json_value.snap new file mode 100644 index 000000000..e98a2f85e --- /dev/null +++ b/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_serde_json_value__json_value.snap @@ -0,0 +1,833 @@ +--- +source: borsh/tests/roundtrip/test_serde_json_value.rs +expression: serialized +--- +[ + 5, + 17, + 0, + 0, + 0, + 15, + 0, + 0, + 0, + 97, + 114, + 114, + 97, + 121, + 95, + 111, + 102, + 95, + 97, + 114, + 114, + 97, + 121, + 115, + 4, + 3, + 0, + 0, + 0, + 4, + 3, + 0, + 0, + 0, + 2, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 3, + 0, + 0, + 0, + 2, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 3, + 0, + 0, + 0, + 2, + 0, + 7, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 8, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 14, + 0, + 0, + 0, + 97, + 114, + 114, + 97, + 121, + 95, + 111, + 102, + 95, + 110, + 117, + 108, + 108, + 115, + 4, + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 16, + 0, + 0, + 0, + 97, + 114, + 114, + 97, + 121, + 95, + 111, + 102, + 95, + 110, + 117, + 109, + 98, + 101, + 114, + 115, + 4, + 6, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 1, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 2, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 2, + 154, + 153, + 153, + 153, + 153, + 153, + 241, + 63, + 2, + 2, + 154, + 153, + 153, + 153, + 153, + 153, + 241, + 191, + 2, + 0, + 244, + 250, + 18, + 2, + 0, + 0, + 0, + 0, + 16, + 0, + 0, + 0, + 97, + 114, + 114, + 97, + 121, + 95, + 111, + 102, + 95, + 111, + 98, + 106, + 101, + 99, + 116, + 115, + 4, + 3, + 0, + 0, + 0, + 5, + 2, + 0, + 0, + 0, + 3, + 0, + 0, + 0, + 97, + 103, + 101, + 2, + 0, + 30, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 110, + 97, + 109, + 101, + 3, + 5, + 0, + 0, + 0, + 76, + 97, + 114, + 114, + 121, + 5, + 2, + 0, + 0, + 0, + 3, + 0, + 0, + 0, + 97, + 103, + 101, + 2, + 0, + 7, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 110, + 97, + 109, + 101, + 3, + 4, + 0, + 0, + 0, + 74, + 97, + 107, + 101, + 5, + 2, + 0, + 0, + 0, + 3, + 0, + 0, + 0, + 97, + 103, + 101, + 2, + 0, + 8, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 110, + 97, + 109, + 101, + 3, + 7, + 0, + 0, + 0, + 80, + 117, + 109, + 112, + 107, + 105, + 110, + 16, + 0, + 0, + 0, + 97, + 114, + 114, + 97, + 121, + 95, + 111, + 102, + 95, + 115, + 116, + 114, + 105, + 110, + 103, + 115, + 4, + 3, + 0, + 0, + 0, + 3, + 5, + 0, + 0, + 0, + 76, + 97, + 114, + 114, + 121, + 3, + 4, + 0, + 0, + 0, + 74, + 97, + 107, + 101, + 3, + 7, + 0, + 0, + 0, + 80, + 117, + 109, + 112, + 107, + 105, + 110, + 5, + 0, + 0, + 0, + 102, + 97, + 108, + 115, + 101, + 1, + 0, + 14, + 0, + 0, + 0, + 110, + 101, + 103, + 97, + 116, + 105, + 118, + 101, + 95, + 102, + 108, + 111, + 97, + 116, + 2, + 2, + 215, + 163, + 112, + 61, + 10, + 199, + 139, + 192, + 16, + 0, + 0, + 0, + 110, + 101, + 103, + 97, + 116, + 105, + 118, + 101, + 95, + 105, + 110, + 116, + 101, + 103, + 101, + 114, + 2, + 1, + 200, + 164, + 254, + 255, + 255, + 255, + 255, + 255, + 12, + 0, + 0, + 0, + 110, + 101, + 103, + 97, + 116, + 105, + 118, + 101, + 95, + 109, + 97, + 120, + 2, + 2, + 255, + 255, + 255, + 255, + 255, + 255, + 239, + 255, + 4, + 0, + 0, + 0, + 110, + 117, + 108, + 108, + 0, + 6, + 0, + 0, + 0, + 111, + 98, + 106, + 101, + 99, + 116, + 5, + 3, + 0, + 0, + 0, + 3, + 0, + 0, + 0, + 97, + 103, + 101, + 2, + 0, + 30, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 110, + 97, + 109, + 101, + 3, + 5, + 0, + 0, + 0, + 76, + 97, + 114, + 114, + 121, + 4, + 0, + 0, + 0, + 112, + 101, + 116, + 115, + 4, + 2, + 0, + 0, + 0, + 5, + 2, + 0, + 0, + 0, + 3, + 0, + 0, + 0, + 97, + 103, + 101, + 2, + 0, + 7, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 110, + 97, + 109, + 101, + 3, + 4, + 0, + 0, + 0, + 74, + 97, + 107, + 101, + 5, + 2, + 0, + 0, + 0, + 3, + 0, + 0, + 0, + 97, + 103, + 101, + 2, + 0, + 8, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 110, + 97, + 109, + 101, + 3, + 7, + 0, + 0, + 0, + 80, + 117, + 109, + 112, + 107, + 105, + 110, + 14, + 0, + 0, + 0, + 112, + 111, + 115, + 105, + 116, + 105, + 118, + 101, + 95, + 102, + 108, + 111, + 97, + 116, + 2, + 2, + 205, + 204, + 204, + 204, + 204, + 220, + 94, + 64, + 16, + 0, + 0, + 0, + 112, + 111, + 115, + 105, + 116, + 105, + 118, + 101, + 95, + 105, + 110, + 116, + 101, + 103, + 101, + 114, + 2, + 0, + 57, + 48, + 0, + 0, + 0, + 0, + 0, + 0, + 12, + 0, + 0, + 0, + 112, + 111, + 115, + 105, + 116, + 105, + 118, + 101, + 95, + 109, + 97, + 120, + 2, + 2, + 255, + 255, + 255, + 255, + 255, + 255, + 239, + 127, + 6, + 0, + 0, + 0, + 115, + 116, + 114, + 105, + 110, + 103, + 3, + 5, + 0, + 0, + 0, + 76, + 97, + 114, + 114, + 121, + 4, + 0, + 0, + 0, + 116, + 114, + 117, + 101, + 1, + 1, + 4, + 0, + 0, + 0, + 122, + 101, + 114, + 111, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, +] diff --git a/borsh/tests/roundtrip/test_serde_json_value.rs b/borsh/tests/roundtrip/test_serde_json_value.rs new file mode 100644 index 000000000..337d455a6 --- /dev/null +++ b/borsh/tests/roundtrip/test_serde_json_value.rs @@ -0,0 +1,63 @@ +use serde_json::json; + +#[test] +fn test_json_value() { + let original = json!({ + "null": null, + "true": true, + "false": false, + "zero": 0, + "positive_integer": 12345, + "negative_integer": -88888, + "positive_float": 123.45, + "negative_float": -888.88, + "positive_max": 1.7976931348623157e+308, + "negative_max": -1.7976931348623157e+308, + "string": "Larry", + "array_of_nulls": [null, null, null], + "array_of_numbers": [0, -1, 1, 1.1, -1.1, 34798324], + "array_of_strings": ["Larry", "Jake", "Pumpkin"], + "array_of_arrays": [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9] + ], + "array_of_objects": [ + { + "name": "Larry", + "age": 30 + }, + { + "name": "Jake", + "age": 7 + }, + { + "name": "Pumpkin", + "age": 8 + } + ], + "object": { + "name": "Larry", + "age": 30, + "pets": [ + { + "name": "Jake", + "age": 7 + }, + { + "name": "Pumpkin", + "age": 8 + } + ] + } + }); + + let serialized = borsh::to_vec(&original).unwrap(); + + #[cfg(feature = "std")] + insta::assert_debug_snapshot!(serialized); + + let deserialized: serde_json::Value = borsh::from_slice(&serialized).unwrap(); + + assert_eq!(original, deserialized); +} diff --git a/borsh/tests/schema/test_serde_json_value.rs b/borsh/tests/schema/test_serde_json_value.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/borsh/tests/schema/test_serde_json_value.rs @@ -0,0 +1 @@ + diff --git a/borsh/tests/tests.rs b/borsh/tests/tests.rs index ea8cad5d4..6ee0b4783 100644 --- a/borsh/tests/tests.rs +++ b/borsh/tests/tests.rs @@ -52,6 +52,8 @@ mod roundtrip { mod test_cells; #[cfg(feature = "rc")] mod test_rc; + #[cfg(feature = "serde_json_value")] + mod test_serde_json_value; #[cfg(feature = "derive")] mod requires_derive_category { @@ -95,6 +97,9 @@ mod schema { mod test_cells; #[cfg(feature = "rc")] mod test_rc; + #[cfg(feature = "serde_json_value")] + // TODO: add content for schema tests + mod test_serde_json_value; mod test_simple_structs; mod test_generic_structs; mod test_simple_enums;