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}