diff --git a/README.md b/README.md index f2dfb0b77..edeccd743 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,12 @@ enum A { } ``` +## Advanced examples + +Some of the less trivial examples are present in [examples](./borsh/examples) folder: + +- [implementing `BorshSerialize`/`BorshDeserialize` for third-party `serde_json::Value`](./borsh/examples/serde_json_value.rs) + ## Testing Integration tests should generally be preferred to unit ones. Root module of integration tests of `borsh` crate is [linked](./borsh/tests/tests.rs) here. diff --git a/borsh-derive/src/lib.rs b/borsh-derive/src/lib.rs index 4ecf8dbfd..1cebd6f4a 100644 --- a/borsh-derive/src/lib.rs +++ b/borsh-derive/src/lib.rs @@ -282,11 +282,15 @@ struct B { } ``` +###### usage (comprehensive example) + +[borsh/examples/serde_json_value.rs](https://github.com/near/borsh-rs/blob/master/borsh/examples/serde_json_value.rs) is +a more complex example of how the attribute may be used. + ###### interaction with `#[borsh(skip)]` `#[borsh(serialize_with = ...)]` is not allowed to be used simultaneously with `#[borsh(skip)]`. - */ #[proc_macro_derive(BorshSerialize, attributes(borsh))] pub fn borsh_serialize(input: TokenStream) -> TokenStream { @@ -622,6 +626,11 @@ struct B { } ``` +###### usage (comprehensive example) + +[borsh/examples/serde_json_value.rs](https://github.com/near/borsh-rs/blob/master/borsh/examples/serde_json_value.rs) is +a more complex example of how the attribute may be used. + ###### interaction with `#[borsh(skip)]` `#[borsh(deserialize_with = ...)]` is not allowed to be used simultaneously with `#[borsh(skip)]`. diff --git a/borsh/Cargo.toml b/borsh/Cargo.toml index e0a9465fe..0480c85c1 100644 --- a/borsh/Cargo.toml +++ b/borsh/Cargo.toml @@ -18,6 +18,10 @@ exclude = ["*.snap"] name = "borsh" path = "src/lib.rs" +[[example]] +name = "serde_json_value" +required-features = ["std", "derive"] + [[bin]] name = "generate_schema_schema" path = "src/generate_schema_schema.rs" @@ -39,6 +43,7 @@ bson = { version = "2", optional = true } [dev-dependencies] insta = "1.29.0" +serde_json = { version = "1" } [package.metadata.docs.rs] features = ["derive", "unstable__schema", "rc"] diff --git a/borsh/examples/serde_json_value.rs b/borsh/examples/serde_json_value.rs new file mode 100644 index 000000000..21cadf758 --- /dev/null +++ b/borsh/examples/serde_json_value.rs @@ -0,0 +1,333 @@ +use std::collections::HashMap; + +use borsh::{BorshDeserialize, BorshSerialize}; + +mod serde_json_value { + pub use de::deserialize_value; + pub use ser::serialize_value; + mod ser { + use borsh::{ + io::{ErrorKind, Result, Write}, + BorshSerialize, + }; + use core::convert::TryFrom; + + /// this is mutually recursive with `serialize_array` and `serialize_map` + pub fn serialize_value(value: &serde_json::Value, writer: &mut W) -> Result<()> { + match value { + serde_json::Value::Null => 0_u8.serialize(writer), + serde_json::Value::Bool(b) => { + 1_u8.serialize(writer)?; + b.serialize(writer) + } + serde_json::Value::Number(n) => { + 2_u8.serialize(writer)?; + serialize_number(n, writer) + } + serde_json::Value::String(s) => { + 3_u8.serialize(writer)?; + s.serialize(writer) + } + serde_json::Value::Array(a) => { + 4_u8.serialize(writer)?; + serialize_array(a, writer) + } + serde_json::Value::Object(o) => { + 5_u8.serialize(writer)?; + serialize_map(o, writer) + } + } + } + + fn serialize_number(number: &serde_json::Number, 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) = number.as_u64() { + 0_u8.serialize(writer)?; + return u.serialize(writer); + } + + if let Some(i) = number.as_i64() { + 1_u8.serialize(writer)?; + return i.serialize(writer); + } + + if let Some(f) = number.as_f64() { + 2_u8.serialize(writer)?; + return f.serialize(writer); + } + + // technically, it should not be unreachable, but an error instead, + // as assumption about unreachable depends on private implementation detail + // but it's fine to leave it be unreachable! for an example + unreachable!("number is neither a u64, i64, nor f64"); + } + + /// this is mutually recursive with `serialize_value` + fn serialize_array(array: &Vec, writer: &mut W) -> Result<()> { + // The implementation here is very similar to that of Vec. + writer.write_all( + &(u32::try_from(array.len()).map_err(|_| ErrorKind::InvalidData)?).to_le_bytes(), + )?; + for item in array { + serialize_value(&item, writer)?; + } + Ok(()) + } + + /// this is mutually recursive with `serialize_value` + fn serialize_map( + map: &serde_json::Map, + writer: &mut W, + ) -> Result<()> { + // The implementation here is very similar to that of BTreeMap. + u32::try_from(map.len()) + .map_err(|_| ErrorKind::InvalidData)? + .serialize(writer)?; + + for (key, value) in map { + key.serialize(writer)?; + serialize_value(&value, writer)?; + } + + Ok(()) + } + } + mod de { + use borsh::{ + io::{Error, ErrorKind, Read, Result}, + BorshDeserialize, + }; + + /// this is copy-paste of https://github.com/near/borsh-rs/blob/master/borsh/src/de/hint.rs#L2-L5 + fn hint_cautious(hint: u32) -> usize { + let el_size = core::mem::size_of::() as u32; + core::cmp::max(core::cmp::min(hint, 4096 / el_size), 1) as usize + } + + /// this is mutually recursive with `deserialize_array`, `deserialize_map` + pub fn deserialize_value(reader: &mut R) -> Result { + let flag: u8 = BorshDeserialize::deserialize_reader(reader)?; + match flag { + 0 => Ok(serde_json::Value::Null), + 1 => { + let b: bool = BorshDeserialize::deserialize_reader(reader)?; + Ok(serde_json::Value::Bool(b)) + } + 2 => { + let n: serde_json::Number = deserialize_number(reader)?; + Ok(serde_json::Value::Number(n)) + } + 3 => { + let s: String = BorshDeserialize::deserialize_reader(reader)?; + Ok(serde_json::Value::String(s)) + } + 4 => { + let a: Vec = deserialize_array(reader)?; + Ok(serde_json::Value::Array(a)) + } + 5 => { + let o: serde_json::Map<_, _> = deserialize_map(reader)?; + Ok(serde_json::Value::Object(o)) + } + _ => { + let msg = format!( + "Invalid JSON value representation: {}. The first byte must be 0-5", + flag + ); + + Err(Error::new(ErrorKind::InvalidData, msg)) + } + } + } + + fn deserialize_number(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. + serde_json::Number::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)) + } + } + } + + /// this is mutually recursive with `deserialize_value` + fn deserialize_array(reader: &mut R) -> Result> { + // The implementation here is very similar to that of Vec. + let len = u32::deserialize_reader(reader)?; + let mut result = Vec::with_capacity(hint_cautious::<(String, serde_json::Value)>(len)); + for _ in 0..len { + let value = deserialize_value(reader)?; + result.push(value); + } + Ok(result) + } + + /// this is mutually recursive with `deserialize_value` + fn deserialize_map( + reader: &mut R, + ) -> Result> { + // The implementation here is very similar to that of BTreeMap. + + let vec: Vec<(String, serde_json::Value)> = { + // The implementation here is very similar to that of Vec<(K, V)>. + let len = u32::deserialize_reader(reader)?; + let mut result = + Vec::with_capacity(hint_cautious::<(String, serde_json::Value)>(len)); + for _ in 0..len { + let pair = { + let key = String::deserialize_reader(reader)?; + let value = deserialize_value(reader)?; + (key, value) + }; + result.push(pair); + } + result + }; + + Ok(vec.into_iter().collect()) + } + } +} + +mod borsh_wrapper { + use borsh::{BorshDeserialize, BorshSerialize}; + + #[derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] + pub struct SerdeJsonBorshWrapper( + #[borsh( + serialize_with = "super::serde_json_value::serialize_value", + deserialize_with = "super::serde_json_value::deserialize_value" + )] + pub serde_json::Value, + ); + + impl From for SerdeJsonBorshWrapper { + fn from(value: serde_json::Value) -> Self { + Self(value) + } + } + + impl From for serde_json::Value { + fn from(value: SerdeJsonBorshWrapper) -> Self { + value.0 + } + } +} + +use borsh_wrapper::SerdeJsonBorshWrapper; + +#[derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +struct SerdeJsonAsField { + pub examples: HashMap, +} + +fn main() { + // original code is from https://github.com/near/borsh-rs/pull/312 + let original = serde_json::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 mut examples = HashMap::new(); + examples.insert("Larry Jake Pumpkin".into(), original.clone().into()); + + let complex_struct = SerdeJsonAsField { examples }; + let serialized = borsh::to_vec(&complex_struct).unwrap(); + + let mut deserialized: SerdeJsonAsField = borsh::from_slice(&serialized).unwrap(); + + assert_eq!(complex_struct, deserialized); + + let deserialized_value: serde_json::Value = deserialized + .examples + .remove("Larry Jake Pumpkin") + .expect("key present") + .into(); + + assert_eq!(original, deserialized_value); + + let number = deserialized_value + .get("array_of_numbers") + .expect("has key") + .as_array() + .expect("is array") + .get(5) + .expect("has index") + .as_i64() + .expect("is i64"); + + assert_eq!(number, 34798324); +}