Skip to content

Commit

Permalink
doc: serde_json::Value bridging example/doc (#317)
Browse files Browse the repository at this point in the history
  • Loading branch information
dj8yfo authored Oct 30, 2024
1 parent fa9a0a1 commit 21c024f
Show file tree
Hide file tree
Showing 4 changed files with 354 additions and 1 deletion.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 10 additions & 1 deletion borsh-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,11 +282,15 @@ struct B<K, V> {
}
```
###### 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 {
Expand Down Expand Up @@ -622,6 +626,11 @@ struct B<K: Hash + Eq, V> {
}
```
###### 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)]`.
Expand Down
5 changes: 5 additions & 0 deletions borsh/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"]
Expand Down
333 changes: 333 additions & 0 deletions borsh/examples/serde_json_value.rs
Original file line number Diff line number Diff line change
@@ -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<W: Write>(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<W: Write>(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<W: Write>(array: &Vec<serde_json::Value>, writer: &mut W) -> Result<()> {
// The implementation here is very similar to that of Vec<V>.
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<W: Write>(
map: &serde_json::Map<String, serde_json::Value>,
writer: &mut W,
) -> Result<()> {
// The implementation here is very similar to that of BTreeMap<K, V>.
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<T>(hint: u32) -> usize {
let el_size = core::mem::size_of::<T>() 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<R: Read>(reader: &mut R) -> Result<serde_json::Value> {
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<serde_json::Value> = 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<R: Read>(reader: &mut R) -> Result<serde_json::Number> {
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<R: Read>(reader: &mut R) -> Result<Vec<serde_json::Value>> {
// The implementation here is very similar to that of Vec<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 value = deserialize_value(reader)?;
result.push(value);
}
Ok(result)
}

/// this is mutually recursive with `deserialize_value`
fn deserialize_map<R: Read>(
reader: &mut R,
) -> Result<serde_json::Map<String, serde_json::Value>> {
// The implementation here is very similar to that of BTreeMap<K, V>.

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<serde_json::Value> for SerdeJsonBorshWrapper {
fn from(value: serde_json::Value) -> Self {
Self(value)
}
}

impl From<SerdeJsonBorshWrapper> 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<String, SerdeJsonBorshWrapper>,
}

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);
}

0 comments on commit 21c024f

Please sign in to comment.