simple-update-in 0.2.0

Immutable nested update with structural sharing
Documentation
//! Predicate paths: branching, partial updates, auto-creation under each match,
//! and no-op when nothing matches or the parent is missing.

mod common;

use common::*;
use simple_update_in::{Accessor, Selector, Value};

/// Pull the number out of a value, or `0.0` if it is not a number.
fn num(v: &Value) -> f64 {
    match v {
        Value::Number(x) => *x,
        _ => 0.0,
    }
}

#[test]
fn update_array_using_predicate() {
    // value even and index odd
    let from = arr(vec![n(1.0), n(2.0), n(3.0), n(4.0), n(5.0)]);
    let pred = Accessor::predicate(|v, sel| {
        let even_value = num(v) % 2.0 == 0.0;
        let odd_index = matches!(sel, Selector::Index(i) if i % 2 == 1);
        even_value && odd_index
    });
    let actual = run(from, &[pred], multiply(10.0));
    assert_eq!(actual, arr(vec![n(1.0), n(20.0), n(3.0), n(40.0), n(5.0)]),);
}

#[test]
fn update_map_using_predicate() {
    let from = obj(vec![
        ("abc", n(123.0)),
        ("def", n(456.0)),
        ("ghi", n(789.0)),
    ]);
    let pred = Accessor::predicate(|v, sel| {
        let odd = (num(v) as i64) % 2 != 0;
        let key_ok = matches!(sel, Selector::Key(k) if *k == "abc" || *k == "ghi");
        odd && key_ok
    });
    let actual = run(from, &[pred], multiply(10.0));
    assert_eq!(
        actual,
        obj(vec![
            ("abc", n(1230.0)),
            ("def", n(456.0)),
            ("ghi", n(7890.0)),
        ]),
    );
}

#[test]
fn update_using_multiple_predicates() {
    let from = obj(vec![
        ("one", arr(vec![n(1.1), n(1.2), n(1.3)])),
        ("two", arr(vec![n(2.1), n(2.2), n(2.3)])),
    ]);
    let always = Accessor::predicate(|_, _| true);
    let pick = Accessor::predicate(|v, _| num(v) == 1.2 || num(v) == 2.2);
    let actual = run(from, &[always, pick], multiply(10.0));
    assert_eq!(
        actual,
        obj(vec![
            ("one", arr(vec![n(1.1), n(12.0), n(1.3)])),
            ("two", arr(vec![n(2.1), n(22.0), n(2.3)])),
        ]),
    );
}

#[test]
fn predicate_removal_splices_left_to_right() {
    // The predicate resolves concrete paths [[0], [1]] against the original
    // array. Writes fold left to right. Removing index 0 shifts index 1 down,
    // so the second write targets index 1 on a one-element array and is a
    // no-op. The result is [20], not [].
    let from = arr(vec![n(10.0), n(20.0)]);
    let always = Accessor::predicate(|_, _| true);
    let actual = remove(from, &[always]);
    assert_eq!(actual, arr(vec![n(20.0)]));
}

#[test]
fn predicate_without_match_in_array_is_no_op() {
    let from = arr(vec![n(1.0), n(2.0), n(3.0), n(4.0), n(5.0)]);
    let pred = Accessor::predicate(|_, _| false);
    let actual = run(from.clone(), &[pred], constant(n(0.0)));
    assert!(from.ptr_eq(&actual));
}

#[test]
fn predicate_without_match_in_map_is_no_op() {
    let from = obj(vec![
        ("abc", n(123.0)),
        ("def", n(456.0)),
        ("ghi", n(789.0)),
    ]);
    let pred = Accessor::predicate(|_, _| false);
    let actual = run(from.clone(), &[pred], constant(n(0.0)));
    assert!(from.ptr_eq(&actual));
}

#[test]
fn array_auto_creation_using_predicate() {
    let from = arr(vec![arr(vec![n(1.1)]), arr(vec![n(2.2)])]);
    let always = Accessor::predicate(|_, _| true);
    let actual = run(from, &[always, idx(1)], constant(s("fin")));
    assert_eq!(
        actual,
        arr(vec![
            arr(vec![n(1.1), s("fin")]),
            arr(vec![n(2.2), s("fin")]),
        ]),
    );
}

#[test]
fn map_auto_creation_using_predicate() {
    let from = obj(vec![("abc", obj(vec![])), ("def", obj(vec![]))]);
    let always = Accessor::predicate(|_, _| true);
    let actual = run(from, &[always, key("value")], constant(n(123.0)));
    assert_eq!(
        actual,
        obj(vec![
            ("abc", obj(vec![("value", n(123.0))])),
            ("def", obj(vec![("value", n(123.0))])),
        ]),
    );
}

