tirea_state/
apply.rs

1//! Patch application logic.
2//!
3//! This module contains the pure `apply_patch` function that applies a patch
4//! to a JSON document and returns a new document.
5
6use crate::{
7    error::{value_type_name, TireaError, TireaResult},
8    lattice::LatticeRegistry,
9    Number, Op, Patch, Path, Seg,
10};
11use serde_json::{Map, Value};
12
13/// Apply a patch to a JSON document (pure function).
14///
15/// This function is deterministic: given the same document and patch,
16/// it will always produce the same result.
17///
18/// # Arguments
19///
20/// * `doc` - The original document (not modified)
21/// * `patch` - The patch to apply
22///
23/// # Returns
24///
25/// A new document with all operations applied, or an error if any operation fails.
26///
27/// # Examples
28///
29/// ```
30/// use tirea_state::{apply_patch, Patch, Op, path};
31/// use serde_json::json;
32///
33/// let doc = json!({"count": 0});
34/// let patch = Patch::new()
35///     .with_op(Op::set(path!("count"), json!(10)))
36///     .with_op(Op::set(path!("name"), json!("test")));
37///
38/// let new_doc = apply_patch(&doc, &patch).unwrap();
39/// assert_eq!(new_doc["count"], 10);
40/// assert_eq!(new_doc["name"], "test");
41///
42/// // Original is unchanged (pure function)
43/// assert_eq!(doc["count"], 0);
44/// ```
45pub fn apply_patch(doc: &Value, patch: &Patch) -> TireaResult<Value> {
46    let mut result = doc.clone();
47    apply_patch_in_place(&mut result, patch)?;
48    Ok(result)
49}
50
51/// Apply a patch directly to an existing document.
52pub(crate) fn apply_patch_in_place(doc: &mut Value, patch: &Patch) -> TireaResult<()> {
53    for op in patch.ops() {
54        apply_op(doc, op)?;
55    }
56    Ok(())
57}
58
59/// Apply multiple patches to a JSON document in sequence (pure function).
60///
61/// This is a convenience function that applies patches one by one.
62/// If any patch fails, the error is returned and no further patches are applied.
63///
64/// # Arguments
65///
66/// * `doc` - The original document (not modified)
67/// * `patches` - Iterator of patches to apply in order
68///
69/// # Returns
70///
71/// A new document with all patches applied, or an error if any patch fails.
72///
73/// # Examples
74///
75/// ```
76/// use tirea_state::{apply_patches, Patch, Op, path};
77/// use serde_json::json;
78///
79/// let doc = json!({"count": 0});
80/// let patches = vec![
81///     Patch::new().with_op(Op::set(path!("count"), json!(1))),
82///     Patch::new().with_op(Op::set(path!("count"), json!(2))),
83/// ];
84///
85/// let new_doc = apply_patches(&doc, patches.iter()).unwrap();
86/// assert_eq!(new_doc["count"], 2);
87/// ```
88pub fn apply_patches<'a>(
89    doc: &Value,
90    patches: impl IntoIterator<Item = &'a Patch>,
91) -> TireaResult<Value> {
92    let mut result = doc.clone();
93    for patch in patches {
94        apply_patch_in_place(&mut result, patch)?;
95    }
96    Ok(result)
97}
98
99/// Apply multiple patches to a JSON document using a `LatticeRegistry` for proper merges.
100///
101/// `LatticeMerge` ops whose path is registered will perform a real lattice merge;
102/// unregistered paths fall back to `Op::Set` semantics.
103pub fn apply_patches_with_registry<'a>(
104    doc: &Value,
105    patches: impl IntoIterator<Item = &'a Patch>,
106    registry: &LatticeRegistry,
107) -> TireaResult<Value> {
108    let mut result = doc.clone();
109    for patch in patches {
110        apply_patch_in_place_with_registry(&mut result, patch, registry)?;
111    }
112    Ok(result)
113}
114
115/// Apply a patch to a JSON document using a `LatticeRegistry` for proper merges.
116///
117/// `LatticeMerge` ops whose path is registered will perform a real lattice merge;
118/// unregistered paths fall back to `Op::Set` semantics.
119pub fn apply_patch_with_registry(
120    doc: &Value,
121    patch: &Patch,
122    registry: &LatticeRegistry,
123) -> TireaResult<Value> {
124    let mut result = doc.clone();
125    apply_patch_in_place_with_registry(&mut result, patch, registry)?;
126    Ok(result)
127}
128
129/// Apply a patch in-place using a `LatticeRegistry`.
130pub(crate) fn apply_patch_in_place_with_registry(
131    doc: &mut Value,
132    patch: &Patch,
133    registry: &LatticeRegistry,
134) -> TireaResult<()> {
135    for op in patch.ops() {
136        apply_op_with_registry(doc, op, registry)?;
137    }
138    Ok(())
139}
140
141/// Apply a single operation using a registry (for `LatticeMerge` ops).
142fn apply_op_with_registry(doc: &mut Value, op: &Op, registry: &LatticeRegistry) -> TireaResult<()> {
143    match op {
144        Op::LatticeMerge { path, value } => {
145            if let Some(merger) = registry.get(path) {
146                let current = get_at_path(doc, path);
147                let merged = merger.merge(current, value)?;
148                apply_set(doc, path, merged)
149            } else {
150                // Unregistered path → fallback to Set
151                apply_set(doc, path, value.clone())
152            }
153        }
154        _ => apply_op(doc, op),
155    }
156}
157
158/// Apply a single operation to a document (mutating).
159pub(crate) fn apply_op(doc: &mut Value, op: &Op) -> TireaResult<()> {
160    match op {
161        Op::Set { path, value } => apply_set(doc, path, value.clone()),
162        Op::Delete { path } => apply_delete(doc, path),
163        Op::Append { path, value } => apply_append(doc, path, value.clone()),
164        Op::MergeObject { path, value } => apply_merge_object(doc, path, value),
165        Op::Increment { path, amount } => apply_increment(doc, path, amount),
166        Op::Decrement { path, amount } => apply_decrement(doc, path, amount),
167        Op::Insert { path, index, value } => apply_insert(doc, path, *index, value.clone()),
168        Op::Remove { path, value } => apply_remove(doc, path, value),
169        // Without a registry, LatticeMerge falls back to Set semantics
170        Op::LatticeMerge { path, value } => apply_set(doc, path, value.clone()),
171    }
172}
173
174/// Apply a Set operation.
175fn apply_set(doc: &mut Value, path: &Path, value: Value) -> TireaResult<()> {
176    if path.is_empty() {
177        *doc = value;
178        return Ok(());
179    }
180
181    set_at_path(doc, path.segments(), value, path)
182}
183
184/// Recursively set a value at a path, creating intermediate objects as needed.
185fn set_at_path(
186    current: &mut Value,
187    segments: &[Seg],
188    value: Value,
189    full_path: &Path,
190) -> TireaResult<()> {
191    match segments {
192        [] => {
193            *current = value;
194            Ok(())
195        }
196        [Seg::Key(key), rest @ ..] => {
197            // Create intermediate object if needed
198            if !current.is_object() {
199                *current = Value::Object(Map::new());
200            }
201
202            let obj = current.as_object_mut().unwrap();
203
204            if rest.is_empty() {
205                obj.insert(key.clone(), value);
206            } else {
207                let entry = obj.entry(key.clone()).or_insert(Value::Null);
208                set_at_path(entry, rest, value, full_path)?;
209            }
210            Ok(())
211        }
212        [Seg::Index(idx), rest @ ..] => {
213            // Check type first to avoid borrow issues
214            if !current.is_array() {
215                return Err(TireaError::type_mismatch(
216                    full_path.clone(),
217                    "array",
218                    value_type_name(current),
219                ));
220            }
221
222            let arr = current.as_array_mut().unwrap();
223
224            if *idx >= arr.len() {
225                return Err(TireaError::index_out_of_bounds(
226                    full_path.clone(),
227                    *idx,
228                    arr.len(),
229                ));
230            }
231
232            if rest.is_empty() {
233                arr[*idx] = value;
234            } else {
235                set_at_path(&mut arr[*idx], rest, value, full_path)?;
236            }
237            Ok(())
238        }
239    }
240}
241
242/// Apply a Delete operation (no-op if path doesn't exist).
243fn apply_delete(doc: &mut Value, path: &Path) -> TireaResult<()> {
244    if path.is_empty() {
245        *doc = Value::Null;
246        return Ok(());
247    }
248
249    // Delete is a no-op if path doesn't exist
250    let _ = delete_at_path(doc, path.segments());
251    Ok(())
252}
253
254/// Try to delete a value at a path. Returns true if deleted, false if not found.
255fn delete_at_path(current: &mut Value, segments: &[Seg]) -> bool {
256    match segments {
257        [] => false,
258        [Seg::Key(key)] => {
259            if let Some(obj) = current.as_object_mut() {
260                obj.remove(key).is_some()
261            } else {
262                false
263            }
264        }
265        [Seg::Index(idx)] => {
266            if let Some(arr) = current.as_array_mut() {
267                if *idx < arr.len() {
268                    arr.remove(*idx);
269                    true
270                } else {
271                    false
272                }
273            } else {
274                false
275            }
276        }
277        [Seg::Key(key), rest @ ..] => {
278            if let Some(obj) = current.as_object_mut() {
279                if let Some(child) = obj.get_mut(key) {
280                    delete_at_path(child, rest)
281                } else {
282                    false
283                }
284            } else {
285                false
286            }
287        }
288        [Seg::Index(idx), rest @ ..] => {
289            if let Some(arr) = current.as_array_mut() {
290                if let Some(child) = arr.get_mut(*idx) {
291                    delete_at_path(child, rest)
292                } else {
293                    false
294                }
295            } else {
296                false
297            }
298        }
299    }
300}
301
302/// Apply an Append operation.
303fn apply_append(doc: &mut Value, path: &Path, value: Value) -> TireaResult<()> {
304    let target = get_or_create_at_path(doc, path, 0, || Value::Array(vec![]))?;
305
306    match target {
307        Value::Array(arr) => {
308            arr.push(value);
309            Ok(())
310        }
311        _ => Err(TireaError::append_requires_array(path.clone())),
312    }
313}
314
315/// Apply a MergeObject operation.
316fn apply_merge_object(doc: &mut Value, path: &Path, value: &Value) -> TireaResult<()> {
317    let merge_value = value
318        .as_object()
319        .ok_or_else(|| TireaError::merge_requires_object(path.clone()))?;
320
321    let target = get_or_create_at_path(doc, path, 0, || Value::Object(Map::new()))?;
322
323    match target {
324        Value::Object(obj) => {
325            for (k, v) in merge_value {
326                obj.insert(k.clone(), v.clone());
327            }
328            Ok(())
329        }
330        _ => Err(TireaError::merge_requires_object(path.clone())),
331    }
332}
333
334/// Apply an Increment operation.
335fn apply_increment(doc: &mut Value, path: &Path, amount: &Number) -> TireaResult<()> {
336    let target = get_at_path_mut(doc, path.segments())
337        .ok_or_else(|| TireaError::path_not_found(path.clone()))?;
338
339    match target {
340        Value::Number(n) => {
341            let result = if let Some(i) = n.as_i64() {
342                match amount {
343                    Number::Int(a) => {
344                        let value = i.checked_add(*a).ok_or_else(|| {
345                            TireaError::invalid_operation(format!(
346                                "increment overflow at {path}: {i} + {a}"
347                            ))
348                        })?;
349                        Value::Number(value.into())
350                    }
351                    Number::Float(a) => {
352                        Value::Number(finite_number_from_f64(path, i as f64 + a, "increment")?)
353                    }
354                }
355            } else if let Some(f) = n.as_f64() {
356                Value::Number(finite_number_from_f64(
357                    path,
358                    f + amount.as_f64(),
359                    "increment",
360                )?)
361            } else {
362                return Err(TireaError::numeric_on_non_number(path.clone()));
363            };
364            *target = result;
365            Ok(())
366        }
367        _ => Err(TireaError::numeric_on_non_number(path.clone())),
368    }
369}
370
371/// Apply a Decrement operation.
372fn apply_decrement(doc: &mut Value, path: &Path, amount: &Number) -> TireaResult<()> {
373    let target = get_at_path_mut(doc, path.segments())
374        .ok_or_else(|| TireaError::path_not_found(path.clone()))?;
375
376    match target {
377        Value::Number(n) => {
378            let result = if let Some(i) = n.as_i64() {
379                match amount {
380                    Number::Int(a) => {
381                        let value = i.checked_sub(*a).ok_or_else(|| {
382                            TireaError::invalid_operation(format!(
383                                "decrement overflow at {path}: {i} - {a}"
384                            ))
385                        })?;
386                        Value::Number(value.into())
387                    }
388                    Number::Float(a) => {
389                        Value::Number(finite_number_from_f64(path, i as f64 - a, "decrement")?)
390                    }
391                }
392            } else if let Some(f) = n.as_f64() {
393                Value::Number(finite_number_from_f64(
394                    path,
395                    f - amount.as_f64(),
396                    "decrement",
397                )?)
398            } else {
399                return Err(TireaError::numeric_on_non_number(path.clone()));
400            };
401            *target = result;
402            Ok(())
403        }
404        _ => Err(TireaError::numeric_on_non_number(path.clone())),
405    }
406}
407
408/// Apply an Insert operation.
409fn apply_insert(doc: &mut Value, path: &Path, index: usize, value: Value) -> TireaResult<()> {
410    let target = get_at_path_mut(doc, path.segments())
411        .ok_or_else(|| TireaError::path_not_found(path.clone()))?;
412
413    match target {
414        Value::Array(arr) => {
415            if index > arr.len() {
416                return Err(TireaError::index_out_of_bounds(
417                    path.clone(),
418                    index,
419                    arr.len(),
420                ));
421            }
422            arr.insert(index, value);
423            Ok(())
424        }
425        _ => Err(TireaError::type_mismatch(
426            path.clone(),
427            "array",
428            value_type_name(target),
429        )),
430    }
431}
432
433fn finite_number_from_f64(path: &Path, value: f64, op: &str) -> TireaResult<serde_json::Number> {
434    if !value.is_finite() {
435        return Err(TireaError::invalid_operation(format!(
436            "{op} produced non-finite value at {path}"
437        )));
438    }
439
440    serde_json::Number::from_f64(value).ok_or_else(|| {
441        TireaError::invalid_operation(format!("{op} produced non-representable value at {path}"))
442    })
443}
444
445/// Apply a Remove operation (removes first occurrence).
446fn apply_remove(doc: &mut Value, path: &Path, value: &Value) -> TireaResult<()> {
447    let target = get_at_path_mut(doc, path.segments())
448        .ok_or_else(|| TireaError::path_not_found(path.clone()))?;
449
450    match target {
451        Value::Array(arr) => {
452            if let Some(pos) = arr.iter().position(|v| v == value) {
453                arr.remove(pos);
454            }
455            Ok(())
456        }
457        _ => Err(TireaError::type_mismatch(
458            path.clone(),
459            "array",
460            value_type_name(target),
461        )),
462    }
463}
464
465/// Get a mutable reference to a value at a path.
466fn get_at_path_mut<'a>(current: &'a mut Value, segments: &[Seg]) -> Option<&'a mut Value> {
467    match segments {
468        [] => Some(current),
469        [Seg::Key(key), rest @ ..] => {
470            let obj = current.as_object_mut()?;
471            let child = obj.get_mut(key)?;
472            get_at_path_mut(child, rest)
473        }
474        [Seg::Index(idx), rest @ ..] => {
475            let arr = current.as_array_mut()?;
476            let child = arr.get_mut(*idx)?;
477            get_at_path_mut(child, rest)
478        }
479    }
480}
481
482/// Get or create a value at a path.
483fn get_or_create_at_path<'a, F>(
484    current: &'a mut Value,
485    full_path: &Path,
486    consumed: usize,
487    default: F,
488) -> TireaResult<&'a mut Value>
489where
490    F: Fn() -> Value,
491{
492    let segments = &full_path.segments()[consumed..];
493    match segments {
494        [] => {
495            if current.is_null() {
496                *current = default();
497            }
498            Ok(current)
499        }
500        [Seg::Key(key), ..] => {
501            if !current.is_object() {
502                *current = Value::Object(Map::new());
503            }
504
505            let obj = current.as_object_mut().unwrap();
506            let entry = obj.entry(key.clone()).or_insert(Value::Null);
507            get_or_create_at_path(entry, full_path, consumed + 1, default)
508        }
509        [Seg::Index(idx), ..] => {
510            // Build the path up to and including this segment for error reporting
511            let error_path = Path::from_segments(full_path.segments()[..=consumed].to_vec());
512
513            // Check type first to avoid borrow issues
514            if !current.is_array() {
515                return Err(TireaError::type_mismatch(
516                    error_path,
517                    "array",
518                    value_type_name(current),
519                ));
520            }
521
522            let arr = current.as_array_mut().unwrap();
523
524            if *idx >= arr.len() {
525                return Err(TireaError::index_out_of_bounds(error_path, *idx, arr.len()));
526            }
527
528            get_or_create_at_path(&mut arr[*idx], full_path, consumed + 1, default)
529        }
530    }
531}
532
533/// Get a reference to a value at a path (for reading).
534pub fn get_at_path<'a>(doc: &'a Value, path: &Path) -> Option<&'a Value> {
535    let mut current = doc;
536    for seg in path.segments() {
537        match seg {
538            Seg::Key(key) => {
539                current = current.get(key)?;
540            }
541            Seg::Index(idx) => {
542                current = current.get(idx)?;
543            }
544        }
545    }
546    Some(current)
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552    use crate::path;
553    use serde_json::json;
554
555    #[test]
556    fn test_apply_set() {
557        let doc = json!({});
558        let patch = Patch::new().with_op(Op::set(path!("name"), json!("Alice")));
559        let result = apply_patch(&doc, &patch).unwrap();
560        assert_eq!(result["name"], "Alice");
561    }
562
563    #[test]
564    fn test_apply_set_creates_intermediate_objects() {
565        let doc = json!({});
566        let patch = Patch::new().with_op(Op::set(path!("a", "b", "c"), json!(42)));
567        let result = apply_patch(&doc, &patch).unwrap();
568        assert_eq!(result["a"]["b"]["c"], 42);
569    }
570
571    #[test]
572    fn test_apply_set_array_oob() {
573        let doc = json!({"arr": [1, 2, 3]});
574        let patch = Patch::new().with_op(Op::set(path!("arr", 10), json!(42)));
575        let result = apply_patch(&doc, &patch);
576        assert!(matches!(result, Err(TireaError::IndexOutOfBounds { .. })));
577    }
578
579    #[test]
580    fn test_apply_delete_noop() {
581        let doc = json!({"x": 1});
582        let patch = Patch::new().with_op(Op::delete(path!("nonexistent")));
583        let result = apply_patch(&doc, &patch).unwrap();
584        assert_eq!(result, json!({"x": 1}));
585    }
586
587    #[test]
588    fn test_apply_delete_existing() {
589        let doc = json!({"x": 1, "y": 2});
590        let patch = Patch::new().with_op(Op::delete(path!("x")));
591        let result = apply_patch(&doc, &patch).unwrap();
592        assert_eq!(result, json!({"y": 2}));
593    }
594
595    #[test]
596    fn test_apply_append() {
597        let doc = json!({"items": [1, 2]});
598        let patch = Patch::new().with_op(Op::append(path!("items"), json!(3)));
599        let result = apply_patch(&doc, &patch).unwrap();
600        assert_eq!(result["items"], json!([1, 2, 3]));
601    }
602
603    #[test]
604    fn test_apply_append_creates_array() {
605        let doc = json!({});
606        let patch = Patch::new().with_op(Op::append(path!("items"), json!(1)));
607        let result = apply_patch(&doc, &patch).unwrap();
608        assert_eq!(result["items"], json!([1]));
609    }
610
611    #[test]
612    fn test_apply_merge_object() {
613        let doc = json!({"user": {"name": "Alice"}});
614        let patch = Patch::new().with_op(Op::merge_object(
615            path!("user"),
616            json!({"age": 30, "email": "alice@example.com"}),
617        ));
618        let result = apply_patch(&doc, &patch).unwrap();
619        assert_eq!(result["user"]["name"], "Alice");
620        assert_eq!(result["user"]["age"], 30);
621        assert_eq!(result["user"]["email"], "alice@example.com");
622    }
623
624    #[test]
625    fn test_apply_increment() {
626        let doc = json!({"count": 5});
627        let patch = Patch::new().with_op(Op::increment(path!("count"), 3i64));
628        let result = apply_patch(&doc, &patch).unwrap();
629        assert_eq!(result["count"], 8);
630    }
631
632    #[test]
633    fn test_apply_decrement() {
634        let doc = json!({"count": 10});
635        let patch = Patch::new().with_op(Op::decrement(path!("count"), 3i64));
636        let result = apply_patch(&doc, &patch).unwrap();
637        assert_eq!(result["count"], 7);
638    }
639
640    #[test]
641    fn test_apply_increment_nan_amount_returns_error() {
642        let doc = json!({"count": 5});
643        let patch = Patch::new().with_op(Op::increment(path!("count"), f64::NAN));
644        let result = apply_patch(&doc, &patch);
645        assert!(
646            matches!(result, Err(TireaError::InvalidOperation { .. })),
647            "expected InvalidOperation, got: {result:?}"
648        );
649    }
650
651    #[test]
652    fn test_apply_decrement_infinite_amount_returns_error() {
653        let doc = json!({"count": 5});
654        let patch = Patch::new().with_op(Op::decrement(path!("count"), f64::INFINITY));
655        let result = apply_patch(&doc, &patch);
656        assert!(
657            matches!(result, Err(TireaError::InvalidOperation { .. })),
658            "expected InvalidOperation, got: {result:?}"
659        );
660    }
661
662    #[test]
663    fn test_apply_insert() {
664        let doc = json!({"arr": [1, 2, 3]});
665        let patch = Patch::new().with_op(Op::insert(path!("arr"), 1, json!(99)));
666        let result = apply_patch(&doc, &patch).unwrap();
667        assert_eq!(result["arr"], json!([1, 99, 2, 3]));
668    }
669
670    #[test]
671    fn test_apply_remove() {
672        let doc = json!({"arr": [1, 2, 3, 2]});
673        let patch = Patch::new().with_op(Op::remove(path!("arr"), json!(2)));
674        let result = apply_patch(&doc, &patch).unwrap();
675        // Removes first occurrence only
676        assert_eq!(result["arr"], json!([1, 3, 2]));
677    }
678
679    #[test]
680    fn test_apply_is_pure() {
681        let doc = json!({"x": 1});
682        let patch = Patch::new().with_op(Op::set(path!("x"), json!(2)));
683
684        let _ = apply_patch(&doc, &patch).unwrap();
685
686        // Original unchanged
687        assert_eq!(doc["x"], 1);
688    }
689
690    #[test]
691    fn test_get_at_path() {
692        let doc = json!({"a": {"b": {"c": 42}}});
693        let value = get_at_path(&doc, &path!("a", "b", "c"));
694        assert_eq!(value, Some(&json!(42)));
695
696        let missing = get_at_path(&doc, &path!("a", "x"));
697        assert_eq!(missing, None);
698    }
699}