Skip to main content

jpx_core/extensions/
jsonpatch.rs

1//! JSON Patch (RFC 6902) functions.
2
3use std::collections::HashSet;
4
5use serde_json::Value;
6
7use crate::functions::{Function, custom_error};
8use crate::interpreter::SearchResult;
9use crate::registry::register_if_enabled;
10use crate::{Context, Runtime, arg, defn};
11
12// =============================================================================
13// json_patch(obj, patch) -> object (RFC 6902)
14// Apply a JSON Patch (RFC 6902) to an object.
15// =============================================================================
16
17defn!(JsonPatchFn, vec![arg!(any), arg!(array)], None);
18
19impl Function for JsonPatchFn {
20    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
21        self.signature.validate(args, ctx)?;
22
23        // json-patch works with serde_json::Value directly -- no conversion needed
24        let mut result = args[0].clone();
25
26        let patch: json_patch::Patch = serde_json::from_value(args[1].clone())
27            .map_err(|e| custom_error(ctx, &format!("Invalid JSON Patch format: {}", e)))?;
28
29        json_patch::patch(&mut result, &patch)
30            .map_err(|e| custom_error(ctx, &format!("Failed to apply patch: {}", e)))?;
31
32        Ok(result)
33    }
34}
35
36// =============================================================================
37// json_merge_patch(obj, patch) -> object (RFC 7396)
38// Apply a JSON Merge Patch (RFC 7396) to an object.
39// =============================================================================
40
41defn!(JsonMergePatchFn, vec![arg!(any), arg!(any)], None);
42
43impl Function for JsonMergePatchFn {
44    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
45        self.signature.validate(args, ctx)?;
46
47        let mut result = args[0].clone();
48        json_patch::merge(&mut result, &args[1]);
49
50        Ok(result)
51    }
52}
53
54// =============================================================================
55// json_diff(a, b) -> array (RFC 6902 JSON Patch)
56// Generate a JSON Patch (RFC 6902) that transforms the first object into the second.
57// =============================================================================
58
59defn!(JsonDiffFn, vec![arg!(any), arg!(any)], None);
60
61impl Function for JsonDiffFn {
62    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
63        self.signature.validate(args, ctx)?;
64
65        let patch = json_patch::diff(&args[0], &args[1]);
66
67        let patch_json = serde_json::to_value(&patch)
68            .map_err(|e| custom_error(ctx, &format!("Failed to serialize patch: {}", e)))?;
69
70        Ok(patch_json)
71    }
72}
73
74/// Register JSON patch functions filtered by the enabled set.
75pub fn register_filtered(runtime: &mut Runtime, enabled: &HashSet<&str>) {
76    register_if_enabled(runtime, "json_patch", enabled, Box::new(JsonPatchFn::new()));
77    register_if_enabled(
78        runtime,
79        "json_merge_patch",
80        enabled,
81        Box::new(JsonMergePatchFn::new()),
82    );
83    register_if_enabled(runtime, "json_diff", enabled, Box::new(JsonDiffFn::new()));
84}
85
86#[cfg(test)]
87mod tests {
88    use crate::Runtime;
89    use serde_json::json;
90
91    fn setup_runtime() -> Runtime {
92        Runtime::builder()
93            .with_standard()
94            .with_all_extensions()
95            .build()
96    }
97
98    #[test]
99    fn test_json_patch_add() {
100        let runtime = setup_runtime();
101        let data = json!({"doc": {"a": 1}, "patch": [{"op": "add", "path": "/b", "value": 2}]});
102        let expr = runtime.compile("json_patch(doc, patch)").unwrap();
103        let result = expr.search(&data).unwrap();
104        let obj = result.as_object().unwrap();
105        assert_eq!(obj.get("a").unwrap().as_f64().unwrap() as i64, 1);
106        assert_eq!(obj.get("b").unwrap().as_f64().unwrap() as i64, 2);
107    }
108
109    #[test]
110    fn test_json_patch_remove() {
111        let runtime = setup_runtime();
112        let data = json!({"doc": {"a": 1, "b": 2}, "patch": [{"op": "remove", "path": "/b"}]});
113        let expr = runtime.compile("json_patch(doc, patch)").unwrap();
114        let result = expr.search(&data).unwrap();
115        let obj = result.as_object().unwrap();
116        assert_eq!(obj.get("a").unwrap().as_f64().unwrap() as i64, 1);
117        assert!(obj.get("b").is_none());
118    }
119
120    #[test]
121    fn test_json_patch_replace() {
122        let runtime = setup_runtime();
123        let data =
124            json!({"doc": {"a": 1}, "patch": [{"op": "replace", "path": "/a", "value": 99}]});
125        let expr = runtime.compile("json_patch(doc, patch)").unwrap();
126        let result = expr.search(&data).unwrap();
127        let obj = result.as_object().unwrap();
128        assert_eq!(obj.get("a").unwrap().as_f64().unwrap() as i64, 99);
129    }
130
131    #[test]
132    fn test_json_patch_multiple_ops() {
133        let runtime = setup_runtime();
134        let data = json!({
135            "doc": {"a": 1},
136            "patch": [
137                {"op": "add", "path": "/b", "value": 2},
138                {"op": "replace", "path": "/a", "value": 10}
139            ]
140        });
141        let expr = runtime.compile("json_patch(doc, patch)").unwrap();
142        let result = expr.search(&data).unwrap();
143        let obj = result.as_object().unwrap();
144        assert_eq!(obj.get("a").unwrap().as_f64().unwrap() as i64, 10);
145        assert_eq!(obj.get("b").unwrap().as_f64().unwrap() as i64, 2);
146    }
147
148    #[test]
149    fn test_json_merge_patch_simple() {
150        let runtime = setup_runtime();
151        let data = json!({"doc": {"a": 1, "b": 2}, "patch": {"b": 3, "c": 4}});
152        let expr = runtime.compile("json_merge_patch(doc, patch)").unwrap();
153        let result = expr.search(&data).unwrap();
154        let obj = result.as_object().unwrap();
155        assert_eq!(obj.get("a").unwrap().as_f64().unwrap() as i64, 1);
156        assert_eq!(obj.get("b").unwrap().as_f64().unwrap() as i64, 3);
157        assert_eq!(obj.get("c").unwrap().as_f64().unwrap() as i64, 4);
158    }
159
160    #[test]
161    fn test_json_merge_patch_remove_with_null() {
162        let runtime = setup_runtime();
163        let data = json!({"doc": {"a": 1, "b": 2}, "patch": {"b": null}});
164        let expr = runtime.compile("json_merge_patch(doc, patch)").unwrap();
165        let result = expr.search(&data).unwrap();
166        let obj = result.as_object().unwrap();
167        assert_eq!(obj.get("a").unwrap().as_f64().unwrap() as i64, 1);
168        assert!(obj.get("b").is_none());
169    }
170
171    #[test]
172    fn test_json_merge_patch_nested() {
173        let runtime = setup_runtime();
174        let data = json!({"doc": {"a": {"x": 1}}, "patch": {"a": {"y": 2}}});
175        let expr = runtime.compile("json_merge_patch(doc, patch)").unwrap();
176        let result = expr.search(&data).unwrap();
177        let obj = result.as_object().unwrap();
178        let a = obj.get("a").unwrap().as_object().unwrap();
179        assert_eq!(a.get("x").unwrap().as_f64().unwrap() as i64, 1);
180        assert_eq!(a.get("y").unwrap().as_f64().unwrap() as i64, 2);
181    }
182
183    #[test]
184    fn test_json_diff_add() {
185        let runtime = setup_runtime();
186        let data = json!({"a": {"x": 1}, "b": {"x": 1, "y": 2}});
187        let expr = runtime.compile("json_diff(a, b)").unwrap();
188        let result = expr.search(&data).unwrap();
189        let arr = result.as_array().unwrap();
190        assert_eq!(arr.len(), 1);
191        let op = arr[0].as_object().unwrap();
192        assert_eq!(op.get("op").unwrap().as_str().unwrap(), "add");
193        assert_eq!(op.get("path").unwrap().as_str().unwrap(), "/y");
194    }
195
196    #[test]
197    fn test_json_diff_remove() {
198        let runtime = setup_runtime();
199        let data = json!({"a": {"x": 1, "y": 2}, "b": {"x": 1}});
200        let expr = runtime.compile("json_diff(a, b)").unwrap();
201        let result = expr.search(&data).unwrap();
202        let arr = result.as_array().unwrap();
203        assert_eq!(arr.len(), 1);
204        let op = arr[0].as_object().unwrap();
205        assert_eq!(op.get("op").unwrap().as_str().unwrap(), "remove");
206        assert_eq!(op.get("path").unwrap().as_str().unwrap(), "/y");
207    }
208
209    #[test]
210    fn test_json_diff_replace() {
211        let runtime = setup_runtime();
212        let data = json!({"a": {"x": 1}, "b": {"x": 2}});
213        let expr = runtime.compile("json_diff(a, b)").unwrap();
214        let result = expr.search(&data).unwrap();
215        let arr = result.as_array().unwrap();
216        assert_eq!(arr.len(), 1);
217        let op = arr[0].as_object().unwrap();
218        assert_eq!(op.get("op").unwrap().as_str().unwrap(), "replace");
219        assert_eq!(op.get("path").unwrap().as_str().unwrap(), "/x");
220    }
221
222    #[test]
223    fn test_json_diff_no_changes() {
224        let runtime = setup_runtime();
225        let data = json!({"a": {"x": 1}, "b": {"x": 1}});
226        let expr = runtime.compile("json_diff(a, b)").unwrap();
227        let result = expr.search(&data).unwrap();
228        let arr = result.as_array().unwrap();
229        assert_eq!(arr.len(), 0);
230    }
231
232    #[test]
233    fn test_json_diff_roundtrip() {
234        // Generate a diff and apply it - should get the same result
235        let runtime = setup_runtime();
236        let data = json!({"a": {"x": 1}, "b": {"x": 2, "y": 3}});
237
238        // First get the diff
239        let diff_expr = runtime.compile("json_diff(a, b)").unwrap();
240        let diff_result = diff_expr.search(&data).unwrap();
241
242        // Now apply the diff to a
243        let data_with_patch = json!({
244            "doc": {"x": 1},
245            "patch": diff_result
246        });
247        let patch_expr = runtime.compile("json_patch(doc, patch)").unwrap();
248        let patched = patch_expr.search(&data_with_patch).unwrap();
249
250        // Should equal b
251        let obj = patched.as_object().unwrap();
252        assert_eq!(obj.get("x").unwrap().as_f64().unwrap() as i64, 2);
253        assert_eq!(obj.get("y").unwrap().as_f64().unwrap() as i64, 3);
254    }
255}