json-traits 0.1.1

Server-side helper for leaf-validator style PATCH updates: flatten JSON, diff documents, and match dotted paths
Documentation
#![allow(missing_docs)]

use std::collections::BTreeMap;

use json_traits::{DiffJson, JsonPaths, JsonScalar};
use serde_json::json;

fn scalar_paths(value: serde_json::Value) -> BTreeMap<String, JsonScalar> {
    value.paths_and_values()
}

#[test]
fn finds_no_paths_for_a_root_scalar() {
    let paths = scalar_paths(json!("value"));
    assert_eq!(
        paths,
        BTreeMap::from([("".to_owned(), JsonScalar::from("value"))])
    );
}

#[test]
fn finds_shallow_paths() {
    let paths = scalar_paths(json!({ "prop1": "value1", "prop2": "value2" }));
    assert_eq!(
        paths,
        BTreeMap::from([
            ("prop1".to_owned(), JsonScalar::from("value1")),
            ("prop2".to_owned(), JsonScalar::from("value2")),
        ])
    );
}

#[test]
fn finds_deep_object_paths() {
    let paths = scalar_paths(json!({
        "prop1": { "prop2": "value" },
        "contacts": { "info": { "name": "Stewie" } }
    }));
    assert_eq!(
        paths,
        BTreeMap::from([
            ("prop1.prop2".to_owned(), JsonScalar::from("value")),
            ("contacts.info.name".to_owned(), JsonScalar::from("Stewie")),
        ])
    );
}

#[test]
fn finds_deep_object_and_array_paths() {
    let paths = scalar_paths(json!({
        "prop1": { "prop2": "value" },
        "contacts": [
            { "info": { "name": "Stewie" } },
            { "info": { "number": 12 } },
            { "info": { "isAwesome": true } }
        ]
    }));
    assert_eq!(
        paths,
        BTreeMap::from([
            ("prop1.prop2".to_owned(), JsonScalar::from("value")),
            (
                "contacts.0.info.name".to_owned(),
                JsonScalar::from("Stewie"),
            ),
            (
                "contacts.1.info.number".to_owned(),
                JsonScalar::from_integer(12),
            ),
            (
                "contacts.2.info.isAwesome".to_owned(),
                JsonScalar::from(true),
            ),
        ])
    );
}

#[test]
fn records_null_false_and_a_root_array() {
    assert_eq!(
        scalar_paths(json!(null)),
        BTreeMap::from([("".to_owned(), JsonScalar::Null)])
    );
    assert_eq!(
        scalar_paths(json!(false)),
        BTreeMap::from([("".to_owned(), JsonScalar::from(false))])
    );
    assert_eq!(
        scalar_paths(json!([{"a": true}, "x"])),
        BTreeMap::from([
            ("0.a".to_owned(), JsonScalar::from(true)),
            ("1".to_owned(), JsonScalar::from("x")),
        ])
    );
}

#[test]
fn empty_containers_have_no_leaves() {
    assert!(scalar_paths(json!({})).is_empty());
    assert!(scalar_paths(json!([])).is_empty());
    assert!(scalar_paths(json!({"a": {}})).is_empty());
    assert!(scalar_paths(json!({"a": []})).is_empty());
}

#[test]
fn array_indexes_are_decimal_and_not_padded() {
    let mut items = vec![json!(null); 10];
    items.push(json!("last"));
    let paths = serde_json::Value::Array(items).paths_and_values();
    assert_eq!(paths.get("10"), Some(&JsonScalar::from("last")));
}

#[test]
fn a_dot_in_an_object_key_is_not_escaped() {
    let flat = json!({ "a.b": 1 }).paths_and_values();
    let nested = json!({ "a": { "b": 1 } }).paths_and_values();
    assert_eq!(flat, nested);
    assert_eq!(flat.get("a.b"), Some(&JsonScalar::from_integer(1)));
}

#[test]
fn diffs_two_json_documents() {
    let left = json!({
        "prop1": { "prop2": 1 },
        "contacts": [
            { "info": { "name": "Stewie" } },
            { "info": { "number": 12 } },
            { "info": { "isAwesome": true } }
        ]
    });
    let right = json!({
        "prop1": { "prop2": "value2" },
        "contacts": [
            { "info": { "name": "Stewie" } },
            { "info": { "number": 13 } },
            { "info": { "isAwesome": false } },
            { "info": { "isSomething": true } }
        ]
    });

    assert_eq!(
        left.diff_with(&right),
        BTreeMap::from([
            (
                "prop1.prop2".to_owned(),
                (JsonScalar::from_integer(1), JsonScalar::from("value2")),
            ),
            (
                "contacts.1.info.number".to_owned(),
                (JsonScalar::from_integer(12), JsonScalar::from_integer(13)),
            ),
            (
                "contacts.2.info.isAwesome".to_owned(),
                (JsonScalar::from(true), JsonScalar::from(false)),
            ),
            (
                "contacts.3.info.isSomething".to_owned(),
                (JsonScalar::Undefined, JsonScalar::from(true)),
            ),
        ])
    );
}

#[test]
fn identical_documents_and_equal_nulls_have_no_diff() {
    let document = json!({"a": null, "b": [1, {"c": false}]});
    assert!(document.diff_with(&document).is_empty());
}

#[test]
fn reports_added_and_removed_paths() {
    let left = json!({"a": 1, "b": 2});
    let right = json!({"b": 2, "c": 3});
    assert_eq!(
        left.diff_with(&right),
        BTreeMap::from([
            (
                "a".to_owned(),
                (JsonScalar::from_integer(1), JsonScalar::Undefined),
            ),
            (
                "c".to_owned(),
                (JsonScalar::Undefined, JsonScalar::from_integer(3)),
            ),
        ])
    );
}

#[test]
fn null_against_a_missing_path_is_a_difference() {
    let diff = json!({"a": null}).diff_with(&json!({}));
    assert_eq!(
        diff.get("a"),
        Some(&(JsonScalar::Null, JsonScalar::Undefined))
    );
}

#[test]
fn integer_and_matching_decimal_are_the_same_value() {
    assert!(json!({"n": 1}).diff_with(&json!({"n": 1.0})).is_empty());
    assert!(!json!({"n": "1"}).diff_with(&json!({"n": 1})).is_empty());
    assert_eq!(
        JsonScalar::from_float(1.0),
        Some(JsonScalar::from_integer(1))
    );
    assert!(JsonScalar::from_float(f64::NAN).is_none());
    assert!(JsonScalar::from_float(f64::INFINITY).is_none());
}

#[test]
fn distinct_large_integers_stay_different() {
    let left = json!({"n": 9007199254740993_u64});
    let right = json!({"n": 9007199254740992_u64});
    assert!(!left.diff_with(&right).is_empty());
}

#[test]
fn diff_of_path_maps_matches_diff_of_documents() {
    let left = json!({"a": 1});
    let right = json!({"a": 2});
    assert_eq!(
        left.diff_with(&right),
        left.paths_and_values().diff_with(&right.paths_and_values())
    );
}