musli 0.2.0

Müsli is a flexible and efficient serialization framework.
Documentation
use anyhow::Result;
use musli::alloc::Allocator;
use musli::alloc::Global;
use musli::de::{AsDecoder, Decoder};
use musli::json;
use musli::mode::Text;
use musli::value::{self, Value};
use musli::{Decode, Encode};

#[derive(Decode, Encode)]
struct Struct {
    #[musli(default, skip_encoding_if = Option::is_none)]
    field: Option<u32>,
}

/// We want to assert that an option type can be decode from a plain unknown value.
#[test]
fn option() -> Result<()> {
    let value: Value<Global> = json::from_str(r#"{"field":null}"#)?;
    let st: Struct = value::decode_text(&value)?;
    assert_eq!(st.field, None);

    let value: Value<Global> = json::from_str(r#"{"field":42}"#)?;
    let st: Struct = value::decode_text(&value)?;
    assert_eq!(st.field, Some(42));

    let value: Value<Global> = json::from_str(r#"{}"#)?;
    let st: Struct = value::decode_text(&value)?;
    assert_eq!(st.field, None);
    Ok(())
}

/// Numbers are implicitly converted when decoded out of a value.
#[test]
fn number_coercion() -> Result<()> {
    let value = Value::<Global>::from(42u8);
    assert_eq!(value::decode::<u64>(&value)?, 42u64);
    assert_eq!(value::decode::<i32>(&value)?, 42i32);
    assert_eq!(value::decode::<f32>(&value)?, 42.0f32);

    let value = Value::<Global>::from(-1i64);
    assert_eq!(value::decode::<i8>(&value)?, -1i8);
    assert_eq!(value::decode::<f64>(&value)?, -1.0f64);
    assert!(value::decode::<u32>(&value).is_err());

    let value = Value::<Global>::from(3.5f32);
    assert_eq!(value::decode::<f64>(&value)?, 3.5f64);
    assert_eq!(value::decode::<u32>(&value)?, 3u32);

    let value = Value::<Global>::from(u128::MAX);
    assert_eq!(value::decode::<u128>(&value)?, u128::MAX);
    assert!(value::decode::<i128>(&value).is_err());

    let value = Value::<Global>::from(i128::MIN);
    assert_eq!(value::decode::<i128>(&value)?, i128::MIN);
    Ok(())
}

/// Floats which do not fit in the target integer type are rejected rather than
/// silently saturating.
#[test]
fn float_to_integer_coercion() -> Result<()> {
    // Truncation towards zero is fine as long as the result is in range.
    assert_eq!(value::decode::<i32>(&Value::<Global>::from(-3.5f64))?, -3);
    assert_eq!(value::decode::<u8>(&Value::<Global>::from(255.9f64))?, 255);
    assert_eq!(
        value::decode::<u64>(&Value::<Global>::from(9.007199254740992e15f64))?,
        9007199254740992
    );

    // Out of range in either direction.
    assert!(value::decode::<u8>(&Value::<Global>::from(1e30f64)).is_err());
    assert!(value::decode::<u64>(&Value::<Global>::from(1e30f64)).is_err());
    assert!(value::decode::<u32>(&Value::<Global>::from(-1.0f64)).is_err());
    assert!(value::decode::<i8>(&Value::<Global>::from(128.0f64)).is_err());
    assert!(value::decode::<u64>(&Value::<Global>::from(1.8446744073709552e19f64)).is_err());

    // Non-finite values are never integers.
    assert!(value::decode::<u8>(&Value::<Global>::from(f64::NAN)).is_err());
    assert!(value::decode::<i32>(&Value::<Global>::from(f64::INFINITY)).is_err());
    assert!(value::decode::<i32>(&Value::<Global>::from(f64::NEG_INFINITY)).is_err());
    Ok(())
}

/// A JSON document with numerical map keys must decode the same whether or not
/// it is routed through a [`Value`], since object keys are always strings.
#[test]
fn numeric_map_keys() -> Result<()> {
    use std::collections::BTreeMap;

    for (input, expected) in [
        (r#"{}"#, BTreeMap::new()),
        (r#"{"1":2}"#, BTreeMap::from([(1u32, 2u32)])),
        (
            r#"{"0":1,"4294967295":2}"#,
            BTreeMap::from([(0u32, 1u32), (u32::MAX, 2u32)]),
        ),
    ] {
        assert_eq!(json::from_str::<BTreeMap<u32, u32>>(input)?, expected);

        let value: Value<Global> = json::from_str(input)?;
        assert_eq!(value::decode_text::<BTreeMap<u32, u32>>(&value)?, expected);
    }

    // Signed and wide keys.
    let expected = BTreeMap::from([(i64::MIN, 1u32), (-1i64, 2u32)]);
    let value: Value<Global> = json::from_str(&json::to_string(&expected)?)?;
    assert_eq!(value::decode_text::<BTreeMap<i64, u32>>(&value)?, expected);

    let expected = BTreeMap::from([(u128::MAX, 1u32)]);
    let value: Value<Global> = json::from_str(&json::to_string(&expected)?)?;
    assert_eq!(value::decode_text::<BTreeMap<u128, u32>>(&value)?, expected);

    // Binary mode does not coerce string keys into numbers.
    let value: Value<Global> = json::from_str(r#"{"1":2}"#)?;
    assert!(value::decode::<BTreeMap<u32, u32>>(&value).is_err());
    Ok(())
}

/// The exact number representation is preserved.
#[test]
fn number_representation() -> Result<()> {
    let value: Value<Global> = value::encode(-3i16)?;
    assert!(value.is_i16());
    assert_eq!(format!("{value:?}"), "-3");

    let value: Value<Global> = value::encode(1.5f32)?;
    assert!(value.is_f32());
    assert_eq!(format!("{value:?}"), "1.5");
    Ok(())
}

/// A `usize` and an `isize` use the platform specific representation.
#[test]
fn platform_sized_integers() -> Result<()> {
    let value: Value<Global> = value::encode(42usize)?;
    assert!(value.is_usize());
    #[cfg(target_pointer_width = "32")]
    assert_eq!(value, Value::from(42u32));
    #[cfg(target_pointer_width = "64")]
    assert_eq!(value, Value::from(42u64));

    let value: Value<Global> = value::encode(-42isize)?;
    assert!(value.is_isize());
    #[cfg(target_pointer_width = "32")]
    assert_eq!(value, Value::from(-42i32));
    #[cfg(target_pointer_width = "64")]
    assert_eq!(value, Value::from(-42i64));
    Ok(())
}

#[derive(Debug, PartialEq, Decode, Encode)]
enum Enum {
    Unit,
    Tuple(u32, String),
    Struct { a: u32, b: String },
}

/// A value decoded from JSON stores an externally tagged variant as a map with
/// a single entry, which must still decode as a variant.
#[test]
fn json_enum() -> Result<()> {
    for expected in [
        Enum::Unit,
        Enum::Tuple(42, String::from("hello")),
        Enum::Struct {
            a: 42,
            b: String::from("hello"),
        },
    ] {
        let json = json::to_string(&expected)?;
        let value: Value<Global> = json::from_str(&json)?;
        let actual: Enum = value::decode_text(&value)?;
        assert_eq!(actual, expected, "{json}");
    }

    Ok(())
}

/// Maps which do not have exactly one entry are not variants.
#[test]
fn json_enum_malformed() -> Result<()> {
    for json in [
        r#"{}"#,
        r#"{"Unit":{},"Unit":{}}"#,
        r#"{"Unit":{},"Struct":{"a":42,"b":"hello"}}"#,
        r#"[]"#,
        r#"42"#,
    ] {
        let value: Value<Global> = json::from_str(json)?;
        assert!(value::decode_text::<Enum>(&value).is_err(), "{json}");
    }

    let value: Value<Global> = json::from_str(r#"{"Missing":{}}"#)?;
    assert!(value::decode_text::<Enum>(&value).is_err());

    let value: Value<Global> = json::from_str(r#"{"Struct":{"a":"wrong"}}"#)?;
    assert!(value::decode_text::<Enum>(&value).is_err());
    Ok(())
}

/// Decodes an [`Enum`] by buffering it first, which for JSON goes through a
/// value.
#[derive(Debug, PartialEq)]
struct Buffered(Enum);

impl<'de, A> Decode<'de, Text, A> for Buffered
where
    A: Allocator,
{
    const IS_BITWISE_DECODE: bool = false;

    #[inline]
    fn decode<D>(decoder: D) -> Result<Self, D::Error>
    where
        D: Decoder<'de, Mode = Text, Allocator = A>,
    {
        let buffer = decoder.decode_buffer()?;
        Ok(Buffered(buffer.as_decoder()?.decode()?))
    }
}

/// A buffered JSON enum is decoded through a value.
#[test]
fn json_enum_buffered() -> Result<()> {
    let expected = Enum::Struct {
        a: 42,
        b: String::from("hello"),
    };

    let json = json::to_string(&expected)?;
    let Buffered(actual) = json::from_str(&json)?;
    assert_eq!(actual, expected);
    Ok(())
}