Skip to main content

jpx_engine/
json_utils.rs

1//! JSON utility functions: format, diff, patch, merge, keys, paths, stats.
2//!
3//! This module provides JSON manipulation and analysis tools that complement
4//! the core JMESPath evaluation. These operations work directly on JSON data
5//! without requiring JMESPath expressions.
6
7use crate::JpxEngine;
8use crate::error::{EngineError, Result};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::HashMap;
12
13/// Statistics about JSON data structure.
14///
15/// Provides insights into JSON data including type, size, depth, and
16/// detailed field analysis for arrays of objects.
17///
18/// # Example
19///
20/// ```rust
21/// use jpx_engine::JpxEngine;
22///
23/// let engine = JpxEngine::new();
24/// let stats = engine.stats(r#"{"users": [{"name": "alice"}, {"name": "bob"}]}"#).unwrap();
25///
26/// println!("Type: {}", stats.root_type);      // "object"
27/// println!("Size: {}", stats.size_human);     // "52 bytes"
28/// println!("Depth: {}", stats.depth);         // 3
29/// ```
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct StatsResult {
32    /// JSON type of the root value ("object", "array", "string", etc.)
33    pub root_type: String,
34    /// Size of the JSON string in bytes
35    pub size_bytes: usize,
36    /// Human-readable size (e.g., "1.5 KB", "2.3 MB")
37    pub size_human: String,
38    /// Maximum nesting depth (0 for primitives)
39    pub depth: usize,
40    /// Number of items (arrays only)
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub length: Option<usize>,
43    /// Number of keys (objects only)
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub key_count: Option<usize>,
46    /// Field analysis (arrays of objects only)
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub fields: Option<Vec<FieldAnalysis>>,
49    /// Count of each JSON type in array (arrays only)
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub type_distribution: Option<HashMap<String, usize>>,
52}
53
54/// Analysis of a field across an array of objects.
55///
56/// Used by [`StatsResult`] to provide insights into consistent fields
57/// in arrays of objects, including type information and null counts.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct FieldAnalysis {
60    /// Field name (key)
61    pub name: String,
62    /// Most common type for this field
63    pub field_type: String,
64    /// Number of objects where this field is null
65    pub null_count: usize,
66    /// Number of distinct values (omitted for high-cardinality fields)
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub unique_count: Option<usize>,
69}
70
71/// Information about a path in a JSON structure.
72///
73/// Used by [`JpxEngine::paths`] to enumerate all paths in a JSON document.
74///
75/// # Example
76///
77/// ```rust
78/// use jpx_engine::JpxEngine;
79///
80/// let engine = JpxEngine::new();
81/// let paths = engine.paths(r#"{"user": {"name": "alice"}}"#, true, false).unwrap();
82///
83/// for path in paths {
84///     println!("{}: {:?}", path.path, path.path_type);
85/// }
86/// // Output:
87/// // @: Some("object")
88/// // user: Some("object")
89/// // user.name: Some("string")
90/// ```
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct PathInfo {
93    /// Path in dot notation (e.g., "user.name", "items.0.id")
94    pub path: String,
95    /// JSON type at this path (if `include_types` was true)
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub path_type: Option<String>,
98    /// Value at this path (if `include_values` was true, leaf nodes only)
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub value: Option<Value>,
101}
102
103// =============================================================================
104// JpxEngine JSON utility methods
105// =============================================================================
106
107impl JpxEngine {
108    /// Format JSON with configurable indentation.
109    ///
110    /// # Arguments
111    ///
112    /// * `input` - JSON string to format
113    /// * `indent` - Number of spaces per indentation level (0 = compact/minified)
114    ///
115    /// # Example
116    ///
117    /// ```rust
118    /// use jpx_engine::JpxEngine;
119    ///
120    /// let engine = JpxEngine::new();
121    ///
122    /// // Pretty-print with 2-space indent
123    /// let pretty = engine.format_json(r#"{"a":1,"b":2}"#, 2).unwrap();
124    /// assert!(pretty.contains('\n'));
125    ///
126    /// // Compact/minified
127    /// let compact = engine.format_json(r#"{"a":  1, "b": 2}"#, 0).unwrap();
128    /// assert!(!compact.contains('\n'));
129    /// ```
130    pub fn format_json(&self, input: &str, indent: usize) -> Result<String> {
131        let value: Value =
132            serde_json::from_str(input).map_err(|e| EngineError::InvalidJson(e.to_string()))?;
133
134        if indent == 0 {
135            serde_json::to_string(&value).map_err(|e| EngineError::Internal(e.to_string()))
136        } else {
137            let indent_bytes = vec![b' '; indent];
138            let formatter = serde_json::ser::PrettyFormatter::with_indent(&indent_bytes);
139            let mut buf = Vec::new();
140            let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
141            value
142                .serialize(&mut ser)
143                .map_err(|e| EngineError::Internal(e.to_string()))?;
144            String::from_utf8(buf).map_err(|e| EngineError::Internal(e.to_string()))
145        }
146    }
147
148    /// Generate a JSON Patch (RFC 6902) from source to target.
149    ///
150    /// # Arguments
151    ///
152    /// * `source` - Original JSON string
153    /// * `target` - Modified JSON string
154    ///
155    /// # Returns
156    ///
157    /// A JSON array of patch operations that transform `source` into `target`.
158    ///
159    /// # Example
160    ///
161    /// ```rust
162    /// use jpx_engine::JpxEngine;
163    ///
164    /// let engine = JpxEngine::new();
165    /// let patch = engine.diff(r#"{"a": 1}"#, r#"{"a": 2}"#).unwrap();
166    /// assert!(!patch.as_array().unwrap().is_empty());
167    /// ```
168    pub fn diff(&self, source: &str, target: &str) -> Result<Value> {
169        let source_val: Value =
170            serde_json::from_str(source).map_err(|e| EngineError::InvalidJson(e.to_string()))?;
171        let target_val: Value =
172            serde_json::from_str(target).map_err(|e| EngineError::InvalidJson(e.to_string()))?;
173
174        let patch = json_patch::diff(&source_val, &target_val);
175        serde_json::to_value(&patch).map_err(|e| EngineError::Internal(e.to_string()))
176    }
177
178    /// Apply a JSON Patch (RFC 6902) to a document.
179    ///
180    /// # Arguments
181    ///
182    /// * `input` - JSON document to patch
183    /// * `patch` - JSON array of patch operations
184    ///
185    /// # Example
186    ///
187    /// ```rust
188    /// use jpx_engine::JpxEngine;
189    /// use serde_json::json;
190    ///
191    /// let engine = JpxEngine::new();
192    /// let result = engine.patch(
193    ///     r#"{"a": 1}"#,
194    ///     r#"[{"op": "replace", "path": "/a", "value": 2}]"#,
195    /// ).unwrap();
196    /// assert_eq!(result, json!({"a": 2}));
197    /// ```
198    pub fn patch(&self, input: &str, patch: &str) -> Result<Value> {
199        let mut doc: Value =
200            serde_json::from_str(input).map_err(|e| EngineError::InvalidJson(e.to_string()))?;
201        let patch: json_patch::Patch =
202            serde_json::from_str(patch).map_err(|e| EngineError::InvalidJson(e.to_string()))?;
203
204        json_patch::patch(&mut doc, &patch)
205            .map_err(|e| EngineError::evaluation_failed(e.to_string()))?;
206
207        Ok(doc)
208    }
209
210    /// Apply a JSON Merge Patch (RFC 7396) to a document.
211    ///
212    /// Unlike JSON Patch (RFC 6902), merge patch uses a simple object overlay:
213    /// - Present keys with non-null values are set
214    /// - Present keys with null values are removed
215    /// - Absent keys are unchanged
216    ///
217    /// # Arguments
218    ///
219    /// * `input` - JSON document to merge into
220    /// * `patch` - JSON merge patch object
221    ///
222    /// # Example
223    ///
224    /// ```rust
225    /// use jpx_engine::JpxEngine;
226    /// use serde_json::json;
227    ///
228    /// let engine = JpxEngine::new();
229    /// let result = engine.merge(
230    ///     r#"{"a": 1, "b": 2}"#,
231    ///     r#"{"b": 3, "c": 4}"#,
232    /// ).unwrap();
233    /// assert_eq!(result, json!({"a": 1, "b": 3, "c": 4}));
234    /// ```
235    pub fn merge(&self, input: &str, patch: &str) -> Result<Value> {
236        let mut doc: Value =
237            serde_json::from_str(input).map_err(|e| EngineError::InvalidJson(e.to_string()))?;
238        let patch_val: Value =
239            serde_json::from_str(patch).map_err(|e| EngineError::InvalidJson(e.to_string()))?;
240
241        json_patch::merge(&mut doc, &patch_val);
242        Ok(doc)
243    }
244
245    /// Extract keys from a JSON object.
246    ///
247    /// # Arguments
248    ///
249    /// * `input` - JSON string (must be an object for non-recursive mode)
250    /// * `recursive` - If `true`, extracts all nested paths in dot notation
251    ///
252    /// # Example
253    ///
254    /// ```rust
255    /// use jpx_engine::JpxEngine;
256    ///
257    /// let engine = JpxEngine::new();
258    ///
259    /// // Top-level keys (sorted)
260    /// let keys = engine.keys(r#"{"b": 1, "a": {"c": 2}}"#, false).unwrap();
261    /// assert_eq!(keys, vec!["a", "b"]);
262    ///
263    /// // Recursive (dot-notation paths)
264    /// let keys = engine.keys(r#"{"a": {"c": 2}, "b": 1}"#, true).unwrap();
265    /// assert!(keys.contains(&"a.c".to_string()));
266    /// ```
267    pub fn keys(&self, input: &str, recursive: bool) -> Result<Vec<String>> {
268        let value: Value =
269            serde_json::from_str(input).map_err(|e| EngineError::InvalidJson(e.to_string()))?;
270
271        let mut keys = Vec::new();
272        if recursive {
273            extract_keys_recursive(&value, "", &mut keys);
274        } else if let Value::Object(map) = &value {
275            keys = map.keys().cloned().collect();
276            keys.sort();
277        }
278        Ok(keys)
279    }
280
281    /// Extract all paths from a JSON document.
282    ///
283    /// Enumerates every path in the JSON structure, optionally including
284    /// type information and leaf values.
285    ///
286    /// # Arguments
287    ///
288    /// * `input` - JSON string to analyze
289    /// * `include_types` - If `true`, each path includes its JSON type
290    /// * `include_values` - If `true`, leaf paths include their values
291    ///
292    /// # Example
293    ///
294    /// ```rust
295    /// use jpx_engine::JpxEngine;
296    ///
297    /// let engine = JpxEngine::new();
298    /// let paths = engine.paths(r#"{"user": {"name": "alice"}}"#, true, false).unwrap();
299    /// assert!(paths.iter().any(|p| p.path == "user.name"));
300    /// ```
301    pub fn paths(
302        &self,
303        input: &str,
304        include_types: bool,
305        include_values: bool,
306    ) -> Result<Vec<PathInfo>> {
307        let value: Value =
308            serde_json::from_str(input).map_err(|e| EngineError::InvalidJson(e.to_string()))?;
309
310        let mut paths = Vec::new();
311        extract_paths(&value, "", include_types, include_values, &mut paths);
312        Ok(paths)
313    }
314
315    /// Analyze JSON data and return structural statistics.
316    ///
317    /// Returns type information, size, depth, and for arrays of objects,
318    /// per-field analysis including type distribution and null counts.
319    ///
320    /// # Arguments
321    ///
322    /// * `input` - JSON string to analyze
323    ///
324    /// # Example
325    ///
326    /// ```rust
327    /// use jpx_engine::JpxEngine;
328    ///
329    /// let engine = JpxEngine::new();
330    /// let stats = engine.stats(r#"[{"name": "alice"}, {"name": "bob"}]"#).unwrap();
331    /// assert_eq!(stats.root_type, "array");
332    /// assert_eq!(stats.length, Some(2));
333    /// assert!(stats.fields.is_some());
334    /// ```
335    pub fn stats(&self, input: &str) -> Result<StatsResult> {
336        let value: Value =
337            serde_json::from_str(input).map_err(|e| EngineError::InvalidJson(e.to_string()))?;
338
339        let size_bytes = input.len();
340        let depth = calculate_depth(&value);
341
342        let (length, key_count, fields, type_distribution) = match &value {
343            Value::Array(arr) => {
344                let type_dist = calculate_type_distribution(arr);
345                let field_analysis = if arr.iter().all(|v| v.is_object()) {
346                    Some(analyze_array_fields(arr))
347                } else {
348                    None
349                };
350                (Some(arr.len()), None, field_analysis, Some(type_dist))
351            }
352            Value::Object(map) => (None, Some(map.len()), None, None),
353            _ => (None, None, None, None),
354        };
355
356        Ok(StatsResult {
357            root_type: json_type_name(&value).to_string(),
358            size_bytes,
359            size_human: format_size(size_bytes),
360            depth,
361            length,
362            key_count,
363            fields,
364            type_distribution,
365        })
366    }
367}
368
369// =============================================================================
370// Helper functions
371// =============================================================================
372
373/// Extract keys recursively from a JSON value
374fn extract_keys_recursive(value: &Value, prefix: &str, keys: &mut Vec<String>) {
375    match value {
376        Value::Object(map) => {
377            for (k, v) in map {
378                let path = if prefix.is_empty() {
379                    k.clone()
380                } else {
381                    format!("{}.{}", prefix, k)
382                };
383                keys.push(path.clone());
384                extract_keys_recursive(v, &path, keys);
385            }
386        }
387        Value::Array(arr) => {
388            for (i, v) in arr.iter().enumerate() {
389                let path = format!("{}.{}", prefix, i);
390                extract_keys_recursive(v, &path, keys);
391            }
392        }
393        _ => {}
394    }
395}
396
397/// Extract paths from a JSON value
398fn extract_paths(
399    value: &Value,
400    prefix: &str,
401    include_types: bool,
402    include_values: bool,
403    paths: &mut Vec<PathInfo>,
404) {
405    let current_path = if prefix.is_empty() {
406        "@".to_string()
407    } else {
408        prefix.to_string()
409    };
410
411    match value {
412        Value::Object(map) => {
413            paths.push(PathInfo {
414                path: current_path.clone(),
415                path_type: if include_types {
416                    Some("object".to_string())
417                } else {
418                    None
419                },
420                value: None,
421            });
422            for (k, v) in map {
423                let new_prefix = if prefix.is_empty() {
424                    k.clone()
425                } else {
426                    format!("{}.{}", prefix, k)
427                };
428                extract_paths(v, &new_prefix, include_types, include_values, paths);
429            }
430        }
431        Value::Array(arr) => {
432            paths.push(PathInfo {
433                path: current_path.clone(),
434                path_type: if include_types {
435                    Some("array".to_string())
436                } else {
437                    None
438                },
439                value: None,
440            });
441            for (i, v) in arr.iter().enumerate() {
442                let new_prefix = format!("{}.{}", prefix, i);
443                extract_paths(v, &new_prefix, include_types, include_values, paths);
444            }
445        }
446        _ => {
447            paths.push(PathInfo {
448                path: current_path,
449                path_type: if include_types {
450                    Some(json_type_name(value).to_string())
451                } else {
452                    None
453                },
454                value: if include_values {
455                    Some(value.clone())
456                } else {
457                    None
458                },
459            });
460        }
461    }
462}
463
464/// Calculate the nesting depth of a JSON value
465fn calculate_depth(value: &Value) -> usize {
466    match value {
467        Value::Object(map) => 1 + map.values().map(calculate_depth).max().unwrap_or(0),
468        Value::Array(arr) => 1 + arr.iter().map(calculate_depth).max().unwrap_or(0),
469        _ => 0,
470    }
471}
472
473/// Get the type name of a JSON value
474fn json_type_name(value: &Value) -> &'static str {
475    match value {
476        Value::Null => "null",
477        Value::Bool(_) => "boolean",
478        Value::Number(_) => "number",
479        Value::String(_) => "string",
480        Value::Array(_) => "array",
481        Value::Object(_) => "object",
482    }
483}
484
485/// Calculate type distribution in an array
486fn calculate_type_distribution(arr: &[Value]) -> HashMap<String, usize> {
487    let mut dist = HashMap::new();
488    for item in arr {
489        *dist.entry(json_type_name(item).to_string()).or_insert(0) += 1;
490    }
491    dist
492}
493
494/// Analyze fields in an array of objects
495fn analyze_array_fields(arr: &[Value]) -> Vec<FieldAnalysis> {
496    let mut field_types: HashMap<String, HashMap<String, usize>> = HashMap::new();
497    let mut field_null_counts: HashMap<String, usize> = HashMap::new();
498    let mut field_values: HashMap<String, Vec<Value>> = HashMap::new();
499
500    for item in arr {
501        if let Value::Object(map) = item {
502            for (k, v) in map {
503                let types = field_types.entry(k.clone()).or_default();
504                *types.entry(json_type_name(v).to_string()).or_insert(0) += 1;
505
506                if v.is_null() {
507                    *field_null_counts.entry(k.clone()).or_insert(0) += 1;
508                }
509
510                // Track unique values for low-cardinality detection
511                let values = field_values.entry(k.clone()).or_default();
512                if values.len() < 100 && !values.contains(v) {
513                    values.push(v.clone());
514                }
515            }
516        }
517    }
518
519    let mut fields: Vec<FieldAnalysis> = field_types
520        .into_iter()
521        .map(|(name, types)| {
522            let predominant_type = types
523                .into_iter()
524                .max_by_key(|(_, count)| *count)
525                .map(|(t, _)| t)
526                .unwrap_or_else(|| "unknown".to_string());
527
528            let null_count = field_null_counts.get(&name).copied().unwrap_or(0);
529            let unique_count = field_values.get(&name).map(|v| v.len());
530
531            FieldAnalysis {
532                name,
533                field_type: predominant_type,
534                null_count,
535                unique_count,
536            }
537        })
538        .collect();
539
540    fields.sort_by(|a, b| a.name.cmp(&b.name));
541    fields
542}
543
544/// Format size in human-readable form
545fn format_size(bytes: usize) -> String {
546    const KB: usize = 1024;
547    const MB: usize = KB * 1024;
548    const GB: usize = MB * 1024;
549
550    if bytes >= GB {
551        format!("{:.2} GB", bytes as f64 / GB as f64)
552    } else if bytes >= MB {
553        format!("{:.2} MB", bytes as f64 / MB as f64)
554    } else if bytes >= KB {
555        format!("{:.2} KB", bytes as f64 / KB as f64)
556    } else {
557        format!("{} bytes", bytes)
558    }
559}
560
561#[cfg(test)]
562mod tests {
563    use crate::JpxEngine;
564    use serde_json::json;
565
566    #[test]
567    fn test_format_json() {
568        let engine = JpxEngine::new();
569
570        let formatted = engine.format_json(r#"{"a":1,"b":2}"#, 2).unwrap();
571        assert!(formatted.contains('\n'));
572
573        let compact = engine.format_json(r#"{"a":1,"b":2}"#, 0).unwrap();
574        assert!(!compact.contains('\n'));
575    }
576
577    #[test]
578    fn test_diff() {
579        let engine = JpxEngine::new();
580
581        let patch = engine.diff(r#"{"a": 1}"#, r#"{"a": 2}"#).unwrap();
582
583        let patch_arr = patch.as_array().unwrap();
584        assert!(!patch_arr.is_empty());
585    }
586
587    #[test]
588    fn test_patch() {
589        let engine = JpxEngine::new();
590
591        let result = engine
592            .patch(
593                r#"{"a": 1}"#,
594                r#"[{"op": "replace", "path": "/a", "value": 2}]"#,
595            )
596            .unwrap();
597
598        assert_eq!(result, json!({"a": 2}));
599    }
600
601    #[test]
602    fn test_merge() {
603        let engine = JpxEngine::new();
604
605        let result = engine
606            .merge(r#"{"a": 1, "b": 2}"#, r#"{"b": 3, "c": 4}"#)
607            .unwrap();
608
609        assert_eq!(result, json!({"a": 1, "b": 3, "c": 4}));
610    }
611
612    #[test]
613    fn test_keys() {
614        let engine = JpxEngine::new();
615
616        let keys = engine.keys(r#"{"a": 1, "b": {"c": 2}}"#, false).unwrap();
617        assert_eq!(keys, vec!["a", "b"]);
618
619        let recursive_keys = engine.keys(r#"{"a": 1, "b": {"c": 2}}"#, true).unwrap();
620        assert!(recursive_keys.contains(&"b.c".to_string()));
621    }
622
623    #[test]
624    fn test_paths() {
625        let engine = JpxEngine::new();
626
627        let paths = engine.paths(r#"{"a": 1}"#, true, false).unwrap();
628        assert!(!paths.is_empty());
629    }
630
631    #[test]
632    fn test_stats() {
633        let engine = JpxEngine::new();
634
635        let stats = engine.stats(r#"[1, 2, 3]"#).unwrap();
636        assert_eq!(stats.root_type, "array");
637        assert_eq!(stats.length, Some(3));
638    }
639
640    // =========================================================================
641    // format_json additional tests
642    // =========================================================================
643
644    #[test]
645    fn test_format_json_invalid_json() {
646        let engine = JpxEngine::new();
647        let result = engine.format_json("not json", 2);
648        assert!(result.is_err());
649    }
650
651    #[test]
652    fn test_format_json_indent_4() {
653        let engine = JpxEngine::new();
654        let formatted = engine.format_json(r#"{"a":1}"#, 4).unwrap();
655        // The second line should start with 4 spaces
656        let lines: Vec<&str> = formatted.lines().collect();
657        assert!(lines.len() > 1);
658        assert!(lines[1].starts_with("    "));
659    }
660
661    #[test]
662    fn test_format_json_preserves_data() {
663        let engine = JpxEngine::new();
664        let input = r#"{"name":"alice","age":30,"active":true,"score":null}"#;
665        let formatted = engine.format_json(input, 2).unwrap();
666        let original: serde_json::Value = serde_json::from_str(input).unwrap();
667        let roundtripped: serde_json::Value = serde_json::from_str(&formatted).unwrap();
668        assert_eq!(original, roundtripped);
669    }
670
671    // =========================================================================
672    // diff additional tests
673    // =========================================================================
674
675    #[test]
676    fn test_diff_identical() {
677        let engine = JpxEngine::new();
678        let patch = engine.diff(r#"{"a":1,"b":2}"#, r#"{"a":1,"b":2}"#).unwrap();
679        let patch_arr = patch.as_array().unwrap();
680        assert!(patch_arr.is_empty());
681    }
682
683    #[test]
684    fn test_diff_added_key() {
685        let engine = JpxEngine::new();
686        let patch = engine.diff(r#"{"a":1}"#, r#"{"a":1,"b":2}"#).unwrap();
687        let patch_arr = patch.as_array().unwrap();
688        assert!(!patch_arr.is_empty());
689        let has_add = patch_arr
690            .iter()
691            .any(|op| op.get("op").and_then(|v| v.as_str()) == Some("add"));
692        assert!(has_add, "Expected an 'add' operation in the patch");
693    }
694
695    #[test]
696    fn test_diff_removed_key() {
697        let engine = JpxEngine::new();
698        let patch = engine.diff(r#"{"a":1,"b":2}"#, r#"{"a":1}"#).unwrap();
699        let patch_arr = patch.as_array().unwrap();
700        assert!(!patch_arr.is_empty());
701        let has_remove = patch_arr
702            .iter()
703            .any(|op| op.get("op").and_then(|v| v.as_str()) == Some("remove"));
704        assert!(has_remove, "Expected a 'remove' operation in the patch");
705    }
706
707    #[test]
708    fn test_diff_nested_change() {
709        let engine = JpxEngine::new();
710        let patch = engine.diff(r#"{"a":{"b":1}}"#, r#"{"a":{"b":2}}"#).unwrap();
711        let patch_arr = patch.as_array().unwrap();
712        assert!(!patch_arr.is_empty());
713        // The operation should target the nested path /a/b
714        let targets_nested = patch_arr.iter().any(|op| {
715            op.get("path")
716                .and_then(|v| v.as_str())
717                .map(|p| p.contains("/a/b"))
718                .unwrap_or(false)
719        });
720        assert!(targets_nested, "Expected a patch operation targeting /a/b");
721    }
722
723    #[test]
724    fn test_diff_invalid_json() {
725        let engine = JpxEngine::new();
726        let result = engine.diff("not json", r#"{"a":1}"#);
727        assert!(result.is_err());
728    }
729
730    // =========================================================================
731    // patch additional tests
732    // =========================================================================
733
734    #[test]
735    fn test_patch_add_operation() {
736        let engine = JpxEngine::new();
737        let result = engine
738            .patch(r#"{"a":1}"#, r#"[{"op":"add","path":"/b","value":2}]"#)
739            .unwrap();
740        assert_eq!(result, json!({"a": 1, "b": 2}));
741    }
742
743    #[test]
744    fn test_patch_remove_operation() {
745        let engine = JpxEngine::new();
746        let result = engine
747            .patch(r#"{"a":1,"b":2}"#, r#"[{"op":"remove","path":"/b"}]"#)
748            .unwrap();
749        assert_eq!(result, json!({"a": 1}));
750    }
751
752    #[test]
753    fn test_patch_invalid_patch() {
754        let engine = JpxEngine::new();
755        let result = engine.patch(r#"{"a":1}"#, r#"not a patch"#);
756        assert!(result.is_err());
757    }
758
759    #[test]
760    fn test_patch_empty_patch() {
761        let engine = JpxEngine::new();
762        let result = engine.patch(r#"{"a":1,"b":2}"#, r#"[]"#).unwrap();
763        assert_eq!(result, json!({"a": 1, "b": 2}));
764    }
765
766    // =========================================================================
767    // merge additional tests
768    // =========================================================================
769
770    #[test]
771    fn test_merge_overlapping_keys() {
772        let engine = JpxEngine::new();
773        let result = engine
774            .merge(r#"{"a":1,"b":2}"#, r#"{"a":10,"b":20}"#)
775            .unwrap();
776        assert_eq!(result, json!({"a": 10, "b": 20}));
777    }
778
779    #[test]
780    fn test_merge_null_removes_key() {
781        let engine = JpxEngine::new();
782        let result = engine.merge(r#"{"a":1,"b":2}"#, r#"{"b":null}"#).unwrap();
783        assert_eq!(result, json!({"a": 1}));
784    }
785
786    #[test]
787    fn test_merge_nested_objects() {
788        let engine = JpxEngine::new();
789        let result = engine
790            .merge(r#"{"a":{"x":1,"y":2},"b":3}"#, r#"{"a":{"y":20,"z":30}}"#)
791            .unwrap();
792        assert_eq!(result, json!({"a": {"x": 1, "y": 20, "z": 30}, "b": 3}));
793    }
794
795    #[test]
796    fn test_merge_invalid_json() {
797        let engine = JpxEngine::new();
798        let result = engine.merge("not json", r#"{"a":1}"#);
799        assert!(result.is_err());
800    }
801
802    // =========================================================================
803    // keys additional tests
804    // =========================================================================
805
806    #[test]
807    fn test_keys_empty_object() {
808        let engine = JpxEngine::new();
809        let keys = engine.keys(r#"{}"#, false).unwrap();
810        assert!(keys.is_empty());
811    }
812
813    #[test]
814    fn test_keys_recursive_nested() {
815        let engine = JpxEngine::new();
816        let keys = engine.keys(r#"{"a":{"b":{"c":1}},"d":2}"#, true).unwrap();
817        assert!(keys.contains(&"a".to_string()));
818        assert!(keys.contains(&"a.b".to_string()));
819        assert!(keys.contains(&"a.b.c".to_string()));
820        assert!(keys.contains(&"d".to_string()));
821    }
822
823    #[test]
824    fn test_keys_non_object_non_recursive() {
825        let engine = JpxEngine::new();
826        // Arrays do not have string keys, so non-recursive should return empty
827        let keys = engine.keys(r#"[1, 2, 3]"#, false).unwrap();
828        assert!(keys.is_empty());
829    }
830
831    // =========================================================================
832    // paths additional tests
833    // =========================================================================
834
835    #[test]
836    fn test_paths_with_types_and_values() {
837        let engine = JpxEngine::new();
838        let paths = engine
839            .paths(r#"{"name":"alice","age":30}"#, true, true)
840            .unwrap();
841        // Find the "name" path
842        let name_path = paths.iter().find(|p| p.path == "name").unwrap();
843        assert_eq!(name_path.path_type, Some("string".to_string()));
844        assert_eq!(name_path.value, Some(json!("alice")));
845        // Find the "age" path
846        let age_path = paths.iter().find(|p| p.path == "age").unwrap();
847        assert_eq!(age_path.path_type, Some("number".to_string()));
848        assert_eq!(age_path.value, Some(json!(30)));
849    }
850
851    #[test]
852    fn test_paths_root_is_at_sign() {
853        let engine = JpxEngine::new();
854        let paths = engine.paths(r#"{"a":1}"#, false, false).unwrap();
855        assert!(!paths.is_empty());
856        assert_eq!(paths[0].path, "@");
857    }
858
859    #[test]
860    fn test_paths_array_indices() {
861        let engine = JpxEngine::new();
862        let paths = engine.paths(r#"[10, 20, 30]"#, true, true).unwrap();
863        // Root should be "@" with type "array"
864        assert_eq!(paths[0].path, "@");
865        assert_eq!(paths[0].path_type, Some("array".to_string()));
866        // Elements should be indexed as .0, .1, .2
867        let index_paths: Vec<&str> = paths.iter().map(|p| p.path.as_str()).collect();
868        assert!(index_paths.contains(&".0"));
869        assert!(index_paths.contains(&".1"));
870        assert!(index_paths.contains(&".2"));
871    }
872
873    // =========================================================================
874    // stats additional tests
875    // =========================================================================
876
877    #[test]
878    fn test_stats_object() {
879        let engine = JpxEngine::new();
880        let stats = engine
881            .stats(r#"{"name":"alice","age":30,"active":true}"#)
882            .unwrap();
883        assert_eq!(stats.root_type, "object");
884        assert_eq!(stats.key_count, Some(3));
885        assert!(stats.length.is_none());
886    }
887
888    #[test]
889    fn test_stats_empty_array() {
890        let engine = JpxEngine::new();
891        let stats = engine.stats(r#"[]"#).unwrap();
892        assert_eq!(stats.root_type, "array");
893        assert_eq!(stats.length, Some(0));
894        assert_eq!(stats.depth, 1);
895    }
896
897    #[test]
898    fn test_stats_array_of_objects_fields() {
899        let engine = JpxEngine::new();
900        let stats = engine
901            .stats(r#"[{"name":"alice","age":30},{"name":"bob","age":25}]"#)
902            .unwrap();
903        assert_eq!(stats.root_type, "array");
904        assert_eq!(stats.length, Some(2));
905        let fields = stats.fields.unwrap();
906        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
907        assert!(field_names.contains(&"name"));
908        assert!(field_names.contains(&"age"));
909    }
910
911    #[test]
912    fn test_stats_nested_depth() {
913        let engine = JpxEngine::new();
914        // 4 levels deep: root obj -> a obj -> b obj -> c obj -> value
915        let stats = engine.stats(r#"{"a":{"b":{"c":{"d":1}}}}"#).unwrap();
916        assert_eq!(stats.depth, 4);
917    }
918
919    #[test]
920    fn test_stats_invalid_json() {
921        let engine = JpxEngine::new();
922        let result = engine.stats("not json");
923        assert!(result.is_err());
924    }
925
926    // =========================================================================
927    // Private helper function tests
928    // =========================================================================
929
930    #[test]
931    fn test_calculate_depth_primitive() {
932        use super::calculate_depth;
933        assert_eq!(calculate_depth(&json!(42)), 0);
934        assert_eq!(calculate_depth(&json!("hello")), 0);
935        assert_eq!(calculate_depth(&json!(true)), 0);
936        assert_eq!(calculate_depth(&json!(null)), 0);
937    }
938
939    #[test]
940    fn test_calculate_depth_nested() {
941        use super::calculate_depth;
942        // 3 levels: obj -> obj -> obj -> primitive
943        let value = json!({"a": {"b": {"c": 1}}});
944        assert_eq!(calculate_depth(&value), 3);
945    }
946
947    #[test]
948    fn test_format_size_bytes() {
949        use super::format_size;
950        assert_eq!(format_size(100), "100 bytes");
951        assert_eq!(format_size(0), "0 bytes");
952        assert_eq!(format_size(1023), "1023 bytes");
953    }
954
955    #[test]
956    fn test_format_size_kb() {
957        use super::format_size;
958        let result = format_size(1024);
959        assert!(
960            result.contains("KB"),
961            "Expected KB in '{}' for 1024 bytes",
962            result
963        );
964        let result = format_size(2048);
965        assert!(
966            result.contains("KB"),
967            "Expected KB in '{}' for 2048 bytes",
968            result
969        );
970    }
971
972    #[test]
973    fn test_format_size_mb() {
974        use super::format_size;
975        let result = format_size(1024 * 1024);
976        assert!(result.contains("MB"), "Expected MB in '{}' for 1MB", result);
977        let result = format_size(2 * 1024 * 1024);
978        assert!(result.contains("MB"), "Expected MB in '{}' for 2MB", result);
979    }
980}