#[test]
fn array_partial_update_using_predicate_shares_others() {
    let from = arr(vec![obj(vec![]), obj(vec![]), obj(vec![])]);
    let pick = Accessor::predicate(|_, sel| matches!(sel, Selector::Index(0)));
    let actual = run(from.clone(), &[pick, key("value")], constant(n(123.0)));
    assert_eq!(
        actual,
        arr(vec![
            obj(vec![("value", n(123.0))]),
            obj(vec![]),
            obj(vec![]),
        ]),
    );
    assert!(from.as_array().unwrap()[1].ptr_eq(&actual.as_array().unwrap()[1]));
    assert!(from.as_array().unwrap()[2].ptr_eq(&actual.as_array().unwrap()[2]));
}

#[test]
fn map_partial_update_using_predicate_shares_others() {
    let from = obj(vec![
        ("abc", obj(vec![])),
        ("def", obj(vec![])),
        ("ghi", obj(vec![])),
    ]);
    let pick = Accessor::predicate(|_, sel| matches!(sel, Selector::Key(k) if *k == "abc"));
    let actual = run(from.clone(), &[pick, key("value")], constant(n(123.0)));
    assert_eq!(
        actual,
        obj(vec![
            ("abc", obj(vec![("value", n(123.0))])),
            ("def", obj(vec![])),
            ("ghi", obj(vec![])),
        ]),
    );
    let f = from.as_object().unwrap();
    let a = actual.as_object().unwrap();
    assert!(f.get("def").unwrap().ptr_eq(a.get("def").unwrap()));
    assert!(f.get("ghi").unwrap().ptr_eq(a.get("ghi").unwrap()));
}

#[test]
fn array_with_non_existing_index_then_predicate_is_no_op() {
    let from = arr(vec![]);
    let always = Accessor::predicate(|_, _| true);
    let actual = run(from.clone(), &[idx(0), always], constant(n(1.0)));
    assert!(from.ptr_eq(&actual));
}

#[test]
fn map_with_non_existing_key_then_predicate_is_no_op() {
    let from = obj(vec![]);
    let always = Accessor::predicate(|_, _| true);
    let actual = run(from.clone(), &[key("abc"), always], constant(n(1.0)));
    assert!(from.ptr_eq(&actual));
}

#[test]
fn literal_into_existing_container_then_non_matching_predicate_is_no_op() {
    // The literal key descends into a real array, then the predicate matches no
    // index. Resolution descends one real level and still yields no concrete
    // path, so the call returns the root by pointer. This differs from a
    // missing parent: here the container exists.
    let from = obj(vec![("a", arr(vec![n(1.0), n(2.0)]))]);
    let never = Accessor::predicate(|_, _| false);
    let actual = run(from.clone(), &[key("a"), never], constant(n(9.0)));
    assert!(from.ptr_eq(&actual));
    assert_eq!(actual, obj(vec![("a", arr(vec![n(1.0), n(2.0)]))]));
}

#[test]
fn predicate_reads_index_through_accessor() {
    // value even and index even, read through Selector::as_index.
    let from = arr(vec![n(1.0), n(2.0), n(3.0), n(4.0), n(6.0)]);
    let pred = Accessor::predicate(|v, sel| {
        let even_value = num(v) % 2.0 == 0.0;
        let even_index = sel.as_index().is_some_and(|i| i % 2 == 0);
        even_value && even_index
    });
    let actual = run(from, &[pred], multiply(10.0));
    assert_eq!(actual, arr(vec![n(1.0), n(2.0), n(3.0), n(4.0), n(60.0)]));
}

#[test]
fn predicate_reads_key_through_accessor() {
    // Match a map key by reading Selector::as_key, no enum match needed.
    let from = obj(vec![("keep", n(1.0)), ("hit", n(2.0))]);
    let pred = Accessor::predicate(|_, sel| sel.as_key() == Some("hit"));
    let actual = run(from, &[pred], multiply(10.0));
    assert_eq!(actual, obj(vec![("keep", n(1.0)), ("hit", n(20.0))]));
}

// README examples.

#[test]
fn readme_truthiness_predicate() {
    // odd values get multiplied
    let from = arr(vec![n(1.0), n(2.0), n(3.0), n(4.0), n(5.0)]);
    let pred = Accessor::predicate(|v, _| (num(v) as i64) % 2 != 0);
    let actual = run(from, &[pred], multiply(10.0));
    assert_eq!(actual, arr(vec![n(10.0), n(2.0), n(30.0), n(4.0), n(50.0)]),);
}

#[test]
fn readme_branch_then_key() {
    let from = arr(vec![
        obj(vec![("v", n(1.0))]),
        obj(vec![("v", n(2.0))]),
        obj(vec![("v", n(3.0))]),
    ]);
    let always = Accessor::predicate(|_, _| true);
    let actual = run(from, &[always, key("v")], multiply(10.0));
    assert_eq!(
        actual,
        arr(vec![
            obj(vec![("v", n(10.0))]),
            obj(vec![("v", n(20.0))]),
            obj(vec![("v", n(30.0))]),
        ]),
    );
}