Skip to main content

jpx_core/
value_ext.rs

1//! Extension trait for `serde_json::Value` providing JMESPath operations.
2//!
3//! This replaces the 2,719-line `variable.rs` in jmespath.rs with ~200 lines
4//! by leveraging `Value`'s built-in API.
5
6use serde_json::Value;
7
8use crate::ast::Comparator;
9
10/// JMESPath type names used in error messages and the `type()` function.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum JmespathType {
13    Null,
14    String,
15    Number,
16    Boolean,
17    Array,
18    Object,
19    Expref,
20}
21
22impl std::fmt::Display for JmespathType {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            JmespathType::Null => write!(f, "null"),
26            JmespathType::String => write!(f, "string"),
27            JmespathType::Number => write!(f, "number"),
28            JmespathType::Boolean => write!(f, "boolean"),
29            JmespathType::Array => write!(f, "array"),
30            JmespathType::Object => write!(f, "object"),
31            JmespathType::Expref => write!(f, "expref"),
32        }
33    }
34}
35
36/// Extension trait providing JMESPath operations on `serde_json::Value`.
37pub trait ValueExt {
38    /// Returns the JMESPath type name.
39    fn jmespath_type(&self) -> JmespathType;
40
41    /// Returns true if the value is "truthy" per the JMESPath spec.
42    ///
43    /// - `null` is falsy
44    /// - `false` is falsy
45    /// - `""` (empty string) is falsy
46    /// - `[]` (empty array) is falsy
47    /// - `{}` (empty object) is falsy
48    /// - Everything else is truthy
49    fn is_truthy(&self) -> bool;
50
51    /// Extracts a named field from an object; returns `Value::Null` if not found.
52    fn get_field(&self, name: &str) -> Value;
53
54    /// Extracts an element by positive index; returns `Value::Null` if out of range.
55    fn get_index(&self, idx: usize) -> Value;
56
57    /// Extracts an element by negative index (counting from end).
58    fn get_negative_index(&self, idx: usize) -> Value;
59
60    /// Slices an array with start/stop/step, per the JMESPath spec.
61    /// Returns `None` if the value is not an array.
62    fn slice(&self, start: Option<i32>, stop: Option<i32>, step: i32) -> Option<Vec<Value>>;
63
64    /// Compares two values using a JMESPath comparator.
65    /// Returns `None` if the comparison is not valid for these types.
66    fn compare(&self, comparator: &Comparator, other: &Value) -> Option<bool>;
67
68    /// Returns `true` if this is an expref sentinel.
69    fn is_expref(&self) -> bool;
70}
71
72impl ValueExt for Value {
73    fn jmespath_type(&self) -> JmespathType {
74        if self.is_expref() {
75            return JmespathType::Expref;
76        }
77        match self {
78            Value::Null => JmespathType::Null,
79            Value::Bool(_) => JmespathType::Boolean,
80            Value::Number(_) => JmespathType::Number,
81            Value::String(_) => JmespathType::String,
82            Value::Array(_) => JmespathType::Array,
83            Value::Object(_) => JmespathType::Object,
84        }
85    }
86
87    fn is_truthy(&self) -> bool {
88        match self {
89            Value::Null => false,
90            Value::Bool(b) => *b,
91            Value::String(s) => !s.is_empty(),
92            Value::Array(a) => !a.is_empty(),
93            Value::Object(o) => !o.is_empty(),
94            Value::Number(_) => true,
95        }
96    }
97
98    fn get_field(&self, name: &str) -> Value {
99        match self {
100            Value::Object(map) => map.get(name).cloned().unwrap_or(Value::Null),
101            _ => Value::Null,
102        }
103    }
104
105    fn get_index(&self, idx: usize) -> Value {
106        match self {
107            Value::Array(arr) => arr.get(idx).cloned().unwrap_or(Value::Null),
108            _ => Value::Null,
109        }
110    }
111
112    fn get_negative_index(&self, idx: usize) -> Value {
113        match self {
114            Value::Array(arr) => {
115                if idx > arr.len() {
116                    Value::Null
117                } else {
118                    arr.get(arr.len() - idx).cloned().unwrap_or(Value::Null)
119                }
120            }
121            _ => Value::Null,
122        }
123    }
124
125    fn slice(&self, start: Option<i32>, stop: Option<i32>, step: i32) -> Option<Vec<Value>> {
126        let arr = self.as_array()?;
127        // Slice endpoints are i32 (matching the AST); clamp rather than letting
128        // `arr.len() as i32` wrap negative for arrays larger than i32::MAX (which
129        // would then panic on `arr[i as usize]`). Such arrays are unrealistic but
130        // the cast must not be unchecked.
131        let len = i32::try_from(arr.len()).unwrap_or(i32::MAX);
132        if len == 0 {
133            return Some(vec![]);
134        }
135
136        let a: i32 = match start {
137            Some(s) => adjust_slice_endpoint(len, s, step),
138            _ if step < 0 => len - 1,
139            _ => 0,
140        };
141        let b: i32 = match stop {
142            Some(s) => adjust_slice_endpoint(len, s, step),
143            _ if step < 0 => -1,
144            _ => len,
145        };
146
147        let mut result = Vec::new();
148        let mut i = a;
149        if step > 0 {
150            while i < b {
151                result.push(arr[i as usize].clone());
152                i += step;
153            }
154        } else {
155            while i > b {
156                result.push(arr[i as usize].clone());
157                i += step;
158            }
159        }
160        Some(result)
161    }
162
163    fn compare(&self, comparator: &Comparator, other: &Value) -> Option<bool> {
164        match comparator {
165            Comparator::Equal => Some(values_equal(self, other)),
166            Comparator::NotEqual => Some(!values_equal(self, other)),
167            Comparator::LessThan => compare_ordered(self, other).map(|o| o.is_lt()),
168            Comparator::LessThanEqual => compare_ordered(self, other).map(|o| o.is_le()),
169            Comparator::GreaterThan => compare_ordered(self, other).map(|o| o.is_gt()),
170            Comparator::GreaterThanEqual => compare_ordered(self, other).map(|o| o.is_ge()),
171        }
172    }
173
174    fn is_expref(&self) -> bool {
175        matches!(self, Value::Object(map) if map.contains_key(crate::EXPREF_KEY.as_str()))
176    }
177}
178
179/// Adjusts a slice endpoint per the JMESPath spec, accounting for step direction.
180fn adjust_slice_endpoint(len: i32, mut endpoint: i32, step: i32) -> i32 {
181    if endpoint < 0 {
182        endpoint += len;
183        if endpoint >= 0 {
184            endpoint
185        } else if step < 0 {
186            -1
187        } else {
188            0
189        }
190    } else if endpoint < len {
191        endpoint
192    } else if step < 0 {
193        len - 1
194    } else {
195        len
196    }
197}
198
199/// Equality comparison per JMESPath spec.
200fn values_equal(a: &Value, b: &Value) -> bool {
201    match (a, b) {
202        (Value::Null, Value::Null) => true,
203        (Value::Bool(a), Value::Bool(b)) => a == b,
204        (Value::Number(a), Value::Number(b)) => {
205            // Compare as f64 to handle integer/float comparison
206            a.as_f64().zip(b.as_f64()).is_some_and(|(af, bf)| af == bf)
207        }
208        (Value::String(a), Value::String(b)) => a == b,
209        (Value::Array(a), Value::Array(b)) => {
210            a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| values_equal(x, y))
211        }
212        (Value::Object(a), Value::Object(b)) => {
213            a.len() == b.len()
214                && a.iter()
215                    .all(|(k, v)| b.get(k).is_some_and(|bv| values_equal(v, bv)))
216        }
217        _ => false,
218    }
219}
220
221/// Ordering comparison per JMESPath spec - only numbers and strings.
222fn compare_ordered(a: &Value, b: &Value) -> Option<std::cmp::Ordering> {
223    match (a, b) {
224        (Value::Number(a), Value::Number(b)) => {
225            let af = a.as_f64()?;
226            let bf = b.as_f64()?;
227            af.partial_cmp(&bf)
228        }
229        (Value::String(a), Value::String(b)) => Some(a.cmp(b)),
230        _ => None,
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use serde_json::json;
238
239    #[test]
240    fn test_truthy() {
241        assert!(!Value::Null.is_truthy());
242        assert!(!Value::Bool(false).is_truthy());
243        assert!(Value::Bool(true).is_truthy());
244        assert!(!Value::String("".into()).is_truthy());
245        assert!(Value::String("hi".into()).is_truthy());
246        assert!(!json!([]).is_truthy());
247        assert!(json!([1]).is_truthy());
248        assert!(!json!({}).is_truthy());
249        assert!(json!({"a": 1}).is_truthy());
250        assert!(json!(0).is_truthy());
251        assert!(json!(42).is_truthy());
252    }
253
254    #[test]
255    fn test_get_field() {
256        let obj = json!({"foo": "bar"});
257        assert_eq!(obj.get_field("foo"), json!("bar"));
258        assert_eq!(obj.get_field("missing"), Value::Null);
259        assert_eq!(Value::Null.get_field("x"), Value::Null);
260    }
261
262    #[test]
263    fn test_get_index() {
264        let arr = json!([10, 20, 30]);
265        assert_eq!(arr.get_index(0), json!(10));
266        assert_eq!(arr.get_index(2), json!(30));
267        assert_eq!(arr.get_index(5), Value::Null);
268    }
269
270    #[test]
271    fn test_get_negative_index() {
272        let arr = json!([10, 20, 30]);
273        assert_eq!(arr.get_negative_index(1), json!(30));
274        assert_eq!(arr.get_negative_index(3), json!(10));
275        assert_eq!(arr.get_negative_index(4), Value::Null);
276    }
277
278    #[test]
279    fn test_slice() {
280        let arr = json!([0, 1, 2, 3, 4, 5]);
281        assert_eq!(
282            arr.slice(Some(1), Some(4), 1),
283            Some(vec![json!(1), json!(2), json!(3)])
284        );
285        assert_eq!(
286            arr.slice(None, None, 2),
287            Some(vec![json!(0), json!(2), json!(4)])
288        );
289        assert_eq!(
290            arr.slice(None, None, -1),
291            Some(vec![
292                json!(5),
293                json!(4),
294                json!(3),
295                json!(2),
296                json!(1),
297                json!(0)
298            ])
299        );
300    }
301
302    #[test]
303    fn test_compare() {
304        assert_eq!(json!(1).compare(&Comparator::Equal, &json!(1)), Some(true));
305        assert_eq!(
306            json!(1).compare(&Comparator::LessThan, &json!(2)),
307            Some(true)
308        );
309        assert_eq!(
310            json!("a").compare(&Comparator::GreaterThan, &json!("b")),
311            Some(false)
312        );
313        // Mixed types return None for ordering
314        assert_eq!(json!(1).compare(&Comparator::LessThan, &json!("a")), None);
315        // But equal/notequal work across types
316        assert_eq!(
317            json!(1).compare(&Comparator::Equal, &json!("a")),
318            Some(false)
319        );
320    }
321
322    #[test]
323    fn test_jmespath_type() {
324        assert_eq!(Value::Null.jmespath_type(), JmespathType::Null);
325        assert_eq!(json!(true).jmespath_type(), JmespathType::Boolean);
326        assert_eq!(json!(42).jmespath_type(), JmespathType::Number);
327        assert_eq!(json!("s").jmespath_type(), JmespathType::String);
328        assert_eq!(json!([]).jmespath_type(), JmespathType::Array);
329        assert_eq!(json!({}).jmespath_type(), JmespathType::Object);
330    }
331}