Skip to main content

nodedb_query/
json_ops.rs

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