Skip to main content

fionn_core/
patchable.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Format-agnostic patch application
3//!
4//! This module provides the [`Patchable`] trait for applying patches to values.
5//! It extends [`DiffableValue`] with mutation capabilities needed for patch
6//! application.
7//!
8//! # Key Types
9//!
10//! - [`Patchable`] - Trait for values that can have patches applied
11//! - [`PatchError`] - Errors that can occur during patch application
12//!
13//! # Example
14//!
15//! ```ignore
16//! use fionn_core::patchable::{Patchable, apply_patch};
17//! use fionn_core::diffable::{GenericPatch, GenericPatchOperation};
18//!
19//! let mut target = serde_json::json!({"name": "Alice"});
20//! let patch = GenericPatch::with_operations(vec![
21//!     GenericPatchOperation::Replace {
22//!         path: "/name".to_string(),
23//!         value: serde_json::json!("Bob"),
24//!     },
25//! ]);
26//!
27//! apply_patch(&mut target, &patch)?;
28//! assert_eq!(target["name"], "Bob");
29//! ```
30
31use crate::diffable::{DiffableValue, GenericPatch, GenericPatchOperation};
32use thiserror::Error;
33
34// ============================================================================
35// PatchError
36// ============================================================================
37
38/// Errors that can occur during patch application
39#[derive(Error, Debug, Clone, PartialEq, Eq)]
40pub enum PatchError {
41    /// The target path does not exist
42    #[error("Path not found: {0}")]
43    PathNotFound(String),
44
45    /// The target at the path is not the expected type
46    #[error("Invalid target at {path}: expected {expected}, found {found}")]
47    InvalidTarget {
48        /// The path that failed
49        path: String,
50        /// What was expected
51        expected: String,
52        /// What was found
53        found: String,
54    },
55
56    /// Array index out of bounds
57    #[error("Index {index} out of bounds at {path} (length {len})")]
58    IndexOutOfBounds {
59        /// The path containing the array
60        path: String,
61        /// The requested index
62        index: usize,
63        /// The array length
64        len: usize,
65    },
66
67    /// Test operation failed
68    #[error("Test failed at {path}: values do not match")]
69    TestFailed {
70        /// The path that was tested
71        path: String,
72    },
73
74    /// Cannot remove the root element
75    #[error("Cannot remove the root element")]
76    CannotRemoveRoot,
77
78    /// Invalid path syntax
79    #[error("Invalid path syntax: {0}")]
80    InvalidPath(String),
81
82    /// Move/copy source not found
83    #[error("Source path not found: {0}")]
84    SourceNotFound(String),
85}
86
87// ============================================================================
88// Patchable Trait
89// ============================================================================
90
91/// Trait for values that can have patches applied
92///
93/// This trait extends [`DiffableValue`] with mutation capabilities needed
94/// for applying patch operations like add, remove, replace, move, and copy.
95///
96/// # Implementation Notes
97///
98/// - Navigation methods should handle JSON Pointer syntax (RFC 6901)
99/// - Mutations should be atomic when possible
100/// - Test operations should not modify the value
101pub trait Patchable: DiffableValue + Sized {
102    /// Apply a single patch operation
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if the operation cannot be applied.
107    fn apply_operation(&mut self, op: &GenericPatchOperation<Self>) -> Result<(), PatchError>;
108
109    /// Navigate to a path and return a mutable reference
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if the path doesn't exist or is invalid.
114    fn get_mut_at_path(&mut self, path: &str) -> Result<&mut Self, PatchError>;
115
116    /// Navigate to a path and return an immutable reference
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if the path doesn't exist or is invalid.
121    fn get_at_path(&self, path: &str) -> Result<&Self, PatchError>;
122
123    /// Set a value at a path (creating intermediate containers as needed)
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if the path is invalid.
128    fn set_at_path(&mut self, path: &str, value: Self) -> Result<(), PatchError>;
129
130    /// Remove a value at a path
131    ///
132    /// # Errors
133    ///
134    /// Returns an error if the path doesn't exist.
135    fn remove_at_path(&mut self, path: &str) -> Result<Self, PatchError>;
136
137    /// Apply an entire patch
138    ///
139    /// Operations are applied in order. If any operation fails, the
140    /// value may be in a partially modified state.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if any operation fails.
145    fn apply_patch(&mut self, patch: &GenericPatch<Self>) -> Result<(), PatchError> {
146        for op in &patch.operations {
147            self.apply_operation(op)?;
148        }
149        Ok(())
150    }
151}
152
153// ============================================================================
154// JSON Pointer Parsing
155// ============================================================================
156
157/// Parse a JSON Pointer path into segments
158///
159/// # Errors
160///
161/// Returns an error if the path is invalid.
162pub fn parse_pointer(path: &str) -> Result<Vec<String>, PatchError> {
163    if path.is_empty() {
164        return Ok(vec![]);
165    }
166
167    if !path.starts_with('/') {
168        return Err(PatchError::InvalidPath(format!(
169            "JSON Pointer must start with '/': {path}"
170        )));
171    }
172
173    Ok(path[1..].split('/').map(unescape_json_pointer).collect())
174}
175
176/// Unescape JSON Pointer segment
177fn unescape_json_pointer(s: &str) -> String {
178    s.replace("~1", "/").replace("~0", "~")
179}
180
181/// Get the parent path and final segment
182fn split_parent_key(path: &str) -> Result<(String, String), PatchError> {
183    let segments = parse_pointer(path)?;
184    if segments.is_empty() {
185        return Err(PatchError::CannotRemoveRoot);
186    }
187
188    let key = segments.last().unwrap().clone();
189    let parent_path = if segments.len() == 1 {
190        String::new()
191    } else {
192        format!(
193            "/{}",
194            segments[..segments.len() - 1]
195                .iter()
196                .map(|s| escape_json_pointer(s))
197                .collect::<Vec<_>>()
198                .join("/")
199        )
200    };
201
202    Ok((parent_path, key))
203}
204
205fn escape_json_pointer(s: &str) -> String {
206    s.replace('~', "~0").replace('/', "~1")
207}
208
209// ============================================================================
210// Patchable for serde_json::Value
211// ============================================================================
212
213/// Helper: insert value into array at key position
214fn insert_into_array(
215    arr: &mut Vec<serde_json::Value>,
216    key: &str,
217    value: &serde_json::Value,
218    path: &str,
219) -> Result<(), PatchError> {
220    if key == "-" {
221        arr.push(value.clone());
222    } else {
223        let index: usize = key
224            .parse()
225            .map_err(|_| PatchError::InvalidPath(format!("Invalid array index: {key}")))?;
226        if index > arr.len() {
227            return Err(PatchError::IndexOutOfBounds {
228                path: path.to_string(),
229                index,
230                len: arr.len(),
231            });
232        }
233        arr.insert(index, value.clone());
234    }
235    Ok(())
236}
237
238/// Helper: remove from array at key position
239fn remove_from_array(
240    arr: &mut Vec<serde_json::Value>,
241    key: &str,
242    path: &str,
243) -> Result<(), PatchError> {
244    let index: usize = key
245        .parse()
246        .map_err(|_| PatchError::InvalidPath(format!("Invalid array index: {key}")))?;
247    if index >= arr.len() {
248        return Err(PatchError::IndexOutOfBounds {
249            path: path.to_string(),
250            index,
251            len: arr.len(),
252        });
253    }
254    arr.remove(index);
255    Ok(())
256}
257
258/// Helper: apply Add operation
259fn apply_add(
260    doc: &mut serde_json::Value,
261    path: &str,
262    value: &serde_json::Value,
263) -> Result<(), PatchError> {
264    use crate::diffable::DiffableValue;
265
266    if path.is_empty() {
267        *doc = value.clone();
268        return Ok(());
269    }
270
271    let (parent_path, key) = split_parent_key(path)?;
272    let parent = if parent_path.is_empty() {
273        doc
274    } else {
275        <serde_json::Value as Patchable>::get_mut_at_path(doc, &parent_path)?
276    };
277
278    match parent {
279        serde_json::Value::Object(map) => {
280            map.insert(key, value.clone());
281            Ok(())
282        }
283        serde_json::Value::Array(arr) => insert_into_array(arr, &key, value, path),
284        _ => Err(PatchError::InvalidTarget {
285            path: parent_path,
286            expected: "object or array".to_string(),
287            found: format!("{:?}", parent.value_kind()),
288        }),
289    }
290}
291
292/// Helper: apply Remove operation
293fn apply_remove(doc: &mut serde_json::Value, path: &str) -> Result<(), PatchError> {
294    use crate::diffable::DiffableValue;
295
296    if path.is_empty() {
297        return Err(PatchError::CannotRemoveRoot);
298    }
299
300    let (parent_path, key) = split_parent_key(path)?;
301    let parent = if parent_path.is_empty() {
302        doc
303    } else {
304        <serde_json::Value as Patchable>::get_mut_at_path(doc, &parent_path)?
305    };
306
307    match parent {
308        serde_json::Value::Object(map) => {
309            map.remove(&key)
310                .ok_or_else(|| PatchError::PathNotFound(path.to_string()))?;
311            Ok(())
312        }
313        serde_json::Value::Array(arr) => remove_from_array(arr, &key, path),
314        _ => Err(PatchError::InvalidTarget {
315            path: parent_path,
316            expected: "object or array".to_string(),
317            found: format!("{:?}", parent.value_kind()),
318        }),
319    }
320}
321
322/// Helper: apply Replace operation
323fn apply_replace(
324    doc: &mut serde_json::Value,
325    path: &str,
326    value: &serde_json::Value,
327) -> Result<(), PatchError> {
328    if path.is_empty() {
329        *doc = value.clone();
330        return Ok(());
331    }
332
333    let target = <serde_json::Value as Patchable>::get_mut_at_path(doc, path)?;
334    *target = value.clone();
335    Ok(())
336}
337
338impl Patchable for serde_json::Value {
339    fn apply_operation(&mut self, op: &GenericPatchOperation<Self>) -> Result<(), PatchError> {
340        match op {
341            GenericPatchOperation::Add { path, value } => apply_add(self, path, value),
342            GenericPatchOperation::Remove { path } => apply_remove(self, path),
343            GenericPatchOperation::Replace { path, value } => apply_replace(self, path, value),
344            GenericPatchOperation::Move { from, path } => {
345                let val = self.remove_at_path(from)?;
346                self.set_at_path(path, val)
347            }
348            GenericPatchOperation::Copy { from, path } => {
349                let val = self.get_at_path(from)?.clone();
350                self.set_at_path(path, val)
351            }
352            GenericPatchOperation::Test { path, value } => {
353                let actual = self.get_at_path(path)?;
354                if actual == value {
355                    Ok(())
356                } else {
357                    Err(PatchError::TestFailed { path: path.clone() })
358                }
359            }
360        }
361    }
362
363    fn get_mut_at_path(&mut self, path: &str) -> Result<&mut Self, PatchError> {
364        let segments = parse_pointer(path)?;
365        let mut current = self;
366
367        for (i, segment) in segments.iter().enumerate() {
368            let path_so_far = format!(
369                "/{}",
370                segments[..=i]
371                    .iter()
372                    .map(|s| escape_json_pointer(s))
373                    .collect::<Vec<_>>()
374                    .join("/")
375            );
376
377            current = match current {
378                Self::Object(map) => map
379                    .get_mut(segment)
380                    .ok_or(PatchError::PathNotFound(path_so_far))?,
381                Self::Array(arr) => {
382                    let index: usize = segment.parse().map_err(|_| {
383                        PatchError::InvalidPath(format!("Invalid array index: {segment}"))
384                    })?;
385                    let arr_len = arr.len();
386                    arr.get_mut(index).ok_or(PatchError::IndexOutOfBounds {
387                        path: path_so_far,
388                        index,
389                        len: arr_len,
390                    })?
391                }
392                _ => {
393                    return Err(PatchError::InvalidTarget {
394                        path: path_so_far,
395                        expected: "object or array".to_string(),
396                        found: format!("{:?}", current.value_kind()),
397                    });
398                }
399            };
400        }
401
402        Ok(current)
403    }
404
405    fn get_at_path(&self, path: &str) -> Result<&Self, PatchError> {
406        let segments = parse_pointer(path)?;
407        let mut current = self;
408
409        for (i, segment) in segments.iter().enumerate() {
410            let path_so_far = format!(
411                "/{}",
412                segments[..=i]
413                    .iter()
414                    .map(|s| escape_json_pointer(s))
415                    .collect::<Vec<_>>()
416                    .join("/")
417            );
418
419            current = match current {
420                Self::Object(map) => map
421                    .get(segment)
422                    .ok_or(PatchError::PathNotFound(path_so_far))?,
423                Self::Array(arr) => {
424                    let index: usize = segment.parse().map_err(|_| {
425                        PatchError::InvalidPath(format!("Invalid array index: {segment}"))
426                    })?;
427                    arr.get(index).ok_or(PatchError::IndexOutOfBounds {
428                        path: path_so_far,
429                        index,
430                        len: arr.len(),
431                    })?
432                }
433                _ => {
434                    return Err(PatchError::InvalidTarget {
435                        path: path_so_far,
436                        expected: "object or array".to_string(),
437                        found: format!("{:?}", current.value_kind()),
438                    });
439                }
440            };
441        }
442
443        Ok(current)
444    }
445
446    fn set_at_path(&mut self, path: &str, value: Self) -> Result<(), PatchError> {
447        if path.is_empty() {
448            *self = value;
449            return Ok(());
450        }
451
452        let (parent_path, key) = split_parent_key(path)?;
453        let parent = if parent_path.is_empty() {
454            self
455        } else {
456            self.get_mut_at_path(&parent_path)?
457        };
458
459        match parent {
460            Self::Object(map) => {
461                map.insert(key, value);
462                Ok(())
463            }
464            Self::Array(arr) => {
465                let index: usize = key
466                    .parse()
467                    .map_err(|_| PatchError::InvalidPath(format!("Invalid array index: {key}")))?;
468                while arr.len() <= index {
469                    arr.push(Self::Null);
470                }
471                arr[index] = value;
472                Ok(())
473            }
474            _ => Err(PatchError::InvalidTarget {
475                path: parent_path,
476                expected: "object or array".to_string(),
477                found: format!("{:?}", parent.value_kind()),
478            }),
479        }
480    }
481
482    fn remove_at_path(&mut self, path: &str) -> Result<Self, PatchError> {
483        if path.is_empty() {
484            return Err(PatchError::CannotRemoveRoot);
485        }
486
487        let (parent_path, key) = split_parent_key(path)?;
488        let parent = if parent_path.is_empty() {
489            self
490        } else {
491            self.get_mut_at_path(&parent_path)?
492        };
493
494        match parent {
495            Self::Object(map) => map
496                .remove(&key)
497                .ok_or_else(|| PatchError::PathNotFound(path.to_string())),
498            Self::Array(arr) => {
499                let index: usize = key
500                    .parse()
501                    .map_err(|_| PatchError::InvalidPath(format!("Invalid array index: {key}")))?;
502                if index >= arr.len() {
503                    return Err(PatchError::IndexOutOfBounds {
504                        path: path.to_string(),
505                        index,
506                        len: arr.len(),
507                    });
508                }
509                Ok(arr.remove(index))
510            }
511            _ => Err(PatchError::InvalidTarget {
512                path: parent_path,
513                expected: "object or array".to_string(),
514                found: format!("{:?}", parent.value_kind()),
515            }),
516        }
517    }
518}
519
520// ============================================================================
521// Helper Functions
522// ============================================================================
523
524/// Apply a patch to a value
525///
526/// Convenience function that calls `value.apply_patch(patch)`.
527///
528/// # Errors
529///
530/// Returns an error if any operation fails.
531pub fn apply_patch<V: Patchable>(value: &mut V, patch: &GenericPatch<V>) -> Result<(), PatchError> {
532    value.apply_patch(patch)
533}
534
535/// Apply a single operation to a value
536///
537/// Convenience function that calls `value.apply_operation(op)`.
538///
539/// # Errors
540///
541/// Returns an error if the operation fails.
542pub fn apply_operation<V: Patchable>(
543    value: &mut V,
544    op: &GenericPatchOperation<V>,
545) -> Result<(), PatchError> {
546    value.apply_operation(op)
547}
548
549// ============================================================================
550// Tests
551// ============================================================================
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556    use serde_json::json;
557
558    #[test]
559    fn test_parse_pointer_empty() {
560        let segments = parse_pointer("").unwrap();
561        assert!(segments.is_empty());
562    }
563
564    #[test]
565    fn test_parse_pointer_root() {
566        let segments = parse_pointer("/").unwrap();
567        assert_eq!(segments, vec![""]);
568    }
569
570    #[test]
571    fn test_parse_pointer_simple() {
572        let segments = parse_pointer("/foo/bar").unwrap();
573        assert_eq!(segments, vec!["foo", "bar"]);
574    }
575
576    #[test]
577    fn test_parse_pointer_escaped() {
578        let segments = parse_pointer("/a~1b/c~0d").unwrap();
579        assert_eq!(segments, vec!["a/b", "c~d"]);
580    }
581
582    #[test]
583    fn test_parse_pointer_invalid() {
584        assert!(parse_pointer("no-leading-slash").is_err());
585    }
586
587    #[test]
588    fn test_get_at_path_simple() {
589        let value = json!({"name": "Alice"});
590        let name = value.get_at_path("/name").unwrap();
591        assert_eq!(name, &json!("Alice"));
592    }
593
594    #[test]
595    fn test_get_at_path_nested() {
596        let value = json!({"user": {"name": "Alice"}});
597        let name = value.get_at_path("/user/name").unwrap();
598        assert_eq!(name, &json!("Alice"));
599    }
600
601    #[test]
602    fn test_get_at_path_array() {
603        let value = json!([1, 2, 3]);
604        assert_eq!(value.get_at_path("/0").unwrap(), &json!(1));
605        assert_eq!(value.get_at_path("/2").unwrap(), &json!(3));
606    }
607
608    #[test]
609    fn test_get_at_path_not_found() {
610        let value = json!({"name": "Alice"});
611        assert!(matches!(
612            value.get_at_path("/age"),
613            Err(PatchError::PathNotFound(_))
614        ));
615    }
616
617    #[test]
618    fn test_apply_add_to_object() {
619        let mut value = json!({"name": "Alice"});
620        let op = GenericPatchOperation::Add {
621            path: "/age".to_string(),
622            value: json!(30),
623        };
624
625        value.apply_operation(&op).unwrap();
626        assert_eq!(value["age"], json!(30));
627    }
628
629    #[test]
630    fn test_apply_add_to_array() {
631        let mut value = json!([1, 2]);
632        let op = GenericPatchOperation::Add {
633            path: "/-".to_string(),
634            value: json!(3),
635        };
636
637        value.apply_operation(&op).unwrap();
638        assert_eq!(value, json!([1, 2, 3]));
639    }
640
641    #[test]
642    fn test_apply_add_to_array_at_index() {
643        let mut value = json!([1, 3]);
644        let op = GenericPatchOperation::Add {
645            path: "/1".to_string(),
646            value: json!(2),
647        };
648
649        value.apply_operation(&op).unwrap();
650        assert_eq!(value, json!([1, 2, 3]));
651    }
652
653    #[test]
654    fn test_apply_remove_from_object() {
655        let mut value = json!({"name": "Alice", "age": 30});
656        let op = GenericPatchOperation::Remove {
657            path: "/age".to_string(),
658        };
659
660        value.apply_operation(&op).unwrap();
661        assert_eq!(value, json!({"name": "Alice"}));
662    }
663
664    #[test]
665    fn test_apply_remove_from_array() {
666        let mut value = json!([1, 2, 3]);
667        let op = GenericPatchOperation::Remove {
668            path: "/1".to_string(),
669        };
670
671        value.apply_operation(&op).unwrap();
672        assert_eq!(value, json!([1, 3]));
673    }
674
675    #[test]
676    fn test_apply_replace() {
677        let mut value = json!({"name": "Alice"});
678        let op = GenericPatchOperation::Replace {
679            path: "/name".to_string(),
680            value: json!("Bob"),
681        };
682
683        value.apply_operation(&op).unwrap();
684        assert_eq!(value["name"], json!("Bob"));
685    }
686
687    #[test]
688    fn test_apply_replace_root() {
689        let mut value = json!({"old": "data"});
690        let op = GenericPatchOperation::Replace {
691            path: String::new(),
692            value: json!({"new": "data"}),
693        };
694
695        value.apply_operation(&op).unwrap();
696        assert_eq!(value, json!({"new": "data"}));
697    }
698
699    #[test]
700    fn test_apply_move() {
701        let mut value = json!({"first": "Alice", "last": "Smith"});
702        let op = GenericPatchOperation::Move {
703            from: "/first".to_string(),
704            path: "/name".to_string(),
705        };
706
707        value.apply_operation(&op).unwrap();
708        assert!(value.get("first").is_none());
709        assert_eq!(value["name"], json!("Alice"));
710    }
711
712    #[test]
713    fn test_apply_copy() {
714        let mut value = json!({"name": "Alice"});
715        let op = GenericPatchOperation::Copy {
716            from: "/name".to_string(),
717            path: "/alias".to_string(),
718        };
719
720        value.apply_operation(&op).unwrap();
721        assert_eq!(value["name"], json!("Alice"));
722        assert_eq!(value["alias"], json!("Alice"));
723    }
724
725    #[test]
726    fn test_apply_test_success() {
727        let mut value = json!({"name": "Alice"});
728        let op = GenericPatchOperation::Test {
729            path: "/name".to_string(),
730            value: json!("Alice"),
731        };
732
733        assert!(value.apply_operation(&op).is_ok());
734    }
735
736    #[test]
737    fn test_apply_test_failure() {
738        let mut value = json!({"name": "Alice"});
739        let op = GenericPatchOperation::Test {
740            path: "/name".to_string(),
741            value: json!("Bob"),
742        };
743
744        assert!(matches!(
745            value.apply_operation(&op),
746            Err(PatchError::TestFailed { .. })
747        ));
748    }
749
750    #[test]
751    fn test_apply_patch() {
752        let mut value = json!({"name": "Alice"});
753        let patch = GenericPatch::with_operations(vec![
754            GenericPatchOperation::Add {
755                path: "/age".to_string(),
756                value: json!(30),
757            },
758            GenericPatchOperation::Replace {
759                path: "/name".to_string(),
760                value: json!("Alice Smith"),
761            },
762        ]);
763
764        apply_patch(&mut value, &patch).unwrap();
765        assert_eq!(value["name"], json!("Alice Smith"));
766        assert_eq!(value["age"], json!(30));
767    }
768
769    #[test]
770    fn test_remove_root_error() {
771        let mut value = json!({"name": "Alice"});
772        let op: GenericPatchOperation<serde_json::Value> = GenericPatchOperation::Remove {
773            path: String::new(),
774        };
775
776        assert!(matches!(
777            value.apply_operation(&op),
778            Err(PatchError::CannotRemoveRoot)
779        ));
780    }
781
782    #[test]
783    fn test_set_at_path() {
784        let mut value = json!({"user": {}});
785        value.set_at_path("/user/name", json!("Alice")).unwrap();
786        assert_eq!(value["user"]["name"], json!("Alice"));
787    }
788
789    #[test]
790    fn test_remove_at_path() {
791        let mut value = json!({"name": "Alice", "age": 30});
792        let removed = value.remove_at_path("/age").unwrap();
793        assert_eq!(removed, json!(30));
794        assert!(value.get("age").is_none());
795    }
796}