simple-update-in 0.2.0

Immutable nested update with structural sharing
Documentation
//! Edge values that cannot live in plain JSON: sparse holes, `-0`, `NaN`,
//! an absent root, and a string root.
//!
//! Sparse holes map to `Undefined`. SameValue drives the no-op check, so `-0`
//! over `0` is a change and `NaN` over `NaN` is not.

mod common;

use common::*;
use simple_update_in::Value;

#[test]
fn expand_undefined_as_array() {
    // undefined -> [<hole>, 1]
    let actual = run(Value::Undefined, &[idx(1)], constant(n(1.0)));
    assert_eq!(actual, arr(vec![hole(), n(1.0)]));
}

#[test]
fn expand_array_with_hole() {
    // [0, 1] set index 3 -> [0, 1, <hole>, 3]
    let from = arr(vec![n(0.0), n(1.0)]);
    let actual = run(from.clone(), &[idx(3)], constant(n(3.0)));
    assert!(!from.ptr_eq(&actual));
    assert_eq!(actual, arr(vec![n(0.0), n(1.0), hole(), n(3.0)]));
}

#[test]
fn fill_past_end_stores_hole_as_undefined() {
    // Pin the hole variant directly. A gap filled past the end is Undefined,
    // not Null, so it reads back as missing like a JavaScript sparse slot.
    let from = arr(vec![n(0.0)]);
    let actual = run(from, &[idx(2)], constant(n(3.0)));
    let items = actual.as_array().unwrap();
    assert!(matches!(items[1], Value::Undefined));
    assert!(items[1].is_undefined());
}

#[test]
fn incompatible_type_convert_map_to_array() {
    // {one:1} set index 1 -> [<hole>, 1]
    let from = obj(vec![("one", n(1.0))]);
    let actual = run(from.clone(), &[idx(1)], constant(n(1.0)));
    assert!(!from.ptr_eq(&actual));
    assert_eq!(actual, arr(vec![hole(), n(1.0)]));
}

#[test]
fn incompatible_type_convert_string_to_array() {
    // "123" set index 1 -> [<hole>, 1]
    let from = s("123");
    let actual = run(from, &[idx(1)], constant(n(1.0)));
    assert_eq!(actual, arr(vec![hole(), n(1.0)]));
}

#[test]
fn update_0_with_negative_0() {
    // Object.is(0, -0) === false, so this is a change.
    let from = arr(vec![n(0.0)]);
    let actual = run(from.clone(), &[idx(0)], constant(n(-0.0)));
    assert!(!from.ptr_eq(&actual));
    assert_eq!(actual, arr(vec![n(-0.0)]));
}

#[test]
fn update_nan_with_nan() {
    // Object.is(NaN, NaN) === true, so this is a no-op.
    let from = arr(vec![n(f64::NAN)]);
    let actual = run(from.clone(), &[idx(0)], constant(n(f64::NAN)));
    assert!(from.ptr_eq(&actual));
}

#[test]
fn change_across_type_is_not_no_op() {
    // Object.is(1, "1") === false, so writing a string over a number changes.
    let from = arr(vec![n(1.0)]);
    let actual = run(from.clone(), &[idx(0)], constant(s("1")));
    assert!(!from.ptr_eq(&actual));
    assert_eq!(actual, arr(vec![s("1")]));
}

#[test]
fn null_root_with_key_coerces_to_map() {
    // A null node counts as a non-array object, so a key accessor builds an
    // empty map, then sets the key.
    let actual = run(Value::Null, &[key("x")], constant(n(1.0)));
    assert_eq!(actual, obj(vec![("x", n(1.0))]));
}

#[test]
fn null_root_with_index_coerces_to_array() {
    // A null node is not an array, so an index accessor builds an empty array
    // and fills the gap with a hole.
    let actual = run(Value::Null, &[idx(1)], constant(n(1.0)));
    assert_eq!(actual, arr(vec![hole(), n(1.0)]));
}

#[test]
fn null_intermediate_coerces_to_map() {
    // A null value mid-path coerces to a map under a key accessor.
    let from = obj(vec![("a", Value::Null)]);
    let actual = run(from, &[key("a"), key("b")], constant(n(1.0)));
    assert_eq!(actual, obj(vec![("a", obj(vec![("b", n(1.0))]))]));
}

#[test]
fn updater_sees_falsy_present_intermediate_not_undefined() {
    // {a: 0} with path [a, b]. The walk hits a present-but-falsy 0 and stops,
    // so the updater receives Number(0), not Undefined. The distinction is
    // observable for an updater that branches on the input.
    use std::cell::Cell;
    let from = obj(vec![("a", n(0.0))]);
    let seen = Cell::new(Value::Undefined);
    let actual = run(from, &[key("a"), key("b")], |v| {
        seen.set(v);
        n(1.0)
    });
    assert_eq!(seen.into_inner(), n(0.0));
    assert_eq!(actual, obj(vec![("a", obj(vec![("b", n(1.0))]))]));
}