tru 0.2.3

TOON reference implementation in Rust (JSON <-> TOON)
Documentation
use crate::{JsonArray, JsonObject, JsonPrimitive, JsonValue, StringOrNumberOrBoolOrNull};

pub fn normalize_json_value(value: JsonValue) -> JsonValue {
    match value {
        JsonValue::Primitive(primitive) => JsonValue::Primitive(normalize_primitive(primitive)),
        JsonValue::Array(items) => {
            JsonValue::Array(items.into_iter().map(normalize_json_value).collect())
        }
        JsonValue::Object(entries) => JsonValue::Object(
            entries
                .into_iter()
                .map(|(key, value)| (key, normalize_json_value(value)))
                .collect(),
        ),
    }
}

#[must_use]
pub fn normalize_primitive(value: JsonPrimitive) -> JsonPrimitive {
    match value {
        StringOrNumberOrBoolOrNull::Number(value) => {
            if !value.is_finite() {
                StringOrNumberOrBoolOrNull::Null
            } else if value == 0.0 {
                StringOrNumberOrBoolOrNull::Number(0.0)
            } else {
                StringOrNumberOrBoolOrNull::Number(value)
            }
        }
        _ => value,
    }
}

#[must_use]
pub const fn is_json_primitive(value: &JsonValue) -> bool {
    matches!(value, JsonValue::Primitive(_))
}

#[must_use]
pub const fn is_json_array(value: &JsonValue) -> bool {
    matches!(value, JsonValue::Array(_))
}

#[must_use]
pub const fn is_json_object(value: &JsonValue) -> bool {
    matches!(value, JsonValue::Object(_))
}

#[must_use]
pub const fn is_empty_object(value: &JsonObject) -> bool {
    value.is_empty()
}

#[must_use]
pub fn is_array_of_primitives(value: &JsonArray) -> bool {
    value
        .iter()
        .all(|item| matches!(item, JsonValue::Primitive(_)))
}

#[must_use]
pub fn is_array_of_arrays(value: &JsonArray) -> bool {
    value.iter().all(|item| matches!(item, JsonValue::Array(_)))
}

