# immutability-helper

Immutable nested-state updates through a command-spec DSL.

Apply a declarative spec to a value and get back a new value. Only the touched
parts are copied. Everything else is shared with the input by reference, so an
unchanged branch stays pointer-identical. That sharing makes cheap identity
checks possible after an update.

The spec is a tree of `$`-prefixed commands and child key paths, inspired by
MongoDB update operators.

## Installation

```toml
[dependencies]
immutability-helper = "0.1"
```

## Commands

| Command | Target | Effect |
| --- | --- | --- |
| `$push` | array | append items |
| `$unshift` | array | prepend items |
| `$splice` | array | run each splice in order |
| `$set` | any | replace the value |
| `$toggle` | object | flip boolean fields |
| `$unset` | object | remove keys |
| `$merge` | object | shallow-merge keys |
| `$add` | Map or Set | add entries or values |
| `$remove` | Map or Set | remove entries or values |
| `$apply` | any | replace with `f(current)` |

A bare function is shorthand for `$apply`.

## Examples

Push onto an array:

```rust
use immutability_helper::{update, Spec, Value};

let original = Value::array(vec![Value::from("x")]);
let result = update(&original, &Spec::push(vec![Value::from("y")])).unwrap();
assert_eq!(result, Value::array(vec![Value::from("x"), Value::from("y")]));
```

Set a nested value while keeping untouched branches shared:

```rust
use immutability_helper::{update, Spec, Value};

let original = Value::object(vec![
    ("a", Value::object(vec![("b", Value::from(1))])),
    ("c", Value::object(vec![("d", Value::from(2))])),
]);

let spec = Spec::at("a", Spec::at("b", Spec::set(Value::from(2))));
let result = update(&original, &spec).unwrap();

let (Value::Object(orig), Value::Object(next)) = (&original, &result) else {
    unreachable!()
};
// The c branch was not touched, so it is the same allocation.
assert!(orig["c"].same_value(&next["c"]));
```

## Reference equality

Container values hold their contents behind `Rc`. A no-op update returns the
original reference. `Value::same_value` reports whether two values share an
allocation. Use it to tell whether an update changed a node.

The comparison function is pluggable. The default uses `===` semantics:
references for containers, values for primitives. A `Context` can install a
deeper comparison so value-preserving updates collapse back to the original.

## Custom commands

A `Context` carries its own command table. `extend` registers a directive. The
callback receives the spec value, the current value, and the original target.

```rust
use immutability_helper::{Context, Spec, Value};

let mut ctx = Context::new();
ctx.extend("$addtax", |tax, original, _orig| {
    let tax = if let Value::Number(n) = tax { *n } else { 0.0 };
    let base = if let Value::Number(n) = original { *n } else { 0.0 };
    Ok(Value::Number(base + tax * base))
});

let result = ctx
    .update(&Value::from(5), &Spec::custom("$addtax", Value::from(0.10)))
    .unwrap();
assert_eq!(result, Value::Number(5.5));
```

## Scope

This crate models plain data only. It has no prototype chains and no symbol
keys. Object keys are strings in insertion order. Arrays do not carry extra
non-index properties.

Map and Set members are compared by value, not by reference. `$add` and
`$remove` treat a structurally equal value as already present. Adding a value
that equals an existing member is a no-op, and removing a value that equals a
member drops it even when it is a different allocation. The data model keys Map
and Set contents by structure, so two equal values cannot hold separate
identities.

## License

Licensed under the [MIT license](LICENSE).
