Skip to main content

simple_update_in/
lib.rs

1//! Immutable nested update with structural sharing.
2//!
3//! `update_in` walks a path into a JSON-like tree, transforms the value at the
4//! end, and returns a new root. Every subtree that did not change is shared by
5//! pointer, so a one-leaf edit clones only the nodes along the path.
6//!
7//! # Model
8//!
9//! Values are [`Value`]: maps, arrays, and scalars, plus an explicit
10//! `Undefined` for an absent root and for removal. Arrays and maps sit behind
11//! `Rc`, so [`Value::ptr_eq`] reports whether two values share a node.
12//!
13//! # Behavior
14//!
15//! - Immutable. The input is never mutated. The result is a mixed clone: deep
16//!   along the path, shared off it.
17//! - No-op identity. If the edit produces no change under `SameValue`, the
18//!   original root is returned by the same pointer.
19//! - Upsert. Missing intermediates are created: a map for a key, an array for
20//!   an index.
21//! - Type coercion. An incompatible container is replaced. A key needs a map,
22//!   an index needs an array.
23//! - Removal. An absent updater, or an updater returning `Undefined`, removes
24//!   the addressed key or index.
25//! - Predicate paths. A path step may be a predicate that selects matching
26//!   children, branching the update.
27//! - Reserved keys. Steps equal to `__proto__`, `constructor`, or `prototype`
28//!   make the whole call a no-op.
29//!
30//! # Example
31//!
32//! ```
33//! use simple_update_in::{update_in, Accessor, Value};
34//!
35//! let from = Value::from_pairs([
36//!     ("odd", Value::from_pairs([("one", 1.0.into()), ("three", 3.0.into())])),
37//!     ("even", Value::from_pairs([("two", 2.0.into())])),
38//! ]);
39//!
40//! let path = [Accessor::key("odd"), Accessor::key("one")];
41//! let actual = update_in(from.clone(), &path, Some(&|v| match v {
42//!     Value::Number(n) => Value::Number(n * 10.0),
43//!     other => other,
44//! }));
45//!
46//! // The off-path subtree is shared.
47//! let from_even = from.as_object().unwrap().get("even").unwrap().clone();
48//! let actual_even = actual.as_object().unwrap().get("even").unwrap().clone();
49//! assert!(from_even.ptr_eq(&actual_even));
50//! ```
51
52#![forbid(unsafe_code)]
53#![warn(missing_docs)]
54
55mod engine;
56mod path;
57mod value;
58
59pub use path::{Accessor, Predicate, Selector};
60pub use value::{Map, Value};
61
62/// Update the value at `path` and return a new root.
63///
64/// `path` is a list of [`Accessor`] steps. `updater` is called with the current
65/// value at the resolved leaf and returns the new value. `None` removes the
66/// leaf. `Some` returning [`Value::Undefined`] also removes it.
67///
68/// When the edit makes no change under `SameValue`, the original `obj` is
69/// returned by the same pointer. Untouched sibling subtrees are shared by
70/// pointer too.
71///
72/// # Example
73///
74/// ```
75/// use simple_update_in::{update_in, Accessor, Value};
76///
77/// let from = Value::array(vec![0.0.into(), 1.0.into(), 2.0.into()]);
78/// let actual = update_in(from, &[Accessor::index(1)], Some(&|_| 3.0.into()));
79/// assert_eq!(actual, Value::array(vec![0.0.into(), 3.0.into(), 2.0.into()]));
80/// ```
81#[must_use]
82pub fn update_in(obj: Value, path: &[Accessor], updater: Option<&dyn Fn(Value) -> Value>) -> Value {
83    engine::update_in(obj, path, updater)
84}
85
86/// Async wrapper over [`update_in`].
87///
88/// The engine does no IO, so this resolves immediately and returns the same
89/// result as [`update_in`]. It exists so async callers can await a single entry
90/// point that matches the sync one. The updater and predicates stay synchronous,
91/// so the function holds no await point. It is kept `async` on purpose so the
92/// call site reads `update_in_async(...).await`.
93///
94/// # Example
95///
96/// ```
97/// use simple_update_in::{update_in_async, Accessor, Value};
98///
99/// # async fn run() {
100/// let from = Value::from_pairs([("abc", 123.0.into())]);
101/// let actual = update_in_async(from, &[Accessor::key("xyz")], Some(&|_| 789.0.into())).await;
102/// assert_eq!(
103///     actual,
104///     Value::from_pairs([("abc", 123.0.into()), ("xyz", 789.0.into())]),
105/// );
106/// # }
107/// ```
108#[must_use]
109#[allow(clippy::unused_async)]
110pub async fn update_in_async(
111    obj: Value,
112    path: &[Accessor],
113    updater: Option<&dyn Fn(Value) -> Value>,
114) -> Value {
115    engine::update_in(obj, path, updater)
116}