#[must_use]
pub fn is_array_of_objects(value: &JsonArray) -> bool {
    value
        .iter()
        .all(|item| matches!(item, JsonValue::Object(_)))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn s(v: &str) -> JsonValue {
        JsonValue::Primitive(StringOrNumberOrBoolOrNull::String(v.to_string()))
    }

    fn n(v: f64) -> JsonValue {
        JsonValue::Primitive(StringOrNumberOrBoolOrNull::Number(v))
    }

    #[test]
    fn normalize_primitive_passes_string_through() {
        let v = StringOrNumberOrBoolOrNull::String("hello".into());
        assert_eq!(
            normalize_primitive(v),
            StringOrNumberOrBoolOrNull::String("hello".into())
        );
    }

    #[test]
    fn normalize_primitive_preserves_finite_numbers() {
        let v = StringOrNumberOrBoolOrNull::Number(3.5);
        assert_eq!(
            normalize_primitive(v),
            StringOrNumberOrBoolOrNull::Number(3.5)
        );
    }

    #[test]
    fn normalize_primitive_turns_nan_into_null() {
        let v = StringOrNumberOrBoolOrNull::Number(f64::NAN);
        assert_eq!(normalize_primitive(v), StringOrNumberOrBoolOrNull::Null);
    }

    #[test]
    fn normalize_primitive_turns_infinity_into_null() {
        let v = StringOrNumberOrBoolOrNull::Number(f64::INFINITY);
        assert_eq!(normalize_primitive(v), StringOrNumberOrBoolOrNull::Null);
        let v = StringOrNumberOrBoolOrNull::Number(f64::NEG_INFINITY);
        assert_eq!(normalize_primitive(v), StringOrNumberOrBoolOrNull::Null);
    }

    #[test]
    fn normalize_primitive_zero_is_positive() {
        let v = StringOrNumberOrBoolOrNull::Number(-0.0);
        let normalized = normalize_primitive(v);
        if let StringOrNumberOrBoolOrNull::Number(n) = normalized {
            assert!(!n.is_sign_negative(), "expected positive zero");
        } else {
            panic!("expected Number variant");
        }
    }

    #[test]
    fn normalize_primitive_preserves_bool_and_null() {
        assert_eq!(
            normalize_primitive(StringOrNumberOrBoolOrNull::Bool(true)),
            StringOrNumberOrBoolOrNull::Bool(true)
        );
        assert_eq!(
            normalize_primitive(StringOrNumberOrBoolOrNull::Null),
            StringOrNumberOrBoolOrNull::Null
        );
    }

    #[test]
    fn normalize_json_value_walks_array_and_replaces_nan() {
        let v = JsonValue::Array(vec![n(1.0), n(f64::NAN), s("x")]);
        let out = normalize_json_value(v);
        if let JsonValue::Array(items) = out {
            assert_eq!(items.len(), 3);
            assert!(matches!(
                &items[1],
                JsonValue::Primitive(StringOrNumberOrBoolOrNull::Null)
            ));
        } else {
            panic!("expected Array");
        }
    }

    #[test]
    fn normalize_json_value_walks_object() {
        let v = JsonValue::Object(vec![
            ("ok".to_string(), n(1.0)),
            ("bad".to_string(), n(f64::INFINITY)),
        ]);
        let out = normalize_json_value(v);
        if let JsonValue::Object(entries) = out {
            assert!(matches!(
                &entries[1].1,
                JsonValue::Primitive(StringOrNumberOrBoolOrNull::Null)
            ));
        } else {
            panic!("expected Object");
        }
    }

    #[test]
    fn is_json_primitive_detects_primitives() {
        assert!(is_json_primitive(&n(1.0)));
        assert!(!is_json_primitive(&JsonValue::Array(vec![])));
        assert!(!is_json_primitive(&JsonValue::Object(vec![])));
    }

    #[test]
    fn is_json_array_detects_arrays() {
        assert!(is_json_array(&JsonValue::Array(vec![])));
        assert!(!is_json_array(&n(1.0)));
    }

    #[test]
    fn is_json_object_detects_objects() {
        assert!(is_json_object(&JsonValue::Object(vec![])));
        assert!(!is_json_object(&JsonValue::Array(vec![])));
    }

    #[test]
    fn is_empty_object_detects_empty_and_non_empty() {
        let empty: JsonObject = vec![];
        let non_empty: JsonObject = vec![("a".into(), n(1.0))];
        assert!(is_empty_object(&empty));
        assert!(!is_empty_object(&non_empty));
    }

    #[test]
    fn is_array_of_primitives_checks_every_item() {
        let arr: JsonArray = vec![n(1.0), s("x")];
        assert!(is_array_of_primitives(&arr));
        let arr: JsonArray = vec![n(1.0), JsonValue::Array(vec![])];
        assert!(!is_array_of_primitives(&arr));
    }

    #[test]
    fn is_array_of_primitives_true_for_empty() {
        let arr: JsonArray = vec![];
        assert!(is_array_of_primitives(&arr));
    }

    #[test]
    fn is_array_of_arrays_checks_every_item() {
        let arr: JsonArray = vec![JsonValue::Array(vec![n(1.0)]), JsonValue::Array(vec![])];
        assert!(is_array_of_arrays(&arr));
        let arr: JsonArray = vec![JsonValue::Array(vec![]), n(1.0)];
        assert!(!is_array_of_arrays(&arr));
    }

    #[test]
    fn is_array_of_objects_checks_every_item() {
        let arr: JsonArray = vec![JsonValue::Object(vec![])];
        assert!(is_array_of_objects(&arr));
        let arr: JsonArray = vec![JsonValue::Object(vec![]), n(1.0)];
        assert!(!is_array_of_objects(&arr));
    }
}