desert_core 0.2.0

Binary serialization library for Rust (core crate)
Documentation
use crate::{
    BinaryDeserializer, BinaryInput, BinaryOutput, BinarySerializer, DeserializationContext, Error,
    Result, SerializationContext,
};
use serde_json::{Map, Number, Value};

const JSON_NULL: u8 = 0;
const JSON_FALSE: u8 = 1;
const JSON_TRUE: u8 = 2;
const JSON_I64: u8 = 3;
const JSON_U64: u8 = 4;
const JSON_F64: u8 = 5;
const JSON_DECIMAL_STRING: u8 = 6;
const JSON_STRING: u8 = 7;
const JSON_ARRAY: u8 = 8;
const JSON_OBJECT: u8 = 9;

impl BinarySerializer for Value {
    fn serialize<Output: BinaryOutput>(
        &self,
        context: &mut SerializationContext<Output>,
    ) -> Result<()> {
        if context.options().json_binary {
            serialize_binary_json_value(self, context)
        } else {
            let bytes = serde_json::to_vec(self)
                .map_err(|err| Error::SerializationFailure(err.to_string()))?;
            bytes.serialize(context)
        }
    }
}

impl BinaryDeserializer for Value {
    fn deserialize(context: &mut DeserializationContext<'_>) -> Result<Self> {
        if context.options().json_binary {
            deserialize_binary_json_value(context)
        } else {
            let length = context.read_var_u32()?;
            let bytes = context.read_bytes(length as usize)?;
            let value: Value = serde_json::from_slice(bytes)
                .map_err(|err| Error::DeserializationFailure(err.to_string()))?;
            Ok(value)
        }
    }
}

fn serialize_binary_json_value<Output: BinaryOutput>(
    value: &Value,
    context: &mut SerializationContext<Output>,
) -> Result<()> {
    match value {
        Value::Null => {
            context.write_u8(JSON_NULL);
            Ok(())
        }
        Value::Bool(false) => {
            context.write_u8(JSON_FALSE);
            Ok(())
        }
        Value::Bool(true) => {
            context.write_u8(JSON_TRUE);
            Ok(())
        }
        Value::Number(number) => serialize_binary_json_number(number, context),
        Value::String(string) => {
            context.write_u8(JSON_STRING);
            string.serialize(context)
        }
        Value::Array(values) => {
            context.write_u8(JSON_ARRAY);
            context.write_var_i32(values.len().try_into()?);
            for value in values {
                serialize_binary_json_value(value, context)?;
            }
            Ok(())
        }
        Value::Object(entries) => {
            context.write_u8(JSON_OBJECT);
            context.write_var_i32(entries.len().try_into()?);
            for (key, value) in entries {
                key.serialize(context)?;
                serialize_binary_json_value(value, context)?;
            }
            Ok(())
        }
    }
}

fn serialize_binary_json_number<Output: BinaryOutput>(
    number: &Number,
    context: &mut SerializationContext<Output>,
) -> Result<()> {
    if let Some(value) = number.as_i64() {
        context.write_u8(JSON_I64);
        value.serialize(context)
    } else if let Some(value) = number.as_u64() {
        context.write_u8(JSON_U64);
        value.serialize(context)
    } else if let Some(value) = number.as_f64() {
        context.write_u8(JSON_F64);
        value.serialize(context)
    } else {
        context.write_u8(JSON_DECIMAL_STRING);
        number.to_string().serialize(context)
    }
}

fn deserialize_binary_json_value(context: &mut DeserializationContext<'_>) -> Result<Value> {
    match context.read_u8()? {
        JSON_NULL => Ok(Value::Null),
        JSON_FALSE => Ok(Value::Bool(false)),
        JSON_TRUE => Ok(Value::Bool(true)),
        JSON_I64 => Ok(Value::Number(Number::from(i64::deserialize(context)?))),
        JSON_U64 => Ok(Value::Number(Number::from(u64::deserialize(context)?))),
        JSON_F64 => {
            let value = f64::deserialize(context)?;
            let number = Number::from_f64(value).ok_or_else(|| {
                Error::DeserializationFailure(format!(
                    "Failed to deserialize JSON number from f64: {value}"
                ))
            })?;
            Ok(Value::Number(number))
        }
        JSON_DECIMAL_STRING => {
            let value = String::deserialize(context)?;
            let number = value.parse::<Number>().map_err(|err| {
                Error::DeserializationFailure(format!(
                    "Failed to deserialize JSON decimal number: {err}"
                ))
            })?;
            Ok(Value::Number(number))
        }
        JSON_STRING => Ok(Value::String(String::deserialize(context)?)),
        JSON_ARRAY => {
            let length = read_json_collection_len(context)?;
            let mut values = Vec::with_capacity(length);
            for _ in 0..length {
                values.push(deserialize_binary_json_value(context)?);
            }
            Ok(Value::Array(values))
        }
        JSON_OBJECT => {
            let length = read_json_collection_len(context)?;
            let mut entries = Map::new();
            for _ in 0..length {
                let key = String::deserialize(context)?;
                let value = deserialize_binary_json_value(context)?;
                entries.insert(key, value);
            }
            Ok(Value::Object(entries))
        }
        tag => Err(Error::DeserializationFailure(format!(
            "Failed to deserialize JSON value: invalid tag {tag}"
        ))),
    }
}

