1use crate::{
7 error::{value_type_name, TireaError, TireaResult},
8 lattice::LatticeRegistry,
9 Number, Op, Patch, Path, Seg,
10};
11use serde_json::{Map, Value};
12
13pub 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
51pub(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
59pub 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
99pub 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
115pub 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
129pub(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
141fn 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 apply_set(doc, path, value.clone())
152 }
153 }
154 _ => apply_op(doc, op),
155 }
156}
157
158pub(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 Op::LatticeMerge { path, value } => apply_set(doc, path, value.clone()),
171 }
172}
173
174fn 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
184fn 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 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 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
242fn apply_delete(doc: &mut Value, path: &Path) -> TireaResult<()> {
244 if path.is_empty() {
245 *doc = Value::Null;
246 return Ok(());
247 }
248
249 let _ = delete_at_path(doc, path.segments());
251 Ok(())
252}
253
254fn 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
302fn 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
315fn 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
334fn 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
371fn 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
408fn 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
445fn 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
465fn 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
482fn 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 let error_path = Path::from_segments(full_path.segments()[..=consumed].to_vec());
512
513 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
533pub 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 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 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}