Skip to main content

syncable_ag_ui_core/
patch.rs

1//! JSON Patch utilities for AG-UI state delta generation.
2//!
3//! This module provides utilities for working with JSON Patch (RFC 6902)
4//! operations, enabling efficient state synchronization between agents and
5//! frontends through delta updates.
6//!
7//! # Overview
8//!
9//! JSON Patch is a format for describing changes to a JSON document. Instead
10//! of sending the entire state on every update, you can send just the changes
11//! (patches) which is more efficient for large state objects.
12//!
13//! # Example
14//!
15//! ```rust
16//! use ag_ui_core::patch::{create_patch, apply_patch};
17//! use serde_json::json;
18//!
19//! // Create a patch from two states
20//! let old_state = json!({"count": 0, "items": []});
21//! let new_state = json!({"count": 1, "items": ["apple"]});
22//!
23//! let patch = create_patch(&old_state, &new_state);
24//!
25//! // Apply patch to recreate the new state
26//! let mut state = old_state.clone();
27//! apply_patch(&mut state, &patch).unwrap();
28//! assert_eq!(state, new_state);
29//! ```
30
31use serde_json::Value as JsonValue;
32use std::error::Error;
33use std::fmt;
34
35// Re-export json_patch types for convenience
36pub use json_patch::{
37    AddOperation, CopyOperation, MoveOperation, Patch, PatchOperation, RemoveOperation,
38    ReplaceOperation, TestOperation,
39};
40use jsonptr::PointerBuf;
41
42/// Error type for patch operations.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct PatchError {
45    message: String,
46}
47
48impl PatchError {
49    /// Creates a new patch error with the given message.
50    pub fn new(message: impl Into<String>) -> Self {
51        Self {
52            message: message.into(),
53        }
54    }
55}
56
57impl fmt::Display for PatchError {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        write!(f, "Patch error: {}", self.message)
60    }
61}
62
63impl Error for PatchError {}
64
65impl From<json_patch::PatchError> for PatchError {
66    fn from(err: json_patch::PatchError) -> Self {
67        Self::new(format!("{}", err))
68    }
69}
70
71/// Creates a JSON Patch representing the difference between two JSON values.
72///
73/// The patch, when applied to `from`, will produce `to`.
74///
75/// # Example
76///
77/// ```rust
78/// use ag_ui_core::patch::create_patch;
79/// use serde_json::json;
80///
81/// let from = json!({"name": "Alice", "age": 30});
82/// let to = json!({"name": "Alice", "age": 31});
83///
84/// let patch = create_patch(&from, &to);
85///
86/// // The patch contains a "replace" operation for the age field
87/// assert!(!patch.0.is_empty());
88/// ```
89pub fn create_patch(from: &JsonValue, to: &JsonValue) -> Patch {
90    json_patch::diff(from, to)
91}
92
93/// Applies a JSON Patch to a JSON value in place.
94///
95/// # Errors
96///
97/// Returns an error if any patch operation fails (e.g., path doesn't exist
98/// for a remove operation, or test operation fails).
99///
100/// # Example
101///
102/// ```rust
103/// use ag_ui_core::patch::{create_patch, apply_patch};
104/// use serde_json::json;
105///
106/// let mut state = json!({"count": 0});
107/// let patch = create_patch(&json!({"count": 0}), &json!({"count": 5}));
108///
109/// apply_patch(&mut state, &patch).unwrap();
110/// assert_eq!(state["count"], 5);
111/// ```
112pub fn apply_patch(target: &mut JsonValue, patch: &Patch) -> Result<(), PatchError> {
113    json_patch::patch(target, patch.0.as_slice()).map_err(PatchError::from)
114}
115
116/// Applies a JSON Patch from a JSON array representation.
117///
118/// This is useful when you receive patches as raw JSON values (e.g., from
119/// network events).
120///
121/// # Errors
122///
123/// Returns an error if the patch is not a valid JSON Patch array or if
124/// any operation fails.
125///
126/// # Example
127///
128/// ```rust
129/// use ag_ui_core::patch::apply_patch_from_value;
130/// use serde_json::json;
131///
132/// let mut state = json!({"count": 0});
133/// let patch_json = json!([
134///     {"op": "replace", "path": "/count", "value": 10}
135/// ]);
136///
137/// apply_patch_from_value(&mut state, &patch_json).unwrap();
138/// assert_eq!(state["count"], 10);
139/// ```
140pub fn apply_patch_from_value(target: &mut JsonValue, patch: &JsonValue) -> Result<(), PatchError> {
141    let patch: Patch = serde_json::from_value(patch.clone())
142        .map_err(|e| PatchError::new(format!("Invalid patch format: {}", e)))?;
143    apply_patch(target, &patch)
144}
145
146/// Converts a Patch to a JSON value for serialization.
147///
148/// This is useful when you need to send patches over the network or
149/// store them as JSON.
150///
151/// # Example
152///
153/// ```rust
154/// use ag_ui_core::patch::{create_patch, patch_to_value};
155/// use serde_json::json;
156///
157/// let patch = create_patch(
158///     &json!({"x": 1}),
159///     &json!({"x": 2}),
160/// );
161///
162/// let json = patch_to_value(&patch);
163/// assert!(json.is_array());
164/// ```
165pub fn patch_to_value(patch: &Patch) -> JsonValue {
166    serde_json::to_value(patch).unwrap_or(JsonValue::Array(vec![]))
167}
168
169/// Converts a Patch to a vector of JSON values.
170///
171/// This is the format expected by StateDeltaEvent and ActivityDeltaEvent.
172///
173/// # Example
174///
175/// ```rust
176/// use ag_ui_core::patch::{create_patch, patch_to_vec};
177/// use serde_json::json;
178///
179/// let patch = create_patch(
180///     &json!({"items": []}),
181///     &json!({"items": ["a"]}),
182/// );
183///
184/// let ops = patch_to_vec(&patch);
185/// // Each operation is a separate JSON object
186/// assert!(!ops.is_empty());
187/// ```
188pub fn patch_to_vec(patch: &Patch) -> Vec<JsonValue> {
189    patch
190        .0
191        .iter()
192        .filter_map(|op| serde_json::to_value(op).ok())
193        .collect()
194}
195
196/// A builder for constructing JSON Patches programmatically.
197///
198/// This provides a more ergonomic way to create patches when you know
199/// exactly what operations you want to perform.
200///
201/// # Example
202///
203/// ```rust
204/// use ag_ui_core::patch::PatchBuilder;
205/// use serde_json::json;
206///
207/// let patch = PatchBuilder::new()
208///     .add("/name", json!("Alice"))
209///     .replace("/age", json!(31))
210///     .remove("/temp")
211///     .build();
212///
213/// assert_eq!(patch.0.len(), 3);
214/// ```
215#[derive(Debug, Clone, Default)]
216pub struct PatchBuilder {
217    operations: Vec<PatchOperation>,
218}
219
220impl PatchBuilder {
221    /// Creates a new empty patch builder.
222    pub fn new() -> Self {
223        Self::default()
224    }
225
226    /// Adds an "add" operation to the patch.
227    ///
228    /// The add operation adds a value at the target location. If the target
229    /// location specifies an array index, the value is inserted at that index.
230    pub fn add(mut self, path: impl AsRef<str>, value: JsonValue) -> Self {
231        self.operations.push(PatchOperation::Add(AddOperation {
232            path: PointerBuf::parse(path.as_ref()).unwrap_or_default(),
233            value,
234        }));
235        self
236    }
237
238    /// Adds a "remove" operation to the patch.
239    ///
240    /// The remove operation removes the value at the target location.
241    pub fn remove(mut self, path: impl AsRef<str>) -> Self {
242        self.operations
243            .push(PatchOperation::Remove(RemoveOperation {
244                path: PointerBuf::parse(path.as_ref()).unwrap_or_default(),
245            }));
246        self
247    }
248
249    /// Adds a "replace" operation to the patch.
250    ///
251    /// The replace operation replaces the value at the target location with
252    /// the new value.
253    pub fn replace(mut self, path: impl AsRef<str>, value: JsonValue) -> Self {
254        self.operations
255            .push(PatchOperation::Replace(ReplaceOperation {
256                path: PointerBuf::parse(path.as_ref()).unwrap_or_default(),
257                value,
258            }));
259        self
260    }
261
262    /// Adds a "move" operation to the patch.
263    ///
264    /// The move operation removes the value at a specified location and
265    /// adds it to the target location.
266    pub fn move_value(mut self, from: impl AsRef<str>, path: impl AsRef<str>) -> Self {
267        self.operations.push(PatchOperation::Move(MoveOperation {
268            from: PointerBuf::parse(from.as_ref()).unwrap_or_default(),
269            path: PointerBuf::parse(path.as_ref()).unwrap_or_default(),
270        }));
271        self
272    }
273
274    /// Adds a "copy" operation to the patch.
275    ///
276    /// The copy operation copies the value at a specified location to the
277    /// target location.
278    pub fn copy(mut self, from: impl AsRef<str>, path: impl AsRef<str>) -> Self {
279        self.operations.push(PatchOperation::Copy(CopyOperation {
280            from: PointerBuf::parse(from.as_ref()).unwrap_or_default(),
281            path: PointerBuf::parse(path.as_ref()).unwrap_or_default(),
282        }));
283        self
284    }
285
286    /// Adds a "test" operation to the patch.
287    ///
288    /// The test operation tests that a value at the target location is equal
289    /// to a specified value. If the test fails, the entire patch fails.
290    pub fn test(mut self, path: impl AsRef<str>, value: JsonValue) -> Self {
291        self.operations.push(PatchOperation::Test(TestOperation {
292            path: PointerBuf::parse(path.as_ref()).unwrap_or_default(),
293            value,
294        }));
295        self
296    }
297
298    /// Builds the patch from the accumulated operations.
299    pub fn build(self) -> Patch {
300        Patch(self.operations)
301    }
302
303    /// Builds the patch and returns it as a vector of JSON values.
304    ///
305    /// This is the format expected by StateDeltaEvent and ActivityDeltaEvent.
306    pub fn build_vec(self) -> Vec<JsonValue> {
307        patch_to_vec(&self.build())
308    }
309}
310
311/// Checks if applying a patch would succeed without actually modifying the target.
312///
313/// This is useful for validation before committing to a patch operation.
314///
315/// # Example
316///
317/// ```rust
318/// use ag_ui_core::patch::{can_apply_patch, PatchBuilder};
319/// use serde_json::json;
320///
321/// let state = json!({"count": 0});
322/// let valid_patch = PatchBuilder::new().replace("/count", json!(1)).build();
323/// let invalid_patch = PatchBuilder::new().remove("/nonexistent").build();
324///
325/// assert!(can_apply_patch(&state, &valid_patch));
326/// assert!(!can_apply_patch(&state, &invalid_patch));
327/// ```
328pub fn can_apply_patch(target: &JsonValue, patch: &Patch) -> bool {
329    let mut test_target = target.clone();
330    apply_patch(&mut test_target, patch).is_ok()
331}
332
333/// Merges two patches into one.
334///
335/// The resulting patch applies the operations from the first patch followed
336/// by operations from the second patch.
337///
338/// Note: This is a simple concatenation and does not optimize or simplify
339/// the resulting patch.
340///
341/// # Example
342///
343/// ```rust
344/// use ag_ui_core::patch::{merge_patches, PatchBuilder};
345/// use serde_json::json;
346///
347/// let patch1 = PatchBuilder::new().add("/a", json!(1)).build();
348/// let patch2 = PatchBuilder::new().add("/b", json!(2)).build();
349///
350/// let merged = merge_patches(&patch1, &patch2);
351/// assert_eq!(merged.0.len(), 2);
352/// ```
353pub fn merge_patches(first: &Patch, second: &Patch) -> Patch {
354    let mut operations = first.0.clone();
355    operations.extend(second.0.clone());
356    Patch(operations)
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use serde_json::json;
363
364    #[test]
365    fn test_create_patch_simple() {
366        let from = json!({"count": 0});
367        let to = json!({"count": 5});
368
369        let patch = create_patch(&from, &to);
370        assert!(!patch.0.is_empty());
371
372        // Apply patch and verify
373        let mut result = from.clone();
374        apply_patch(&mut result, &patch).unwrap();
375        assert_eq!(result, to);
376    }
377
378    #[test]
379    fn test_create_patch_add_field() {
380        let from = json!({"name": "Alice"});
381        let to = json!({"name": "Alice", "age": 30});
382
383        let patch = create_patch(&from, &to);
384
385        let mut result = from.clone();
386        apply_patch(&mut result, &patch).unwrap();
387        assert_eq!(result, to);
388    }
389
390    #[test]
391    fn test_create_patch_remove_field() {
392        let from = json!({"name": "Alice", "temp": "value"});
393        let to = json!({"name": "Alice"});
394
395        let patch = create_patch(&from, &to);
396
397        let mut result = from.clone();
398        apply_patch(&mut result, &patch).unwrap();
399        assert_eq!(result, to);
400    }
401
402    #[test]
403    fn test_create_patch_array_operations() {
404        let from = json!({"items": ["a", "b"]});
405        let to = json!({"items": ["a", "b", "c"]});
406
407        let patch = create_patch(&from, &to);
408
409        let mut result = from.clone();
410        apply_patch(&mut result, &patch).unwrap();
411        assert_eq!(result, to);
412    }
413
414    #[test]
415    fn test_apply_patch_from_value() {
416        let mut state = json!({"count": 0});
417        let patch_json = json!([
418            {"op": "replace", "path": "/count", "value": 42}
419        ]);
420
421        apply_patch_from_value(&mut state, &patch_json).unwrap();
422        assert_eq!(state["count"], 42);
423    }
424
425    #[test]
426    fn test_apply_patch_from_value_invalid() {
427        let mut state = json!({"count": 0});
428        let invalid_patch = json!("not an array");
429
430        let result = apply_patch_from_value(&mut state, &invalid_patch);
431        assert!(result.is_err());
432    }
433
434    #[test]
435    fn test_patch_to_value() {
436        let patch = create_patch(&json!({"x": 1}), &json!({"x": 2}));
437        let value = patch_to_value(&patch);
438
439        assert!(value.is_array());
440    }
441
442    #[test]
443    fn test_patch_to_vec() {
444        let patch = create_patch(&json!({"a": 1, "b": 2}), &json!({"a": 1, "b": 3, "c": 4}));
445        let ops = patch_to_vec(&patch);
446
447        // Should have operations for changing b and adding c
448        assert!(!ops.is_empty());
449        for op in &ops {
450            assert!(op.is_object());
451            assert!(op.get("op").is_some());
452        }
453    }
454
455    #[test]
456    fn test_patch_builder_add() {
457        let patch = PatchBuilder::new()
458            .add("/name", json!("Alice"))
459            .build();
460
461        let mut state = json!({});
462        apply_patch(&mut state, &patch).unwrap();
463        assert_eq!(state["name"], "Alice");
464    }
465
466    #[test]
467    fn test_patch_builder_replace() {
468        let patch = PatchBuilder::new()
469            .replace("/count", json!(10))
470            .build();
471
472        let mut state = json!({"count": 0});
473        apply_patch(&mut state, &patch).unwrap();
474        assert_eq!(state["count"], 10);
475    }
476
477    #[test]
478    fn test_patch_builder_remove() {
479        let patch = PatchBuilder::new().remove("/temp").build();
480
481        let mut state = json!({"name": "Alice", "temp": "value"});
482        apply_patch(&mut state, &patch).unwrap();
483        assert!(state.get("temp").is_none());
484        assert_eq!(state["name"], "Alice");
485    }
486
487    #[test]
488    fn test_patch_builder_move() {
489        let patch = PatchBuilder::new()
490            .move_value("/old", "/new")
491            .build();
492
493        let mut state = json!({"old": "value"});
494        apply_patch(&mut state, &patch).unwrap();
495        assert!(state.get("old").is_none());
496        assert_eq!(state["new"], "value");
497    }
498
499    #[test]
500    fn test_patch_builder_copy() {
501        let patch = PatchBuilder::new()
502            .copy("/source", "/dest")
503            .build();
504
505        let mut state = json!({"source": "value"});
506        apply_patch(&mut state, &patch).unwrap();
507        assert_eq!(state["source"], "value");
508        assert_eq!(state["dest"], "value");
509    }
510
511    #[test]
512    fn test_patch_builder_test() {
513        // Test operation succeeds
514        let patch = PatchBuilder::new()
515            .test("/count", json!(0))
516            .replace("/count", json!(1))
517            .build();
518
519        let mut state = json!({"count": 0});
520        apply_patch(&mut state, &patch).unwrap();
521        assert_eq!(state["count"], 1);
522    }
523
524    #[test]
525    fn test_patch_builder_test_fails() {
526        let patch = PatchBuilder::new()
527            .test("/count", json!(999)) // Wrong value
528            .replace("/count", json!(1))
529            .build();
530
531        let mut state = json!({"count": 0});
532        let result = apply_patch(&mut state, &patch);
533        assert!(result.is_err());
534    }
535
536    #[test]
537    fn test_patch_builder_build_vec() {
538        let ops = PatchBuilder::new()
539            .add("/a", json!(1))
540            .replace("/b", json!(2))
541            .build_vec();
542
543        assert_eq!(ops.len(), 2);
544    }
545
546    #[test]
547    fn test_can_apply_patch() {
548        let state = json!({"count": 0});
549
550        let valid_patch = PatchBuilder::new().replace("/count", json!(1)).build();
551        assert!(can_apply_patch(&state, &valid_patch));
552
553        let invalid_patch = PatchBuilder::new().remove("/nonexistent").build();
554        assert!(!can_apply_patch(&state, &invalid_patch));
555    }
556
557    #[test]
558    fn test_merge_patches() {
559        let patch1 = PatchBuilder::new().add("/a", json!(1)).build();
560        let patch2 = PatchBuilder::new().add("/b", json!(2)).build();
561
562        let merged = merge_patches(&patch1, &patch2);
563        assert_eq!(merged.0.len(), 2);
564
565        let mut state = json!({});
566        apply_patch(&mut state, &merged).unwrap();
567        assert_eq!(state["a"], 1);
568        assert_eq!(state["b"], 2);
569    }
570
571    #[test]
572    fn test_patch_error_display() {
573        let err = PatchError::new("test error");
574        assert!(err.to_string().contains("test error"));
575    }
576
577    #[test]
578    fn test_complex_nested_patch() {
579        let from = json!({
580            "user": {
581                "profile": {
582                    "name": "Alice",
583                    "settings": {
584                        "theme": "light"
585                    }
586                }
587            }
588        });
589
590        let to = json!({
591            "user": {
592                "profile": {
593                    "name": "Alice",
594                    "settings": {
595                        "theme": "dark",
596                        "notifications": true
597                    }
598                }
599            }
600        });
601
602        let patch = create_patch(&from, &to);
603
604        let mut result = from.clone();
605        apply_patch(&mut result, &patch).unwrap();
606        assert_eq!(result, to);
607    }
608
609    #[test]
610    fn test_empty_patch() {
611        let state = json!({"count": 0});
612        let patch = create_patch(&state, &state);
613
614        // Patch of identical values should be empty
615        assert!(patch.0.is_empty());
616
617        // Applying empty patch should be no-op
618        let mut result = state.clone();
619        apply_patch(&mut result, &patch).unwrap();
620        assert_eq!(result, state);
621    }
622}