simple-update-in 0.2.0

Immutable nested update with structural sharing
Documentation
//! Immutable nested update with structural sharing.
//!
//! `update_in` walks a path into a JSON-like tree, transforms the value at the
//! end, and returns a new root. Every subtree that did not change is shared by
//! pointer, so a one-leaf edit clones only the nodes along the path.
//!
//! # Model
//!
//! Values are [`Value`]: maps, arrays, and scalars, plus an explicit
//! `Undefined` for an absent root and for removal. Arrays and maps sit behind
//! `Rc`, so [`Value::ptr_eq`] reports whether two values share a node.
//!
//! # Behavior
//!
//! - Immutable. The input is never mutated. The result is a mixed clone: deep
//!   along the path, shared off it.
//! - No-op identity. If the edit produces no change under `SameValue`, the
//!   original root is returned by the same pointer.
//! - Upsert. Missing intermediates are created: a map for a key, an array for
//!   an index.
//! - Type coercion. An incompatible container is replaced. A key needs a map,
//!   an index needs an array.
//! - Removal. An absent updater, or an updater returning `Undefined`, removes
//!   the addressed key or index.
//! - Predicate paths. A path step may be a predicate that selects matching
//!   children, branching the update.
//! - Reserved keys. Steps equal to `__proto__`, `constructor`, or `prototype`
//!   make the whole call a no-op.
//!
//! # Example
//!
//! ```
//! use simple_update_in::{update_in, Accessor, Value};
//!
//! let from = Value::from_pairs([
//!     ("odd", Value::from_pairs([("one", 1.0.into()), ("three", 3.0.into())])),
//!     ("even", Value::from_pairs([("two", 2.0.into())])),
//! ]);
//!
//! let path = [Accessor::key("odd"), Accessor::key("one")];
//! let actual = update_in(from.clone(), &path, Some(&|v| match v {
//!     Value::Number(n) => Value::Number(n * 10.0),
//!     other => other,
//! }));
//!
//! // The off-path subtree is shared.
//! let from_even = from.as_object().unwrap().get("even").unwrap().clone();
//! let actual_even = actual.as_object().unwrap().get("even").unwrap().clone();
//! assert!(from_even.ptr_eq(&actual_even));
//! ```

#![forbid(unsafe_code)]
#![warn(missing_docs)]

mod engine;
mod path;
mod value;

pub use path::{Accessor, Predicate, Selector};
pub use value::{Map, Value};

/// Update the value at `path` and return a new root.
///
/// `path` is a list of [`Accessor`] steps. `updater` is called with the current
/// value at the resolved leaf and returns the new value. `None` removes the
/// leaf. `Some` returning [`Value::Undefined`] also removes it.
///
/// When the edit makes no change under `SameValue`, the original `obj` is
/// returned by the same pointer. Untouched sibling subtrees are shared by
/// pointer too.
///
/// # Example
///
/// ```
/// use simple_update_in::{update_in, Accessor, Value};
///
/// let from = Value::array(vec![0.0.into(), 1.0.into(), 2.0.into()]);
/// let actual = update_in(from, &[Accessor::index(1)], Some(&|_| 3.0.into()));
/// assert_eq!(actual, Value::array(vec![0.0.into(), 3.0.into(), 2.0.into()]));
/// ```
#[must_use]
pub fn update_in(obj: Value, path: &[Accessor], updater: Option<&dyn Fn(Value) -> Value>) -> Value {
    engine::update_in(obj, path, updater)
}

/// Async wrapper over [`update_in`].
///
/// The engine does no IO, so this resolves immediately and returns the same
/// result as [`update_in`]. It exists so async callers can await a single entry
/// point that matches the sync one. The updater and predicates stay synchronous,
/// so the function holds no await point. It is kept `async` on purpose so the
/// call site reads `update_in_async(...).await`.
///
/// # Example
///
/// ```
/// use simple_update_in::{update_in_async, Accessor, Value};
///
/// # async fn run() {
/// let from = Value::from_pairs([("abc", 123.0.into())]);
/// let actual = update_in_async(from, &[Accessor::key("xyz")], Some(&|_| 789.0.into())).await;
/// assert_eq!(
///     actual,
///     Value::from_pairs([("abc", 123.0.into()), ("xyz", 789.0.into())]),
/// );
/// # }
/// ```
#[must_use]
#[allow(clippy::unused_async)]
pub async fn update_in_async(
    obj: Value,
    path: &[Accessor],
    updater: Option<&dyn Fn(Value) -> Value>,
) -> Value {
    engine::update_in(obj, path, updater)
}