Skip to main content

cjson_binding/
cjson_utils.rs

1/***************************************************************************
2 *
3 * cJSON FFI BINDING FOR RUST
4 * Copyright (C) 2026 Antonio Salsi <passy.linux@zresa.it>
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, see <https://www.gnu.org/licenses/>.
18 *
19 ***************************************************************************/
20
21//! Safe Rust wrappers for cJSON_Utils library
22//!
23//! This module provides safe, idiomatic Rust interfaces over the cJSON_Utils C library,
24//! which implements RFC6901 (JSON Pointer), RFC6902 (JSON Patch), and RFC7386 (JSON Merge Patch).
25
26extern crate alloc;
27
28use alloc::ffi::CString;
29use alloc::string::String;
30use core::ffi::{CStr, c_char};
31
32use crate::cjson::{CJson, CJsonError, CJsonResult};
33use crate::cjson_ffi::cJSON;
34use crate::cjson_utils_ffi::*;
35
36/// JSON Pointer utilities (RFC6901)
37pub struct JsonPointer;
38
39impl JsonPointer {
40    /// Get a value from a JSON object using RFC6901 JSON Pointer syntax.
41    /// 
42    /// # Arguments
43    /// * `object` - The JSON object to search in
44    /// * `pointer` - The JSON Pointer string (e.g., "/foo/bar/0")
45    /// 
46    /// # Returns
47    /// A borrowed reference to the found item, or NotFound error
48    pub fn get(object: &CJson, pointer: &str) -> CJsonResult<CJsonRef> {
49        let c_pointer = CString::new(pointer).map_err(|_| CJsonError::InvalidUtf8)?;
50        let ptr = unsafe {
51            cJSONUtils_GetPointer(object.as_ptr() as *mut cJSON, c_pointer.as_ptr() as *const i8)
52        };
53        unsafe { CJsonRef::from_ptr(ptr) }.map_err(|_| CJsonError::NotFound)
54    }
55
56    /// Get a value from a JSON object using RFC6901 JSON Pointer syntax (case-sensitive).
57    /// 
58    /// # Arguments
59    /// * `object` - The JSON object to search in
60    /// * `pointer` - The JSON Pointer string (e.g., "/foo/bar/0")
61    /// 
62    /// # Returns
63    /// A borrowed reference to the found item, or NotFound error
64    pub fn get_case_sensitive(object: &CJson, pointer: &str) -> CJsonResult<CJsonRef> {
65        let c_pointer = CString::new(pointer).map_err(|_| CJsonError::InvalidUtf8)?;
66        let ptr = unsafe {
67            cJSONUtils_GetPointerCaseSensitive(
68                object.as_ptr() as *mut cJSON,
69                c_pointer.as_ptr() as *const i8,
70            )
71        };
72        unsafe { CJsonRef::from_ptr(ptr) }.map_err(|_| CJsonError::NotFound)
73    }
74
75    /// Find a JSON Pointer path from one object to a target value within it.
76    /// 
77    /// # Arguments
78    /// * `object` - The JSON object to search in
79    /// * `target` - The target value to find
80    /// 
81    /// # Returns
82    /// The JSON Pointer path as a String, or NotFound error
83    pub fn find_from_object_to(object: &CJson, target: &CJson) -> CJsonResult<String> {
84        let ptr = unsafe {
85            cJSONUtils_FindPointerFromObjectTo(object.as_ptr(), target.as_ptr())
86        };
87        if ptr.is_null() {
88            return Err(CJsonError::NotFound);
89        }
90        let path = unsafe { CStr::from_ptr(ptr as *const c_char).to_string_lossy().into_owned() };
91        unsafe { crate::cjson_ffi::cJSON_free(ptr as *mut core::ffi::c_void) };
92        Ok(path)
93    }
94}
95
96/// JSON Patch utilities (RFC6902)
97pub struct JsonPatch;
98
99impl JsonPatch {
100    /// Generate a JSON Patch (RFC6902) to transform 'from' into 'to'.
101    /// 
102    /// Note: This function modifies both 'from' and 'to' by sorting their keys.
103    /// 
104    /// # Arguments
105    /// * `from` - The original JSON object
106    /// * `to` - The target JSON object
107    /// 
108    /// # Returns
109    /// A new CJson object containing the patch operations
110    pub fn generate(from: &mut CJson, to: &mut CJson) -> CJsonResult<CJson> {
111        let ptr = unsafe {
112            cJSONUtils_GeneratePatches(from.as_mut_ptr(), to.as_mut_ptr())
113        };
114        unsafe { CJson::from_ptr(ptr) }
115    }
116
117    /// Generate a JSON Patch (RFC6902) to transform 'from' into 'to' (case-sensitive).
118    /// 
119    /// Note: This function modifies both 'from' and 'to' by sorting their keys.
120    /// 
121    /// # Arguments
122    /// * `from` - The original JSON object
123    /// * `to` - The target JSON object
124    /// 
125    /// # Returns
126    /// A new CJson object containing the patch operations
127    pub fn generate_case_sensitive(from: &mut CJson, to: &mut CJson) -> CJsonResult<CJson> {
128        let ptr = unsafe {
129            cJSONUtils_GeneratePatchesCaseSensitive(from.as_mut_ptr(), to.as_mut_ptr())
130        };
131        unsafe { CJson::from_ptr(ptr) }
132    }
133
134    /// Apply a JSON Patch (RFC6902) to an object.
135    /// 
136    /// # Arguments
137    /// * `object` - The JSON object to patch
138    /// * `patches` - The patch operations to apply
139    /// 
140    /// # Returns
141    /// Ok(()) on success, or an error
142    pub fn apply(object: &mut CJson, patches: &CJson) -> CJsonResult<()> {
143        let result = unsafe {
144            cJSONUtils_ApplyPatches(object.as_mut_ptr(), patches.as_ptr())
145        };
146        if result == 0 {
147            Ok(())
148        } else {
149            Err(CJsonError::InvalidOperation)
150        }
151    }
152
153    /// Apply a JSON Patch (RFC6902) to an object (case-sensitive).
154    /// 
155    /// # Arguments
156    /// * `object` - The JSON object to patch
157    /// * `patches` - The patch operations to apply
158    /// 
159    /// # Returns
160    /// Ok(()) on success, or an error
161    pub fn apply_case_sensitive(object: &mut CJson, patches: &CJson) -> CJsonResult<()> {
162        let result = unsafe {
163            cJSONUtils_ApplyPatchesCaseSensitive(object.as_mut_ptr(), patches.as_ptr())
164        };
165        if result == 0 {
166            Ok(())
167        } else {
168            Err(CJsonError::InvalidOperation)
169        }
170    }
171
172    /// Add a patch operation to a patches array.
173    /// 
174    /// # Arguments
175    /// * `array` - The array of patch operations
176    /// * `operation` - The operation type ("add", "remove", "replace", "move", "copy", "test")
177    /// * `path` - The JSON Pointer path
178    /// * `value` - The value for the operation (optional for some operations)
179    pub fn add_to_array(
180        array: &mut CJson,
181        operation: &str,
182        path: &str,
183        value: Option<&CJson>,
184    ) -> CJsonResult<()> {
185        if !array.is_array() {
186            return Err(CJsonError::TypeError);
187        }
188
189        let c_operation = CString::new(operation).map_err(|_| CJsonError::InvalidUtf8)?;
190        let c_path = CString::new(path).map_err(|_| CJsonError::InvalidUtf8)?;
191
192        let value_ptr = value.map(|v| v.as_ptr()).unwrap_or(core::ptr::null());
193
194        unsafe {
195            cJSONUtils_AddPatchToArray(
196                array.as_mut_ptr(),
197                c_operation.as_ptr() as *const i8,
198                c_path.as_ptr() as *const i8,
199                value_ptr,
200            );
201        }
202        Ok(())
203    }
204}
205
206/// JSON Merge Patch utilities (RFC7386)
207pub struct JsonMergePatch;
208
209impl JsonMergePatch {
210    /// Apply a JSON Merge Patch (RFC7386) to a target object.
211    /// 
212    /// # Arguments
213    /// * `target` - The JSON object to merge into
214    /// * `patch` - The merge patch to apply
215    /// 
216    /// # Returns
217    /// A new CJson object with the merged result
218    pub fn apply(target: &mut CJson, patch: &CJson) -> CJsonResult<CJson> {
219        let ptr = unsafe {
220            cJSONUtils_MergePatch(target.as_mut_ptr(), patch.as_ptr())
221        };
222        unsafe { CJson::from_ptr(ptr) }
223    }
224
225    /// Apply a JSON Merge Patch (RFC7386) to a target object (case-sensitive).
226    /// 
227    /// # Arguments
228    /// * `target` - The JSON object to merge into
229    /// * `patch` - The merge patch to apply
230    /// 
231    /// # Returns
232    /// A new CJson object with the merged result
233    pub fn apply_case_sensitive(target: &mut CJson, patch: &CJson) -> CJsonResult<CJson> {
234        let ptr = unsafe {
235            cJSONUtils_MergePatchCaseSensitive(target.as_mut_ptr(), patch.as_ptr())
236        };
237        unsafe { CJson::from_ptr(ptr) }
238    }
239
240    /// Generate a JSON Merge Patch to transform 'from' into 'to'.
241    /// 
242    /// Note: This function modifies both 'from' and 'to' by sorting their keys.
243    /// 
244    /// # Arguments
245    /// * `from` - The original JSON object
246    /// * `to` - The target JSON object
247    /// 
248    /// # Returns
249    /// A new CJson object containing the merge patch
250    pub fn generate(from: &mut CJson, to: &mut CJson) -> CJsonResult<CJson> {
251        let ptr = unsafe {
252            cJSONUtils_GenerateMergePatch(from.as_mut_ptr(), to.as_mut_ptr())
253        };
254        unsafe { CJson::from_ptr(ptr) }
255    }
256
257    /// Generate a JSON Merge Patch to transform 'from' into 'to' (case-sensitive).
258    /// 
259    /// Note: This function modifies both 'from' and 'to' by sorting their keys.
260    /// 
261    /// # Arguments
262    /// * `from` - The original JSON object
263    /// * `to` - The target JSON object
264    /// 
265    /// # Returns
266    /// A new CJson object containing the merge patch
267    pub fn generate_case_sensitive(from: &mut CJson, to: &mut CJson) -> CJsonResult<CJson> {
268        let ptr = unsafe {
269            cJSONUtils_GenerateMergePatchCaseSensitive(from.as_mut_ptr(), to.as_mut_ptr())
270        };
271        unsafe { CJson::from_ptr(ptr) }
272    }
273}
274
275/// Utility functions for JSON object manipulation
276pub struct JsonUtils;
277
278impl JsonUtils {
279    /// Sort object members alphabetically (case-insensitive).
280    /// 
281    /// # Arguments
282    /// * `object` - The JSON object to sort
283    pub fn sort_object(object: &mut CJson) -> CJsonResult<()> {
284        if !object.is_object() {
285            return Err(CJsonError::TypeError);
286        }
287        unsafe { cJSONUtils_SortObject(object.as_mut_ptr()) };
288        Ok(())
289    }
290
291    /// Sort object members alphabetically (case-sensitive).
292    /// 
293    /// # Arguments
294    /// * `object` - The JSON object to sort
295    pub fn sort_object_case_sensitive(object: &mut CJson) -> CJsonResult<()> {
296        if !object.is_object() {
297            return Err(CJsonError::TypeError);
298        }
299        unsafe { cJSONUtils_SortObjectCaseSensitive(object.as_mut_ptr()) };
300        Ok(())
301    }
302}
303
304/// Re-export CJsonRef for use with pointer operations
305pub use crate::cjson::CJsonRef;
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use crate::cjson::CJson;
311
312    #[test]
313    fn test_json_pointer_get() {
314        let json = r#"{"foo":{"bar":[1,2,3]}}"#;
315        let obj = CJson::parse(json).unwrap();
316        
317        let result = JsonPointer::get(&obj, "/foo/bar/1").unwrap();
318        assert_eq!(result.get_number_value().unwrap(), 2.0);
319    }
320
321    #[test]
322    fn test_json_pointer_get_case_sensitive() {
323        let json = r#"{"Foo":{"Bar":"test"}}"#;
324        let obj = CJson::parse(json).unwrap();
325        
326        let result = JsonPointer::get_case_sensitive(&obj, "/Foo/Bar").unwrap();
327        assert_eq!(result.get_string_value().unwrap(), "test");
328    }
329
330    #[test]
331    fn test_json_pointer_not_found() {
332        let json = r#"{"foo":"bar"}"#;
333        let obj = CJson::parse(json).unwrap();
334        
335        assert!(JsonPointer::get(&obj, "/nonexistent").is_err());
336    }
337
338    #[test]
339    fn test_json_patch_generate_and_apply() {
340        let from_json = r#"{"name":"John","age":30}"#;
341        let to_json = r#"{"name":"John","age":31,"city":"NYC"}"#;
342        
343        let mut from = CJson::parse(from_json).unwrap();
344        let mut to = CJson::parse(to_json).unwrap();
345        
346        let patches = JsonPatch::generate(&mut from, &mut to).unwrap();
347        assert!(patches.is_array());
348    }
349
350    #[test]
351    fn test_json_patch_apply() {
352        let obj_json = r#"{"name":"John","age":30}"#;
353        let patch_json = r#"[{"op":"replace","path":"/age","value":31}]"#;
354        
355        let mut obj = CJson::parse(obj_json).unwrap();
356        let patches = CJson::parse(patch_json).unwrap();
357        
358        JsonPatch::apply(&mut obj, &patches).unwrap();
359        
360        let age = obj.get_object_item("age").unwrap();
361        assert_eq!(age.get_number_value().unwrap(), 31.0);
362    }
363
364    #[test]
365    fn test_json_merge_patch_apply() {
366        let target_json = r#"{"name":"John","age":30}"#;
367        let patch_json = r#"{"age":31,"city":"NYC"}"#;
368        
369        let mut target = CJson::parse(target_json).unwrap();
370        let patch = CJson::parse(patch_json).unwrap();
371        
372        let result = JsonMergePatch::apply(&mut target, &patch).unwrap();
373        // target ownership is consumed by apply, don't use it anymore
374        core::mem::forget(target); // Prevent double free
375        
376        let age = result.get_object_item("age").unwrap();
377        assert_eq!(age.get_number_value().unwrap(), 31.0);
378        
379        let city = result.get_object_item("city").unwrap();
380        assert_eq!(city.get_string_value().unwrap(), "NYC");
381    }
382
383    #[test]
384    fn test_json_merge_patch_generate() {
385        let from_json = r#"{"name":"John","age":30}"#;
386        let to_json = r#"{"name":"John","age":31}"#;
387        
388        let mut from = CJson::parse(from_json).unwrap();
389        let mut to = CJson::parse(to_json).unwrap();
390        
391        let patch = JsonMergePatch::generate(&mut from, &mut to).unwrap();
392        assert!(patch.is_object());
393    }
394
395    #[test]
396    fn test_json_utils_sort_object() {
397        let json = r#"{"z":"last","a":"first","m":"middle"}"#;
398        let mut obj = CJson::parse(json).unwrap();
399        
400        JsonUtils::sort_object(&mut obj).unwrap();
401        
402        // After sorting, the object should still be valid
403        assert!(obj.is_object());
404        assert!(obj.has_object_item("a"));
405        assert!(obj.has_object_item("m"));
406        assert!(obj.has_object_item("z"));
407    }
408
409    #[test]
410    fn test_json_utils_sort_object_case_sensitive() {
411        let json = r#"{"Z":"last","a":"first","M":"middle"}"#;
412        let mut obj = CJson::parse(json).unwrap();
413        
414        JsonUtils::sort_object_case_sensitive(&mut obj).unwrap();
415        
416        assert!(obj.is_object());
417        assert!(obj.has_object_item("a"));
418        assert!(obj.has_object_item("M"));
419        assert!(obj.has_object_item("Z"));
420    }
421
422    #[test]
423    fn test_pointer_find_from_object_to() {
424        let json = r#"{"foo":{"bar":"test"}}"#;
425        let obj = CJson::parse(json).unwrap();
426        
427        // Note: This creates a new CJson from the reference for testing
428        let target_owned = CJson::parse(r#"{"bar":"test"}"#).unwrap();
429        
430        // This might not work exactly as expected due to pointer comparison
431        // but tests the API
432        let _ = JsonPointer::find_from_object_to(&obj, &target_owned);
433    }
434
435    #[test]
436    fn test_json_patch_add_to_array() {
437        let mut patches = CJson::create_array().unwrap();
438        
439        let value = CJson::create_string("test").unwrap();
440        JsonPatch::add_to_array(&mut patches, "add", "/foo", Some(&value)).unwrap();
441        
442        assert!(patches.is_array());
443        assert_eq!(patches.get_array_size().unwrap(), 1);
444    }
445
446    #[test]
447    fn test_complex_pointer_path() {
448        let json = r#"{"users":[{"name":"Alice","age":25},{"name":"Bob","age":30}]}"#;
449        let obj = CJson::parse(json).unwrap();
450        
451        let result = JsonPointer::get(&obj, "/users/0/name").unwrap();
452        assert_eq!(result.get_string_value().unwrap(), "Alice");
453        
454        let result = JsonPointer::get(&obj, "/users/1/age").unwrap();
455        assert_eq!(result.get_number_value().unwrap(), 30.0);
456    }
457
458    #[test]
459    fn test_merge_patch_null_removal() {
460        let target_json = r#"{"name":"John","age":30,"city":"NYC"}"#;
461        let patch_json = r#"{"city":null}"#;
462        
463        let mut target = CJson::parse(target_json).unwrap();
464        let patch = CJson::parse(patch_json).unwrap();
465        
466        let result = JsonMergePatch::apply(&mut target, &patch).unwrap();
467        core::mem::forget(target); // Prevent double free
468        
469        // City should be removed
470        assert!(result.get_object_item("city").is_err());
471        assert!(result.has_object_item("name"));
472        assert!(result.has_object_item("age"));
473    }
474}