fn read_json_collection_len(context: &mut DeserializationContext<'_>) -> Result<usize> {
    let length = context.read_var_i32()?;
    if length < 0 {
        Err(Error::DeserializationFailure(
            "Failed to deserialize JSON collection: negative length".to_string(),
        ))
    } else {
        Ok(length as usize)
    }
}

#[cfg(test)]
mod tests {
    use crate::tests::roundtrip_with_options;
    use crate::{
        deserialize_with_options, serialize_to_byte_vec, serialize_to_byte_vec_with_options,
        Options,
    };
    use proptest::collection::{btree_map, vec};
    use proptest::prelude::*;
    use proptest::string::string_regex;
    use serde_json::{json, Value};
    use test_r::test;

    fn json_string_strategy() -> impl Strategy<Value = String> {
        string_regex("[ -~]{0,32}").unwrap()
    }

    fn json_number_strategy() -> impl Strategy<Value = serde_json::Number> {
        prop_oneof![
            any::<i64>().prop_map(serde_json::Number::from),
            any::<u64>().prop_map(serde_json::Number::from),
            (-1.0e12f64..1.0e12f64)
                .prop_filter_map("finite JSON f64", serde_json::Number::from_f64),
        ]
    }

    fn json_value_strategy() -> impl Strategy<Value = Value> {
        let leaf = prop_oneof![
            Just(Value::Null),
            any::<bool>().prop_map(Value::Bool),
            json_number_strategy().prop_map(Value::Number),
            json_string_strategy().prop_map(Value::String),
        ];

        leaf.prop_recursive(4, 64, 8, |inner| {
            prop_oneof![
                vec(inner.clone(), 0..12).prop_map(Value::Array),
                btree_map(json_string_strategy(), inner, 0..12)
                    .prop_map(|entries| Value::Object(entries.into_iter().collect())),
            ]
        })
    }

    #[test]
    fn roundtrip_example1() {
        let input = json!({
            "hello": "world",
            "other": 10,
            "inner": [{"x": 1, "y": 0.5}, {"x": -1, "y": 100}]
        });
        let serialized = crate::serialize_to_bytes(&input).unwrap();
        let deserialized: Value = crate::deserialize(&serialized).unwrap();
        assert_eq!(input, deserialized);
    }

    #[test]
    fn binary_json_roundtrip() {
        let input = json!({
            "null": null,
            "false": false,
            "true": true,
            "negative": -42,
            "unsigned": 18446744073709551615u64,
            "float": 12.5,
            "text": "hello \"json\"",
            "array": [1, null, "item"],
            "object": {
                "nested": true
            }
        });
        let options = Options::default().with_binary_json();
        let serialized = serialize_to_byte_vec_with_options(&input, options.clone()).unwrap();
        let deserialized: Value = deserialize_with_options(&serialized, options).unwrap();

        assert_eq!(input, deserialized);
    }

    #[test]
    fn default_json_format_is_unchanged() {
        let input = json!({
            "hello": "world",
            "other": 10,
            "inner": [{"x": 1, "y": 0.5}, {"x": -1, "y": 100}]
        });

        assert_eq!(
            serialize_to_byte_vec(&input).unwrap(),
            serialize_to_byte_vec_with_options(&input, Options::default()).unwrap()
        );
    }

    proptest! {
        #[test]
        fn binary_json_roundtrips_generated_values(value in json_value_strategy()) {
            roundtrip_with_options(value, Options::default().with_binary_json());
        }
    }
}