Skip to main content

nodedb_query/
json_ops.rs

1//! Shared JSON value operations: comparison, coercion, truthiness.
2//!
3//! Used by both expression evaluation (computed projections) and scan filters
4//! (WHERE predicate evaluation). Pure functions on `serde_json::Value`.
5
6use std::cmp::Ordering;
7
8/// Coerce a JSON value to f64.
9///
10/// - Numbers: `as_f64()` directly
11/// - Strings: parse as f64 (`"5"` → `5.0`)
12/// - Booleans: `true` → `1.0`, `false` → `0.0` (when `coerce_bool` is true)
13/// - Other types: `None`
14pub fn json_to_f64(v: &serde_json::Value, coerce_bool: bool) -> Option<f64> {
15    match v {
16        serde_json::Value::Number(n) => n.as_f64(),
17        serde_json::Value::String(s) => s.parse::<f64>().ok(),
18        serde_json::Value::Bool(b) if coerce_bool => Some(if *b { 1.0 } else { 0.0 }),
19        _ => None,
20    }
21}
22
23/// Compare two JSON values with type coercion.
24///
25/// Tries numeric comparison first (with bool coercion), then falls
26/// back to string comparison.
27pub fn compare_json(a: &serde_json::Value, b: &serde_json::Value) -> Ordering {
28    // Try numeric comparison with bool coercion.
29    if let (Some(na), Some(nb)) = (json_to_f64(a, true), json_to_f64(b, true)) {
30        return na.partial_cmp(&nb).unwrap_or(Ordering::Equal);
31    }
32    // Fallback: string comparison.
33    let sa = json_to_display_string(a);
34    let sb = json_to_display_string(b);
35    sa.cmp(&sb)
36}
37
38/// Compare two optional JSON values (for scan_filter compatibility).
39pub fn compare_json_optional(
40    a: Option<&serde_json::Value>,
41    b: Option<&serde_json::Value>,
42) -> Ordering {
43    match (a, b) {
44        (None, None) => Ordering::Equal,
45        (None, Some(_)) => Ordering::Less,
46        (Some(_), None) => Ordering::Greater,
47        (Some(a), Some(b)) => compare_json(a, b),
48    }
49}
50
51/// Check equality with type coercion.
52///
53/// Handles `"5" == 5` by coercing both sides to f64 when one is a
54/// number and the other is a numeric string.
55pub fn coerced_eq(a: &serde_json::Value, b: &serde_json::Value) -> bool {
56    if a == b {
57        return true;
58    }
59    if let (Some(af), Some(bf)) = (json_to_f64(a, true), json_to_f64(b, true)) {
60        return (af - bf).abs() < f64::EPSILON;
61    }
62    false
63}
64
65/// Check if a JSON value is truthy (for boolean contexts).
66///
67/// - `true` → true, `false` → false
68/// - `null` → false
69/// - Numbers: non-zero → true
70/// - Strings: non-empty → true
71/// - Arrays/Objects: always true
72pub fn is_truthy(v: &serde_json::Value) -> bool {
73    match v {
74        serde_json::Value::Bool(b) => *b,
75        serde_json::Value::Null => false,
76        serde_json::Value::Number(n) => n.as_f64().unwrap_or(0.0) != 0.0,
77        serde_json::Value::String(s) => !s.is_empty(),
78        _ => true,
79    }
80}
81
82/// Convert a JSON value to a display string.
83///
84/// - Strings: returned as-is (no quotes)
85/// - Null: empty string
86/// - Numbers/Bools: `.to_string()`
87/// - Objects/Arrays: JSON serialization
88pub fn json_to_display_string(v: &serde_json::Value) -> String {
89    match v {
90        serde_json::Value::String(s) => s.clone(),
91        serde_json::Value::Null => String::new(),
92        serde_json::Value::Number(n) => n.to_string(),
93        serde_json::Value::Bool(b) => b.to_string(),
94        other => other.to_string(),
95    }
96}
97
98/// Convert a f64 to a JSON number, preferring integer representation.
99///
100/// Returns `Null` for NaN/Infinity.
101pub fn to_json_number(n: f64) -> serde_json::Value {
102    if n.fract() == 0.0 && n.abs() < i64::MAX as f64 {
103        serde_json::Value::Number(serde_json::Number::from(n as i64))
104    } else {
105        serde_json::Number::from_f64(n)
106            .map(serde_json::Value::Number)
107            .unwrap_or(serde_json::Value::Null)
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use serde_json::json;
115
116    #[test]
117    fn coerced_eq_mixed_types() {
118        assert!(coerced_eq(&json!(5), &json!("5")));
119        assert!(coerced_eq(&json!(3.15), &json!("3.15")));
120        assert!(!coerced_eq(&json!(5), &json!("6")));
121    }
122
123    #[test]
124    fn coerced_eq_bool_numeric() {
125        assert!(coerced_eq(&json!(true), &json!(1)));
126        assert!(coerced_eq(&json!(false), &json!(0)));
127        assert!(!coerced_eq(&json!(true), &json!(0)));
128    }
129
130    #[test]
131    fn compare_numeric_coercion() {
132        assert_eq!(
133            compare_json(&json!(5), &json!("4")),
134            std::cmp::Ordering::Greater
135        );
136        assert_eq!(
137            compare_json(&json!("10"), &json!(9)),
138            std::cmp::Ordering::Greater
139        );
140    }
141
142    #[test]
143    fn truthiness() {
144        assert!(is_truthy(&json!(true)));
145        assert!(!is_truthy(&json!(false)));
146        assert!(!is_truthy(&json!(null)));
147        assert!(is_truthy(&json!(1)));
148        assert!(!is_truthy(&json!(0)));
149        assert!(is_truthy(&json!("hello")));
150        assert!(!is_truthy(&json!("")));
151    }
152
153    #[test]
154    fn to_json_number_nan() {
155        assert_eq!(to_json_number(f64::NAN), serde_json::Value::Null);
156    }
157
158    #[test]
159    fn to_json_number_integer() {
160        assert_eq!(to_json_number(42.0), json!(42));
161    }
162
163    #[test]
164    fn to_json_number_float() {
165        assert_eq!(to_json_number(3.15), json!(3.15));
166    }
167}