jmespath_native/
lib.rs

1use serde_json::Value::{self, Array, Null, Object};
2use std::{num::NonZeroIsize, ops, str};
3use thiserror::Error;
4
5#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
6pub struct JMESSlice {
7    pub start: Option<isize>,
8    pub end: Option<isize>,
9    pub step: Option<NonZeroIsize>,
10}
11
12#[derive(Debug, Error, PartialEq, Eq, Hash, Clone, Copy)]
13pub enum ParseJMESSliceError {
14    #[error("Invalid format")]
15    InvalidFormat,
16    #[error("Step not allowed to be Zero")]
17    StepNotAllowedToBeZero,
18}
19
20impl str::FromStr for JMESSlice {
21    type Err = ParseJMESSliceError;
22
23    fn from_str(s: &str) -> Result<Self, Self::Err> {
24        use ParseJMESSliceError::{InvalidFormat, StepNotAllowedToBeZero};
25        let (_whole, start, end, _colon, step) = lazy_regex::regex_captures!(
26            r"^(?P<start>-?\d+)?:(?P<end>-?\d+)?(:(?P<step>-?\d+)?)?$",
27            s
28        )
29        .ok_or(InvalidFormat)?;
30        let option_isize = |s| match s {
31            "" => None,
32            s => Some(s.parse::<isize>().expect("Regex ensures valid")),
33        };
34        let ok = Self {
35            start: option_isize(start),
36            end: option_isize(end),
37            step: match option_isize(step) {
38                Some(i) => Some(NonZeroIsize::new(i).ok_or(StepNotAllowedToBeZero)?),
39                None => None,
40            },
41        };
42        Ok(ok)
43    }
44}
45
46impl From<ops::Range<isize>> for JMESSlice {
47    fn from(range: ops::Range<isize>) -> Self {
48        Self {
49            start: Some(range.start),
50            end: Some(range.end),
51            step: None,
52        }
53    }
54}
55impl From<ops::RangeFrom<isize>> for JMESSlice {
56    fn from(range: ops::RangeFrom<isize>) -> Self {
57        Self {
58            start: Some(range.start),
59            ..Default::default()
60        }
61    }
62}
63impl From<ops::RangeTo<isize>> for JMESSlice {
64    fn from(range: ops::RangeTo<isize>) -> Self {
65        Self {
66            end: Some(range.end),
67            ..Default::default()
68        }
69    }
70}
71
72pub trait JMESPath: Sized {
73    fn identify(self, key: impl AsRef<str>) -> Self;
74    fn index(self, index: isize) -> Self;
75    fn slice(self, slice: impl Into<JMESSlice>) -> Self;
76    fn list_project(self, projection: impl Fn(Self) -> Self) -> Self;
77    fn slice_project(self, slice: impl Into<JMESSlice>, projection: impl Fn(Self) -> Self) -> Self;
78    fn object_project(self, projection: impl Fn(Self) -> Self) -> Self;
79    fn flatten(self) -> Self;
80    fn flatten_project(self, projection: impl Fn(Self) -> Self) -> Self;
81}
82
83impl JMESPath for Value {
84    fn identify(self, key: impl AsRef<str>) -> Self {
85        match self {
86            Object(mut map) => map.remove(key.as_ref()).unwrap_or(Null),
87            _ => Null,
88        }
89    }
90
91    fn index(self, index: isize) -> Self {
92        match self {
93            Array(mut vec) => {
94                let index = if index.is_negative() {
95                    // Get the index from the back
96                    match vec.len().checked_sub(index.unsigned_abs()) {
97                        Some(u) => u,
98                        None => return Null,
99                    }
100                } else {
101                    index.unsigned_abs()
102                };
103                if index < vec.len() {
104                    vec.remove(index)
105                } else {
106                    Null // OOB
107                }
108            }
109            _ => Null,
110        }
111    }
112
113    fn slice(self, slice: impl Into<JMESSlice>) -> Self {
114        use slyce::{Index, Slice}; // Slicing makes my head hurt, use a library
115        let slice: JMESSlice = slice.into();
116        match self {
117            Array(vec) => {
118                let op = Slice {
119                    start: match slice.start {
120                        Some(i) if i.is_negative() => Index::Tail(i.unsigned_abs()),
121                        Some(i) => Index::Head(i.unsigned_abs()),
122                        None => Index::Default,
123                    },
124                    end: match slice.end {
125                        Some(i) if i.is_negative() => Index::Tail(i.unsigned_abs()),
126                        Some(i) => Index::Head(i.unsigned_abs()),
127                        None => Index::Default,
128                    },
129                    step: slice.step.map(isize::from),
130                };
131                Array(op.apply(&vec).map(Clone::clone).collect())
132            }
133            _ => Null,
134        }
135    }
136
137    fn list_project(self, projection: impl Fn(Self) -> Self) -> Self {
138        match self {
139            Array(vec) => Array(
140                vec.into_iter()
141                    .map(projection)
142                    .filter(|value| !value.is_null())
143                    .collect(),
144            ),
145            _ => Null,
146        }
147    }
148
149    fn slice_project(self, slice: impl Into<JMESSlice>, projection: impl Fn(Self) -> Self) -> Self {
150        match self {
151            Array(_) => self.slice(slice).list_project(projection),
152            _ => Null,
153        }
154    }
155
156    fn object_project(self, projection: impl Fn(Self) -> Self) -> Self {
157        match self {
158            Object(map) => Array(
159                map.into_iter()
160                    .map(|(_key, value)| value)
161                    .map(projection)
162                    .filter(|value| !value.is_null())
163                    .collect(),
164            ),
165            _ => Null,
166        }
167    }
168
169    fn flatten(self) -> Self {
170        match self {
171            Array(vec) => {
172                let mut results = Vec::new();
173                for result in vec.into_iter() {
174                    match result {
175                        Array(mut inner) => results.append(&mut inner),
176                        other => results.push(other),
177                    }
178                }
179                Array(results)
180            }
181            _ => Null,
182        }
183    }
184
185    fn flatten_project(self, projection: impl Fn(Self) -> Self) -> Self {
186        println!("self = {:?}", self);
187        match self {
188            // This is the LHS
189            Array(current) => {
190                // Create an empty result list.
191                let mut results = Vec::new();
192                // Iterate over the elements of the current result.
193                for element in current {
194                    match element {
195                        // If the current element is a list, add each element of the current element to the end of the result list.
196                        Array(mut a_list) => results.append(&mut a_list),
197                        // If the current element is not a list, add to the end of the result list.
198                        other => results.push(other),
199                    }
200                }
201                // The result list is now the new current result.
202                // Once the flattening operation has been performed, subsequent operations are projected onto the flattened list with the same semantics as a wildcard expression. Thus the difference between [*] and [] is that [] will first flatten sublists in the current result.
203                println!("results = {:?}", results);
204                Array(results).list_project(projection)
205            }
206            _ => Null,
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use serde_json::json;
215
216    fn flatmap() -> Value {
217        json!({"a": "foo", "b": "bar", "c": "baz"})
218    }
219    fn nested_map() -> Value {
220        json!({"a": {"b": {"c": {"d": "value"}}}})
221    }
222
223    #[test]
224    fn identifier() {
225        assert_eq!(flatmap().identify("a"), json!("foo"));
226        assert_eq!(flatmap().identify("d"), json!(null));
227        assert_eq!(
228            nested_map()
229                .identify("a")
230                .identify("b")
231                .identify("c")
232                .identify("d"),
233            json!("value")
234        )
235    }
236    fn array() -> Value {
237        json!(["a", "b", "c", "d", "e", "f"])
238    }
239
240    #[test]
241    fn index() {
242        assert_eq!(array().index(1), json!("b"));
243        assert_eq!(array().index(-1), json!("f"));
244        assert_eq!(array().index(10), json!(null));
245        assert_eq!(array().index(-10), json!(null));
246    }
247
248    fn complex() -> Value {
249        json!({"a": {
250          "b": {
251            "c": [
252              {"d": [0, [1, 2]]},
253              {"d": [3, 4]}
254            ]
255          }
256        }})
257    }
258
259    #[test]
260    fn combined() {
261        assert_eq!(
262            complex()
263                .identify("a")
264                .identify("b")
265                .identify("c")
266                .index(0)
267                .identify("d")
268                .index(1)
269                .index(0),
270            json!(1)
271        )
272    }
273    #[test]
274    fn parse_jmes_slice() {
275        let res = "::".parse::<JMESSlice>();
276        assert_eq!(res, Ok(JMESSlice::default()));
277        let res = "0:1".parse::<JMESSlice>();
278        assert_eq!(res, Ok((0..1).into()));
279        let res = "-10:".parse::<JMESSlice>();
280        assert_eq!(res, Ok((-10..).into()));
281        let res = ":100".parse::<JMESSlice>();
282        assert_eq!(res, Ok((..100).into()));
283        let res = "::10".parse::<JMESSlice>();
284        assert_eq!(
285            res,
286            Ok(JMESSlice {
287                start: None,
288                end: None,
289                step: Some(NonZeroIsize::new(10).unwrap())
290            })
291        );
292        let res = "::0".parse::<JMESSlice>();
293        assert_eq!(res, Err(ParseJMESSliceError::StepNotAllowedToBeZero));
294    }
295
296    fn slice_example() -> Value {
297        json!([0, 1, 2, 3])
298    }
299    #[test]
300    fn slicing() -> anyhow::Result<()> {
301        assert_eq!(
302            slice_example().slice("0:4:1".parse::<JMESSlice>()?),
303            json!([0, 1, 2, 3])
304        );
305        assert_eq!(
306            slice_example().slice("0:4".parse::<JMESSlice>()?),
307            json!([0, 1, 2, 3])
308        );
309        assert_eq!(
310            slice_example().slice("0:3".parse::<JMESSlice>()?),
311            json!([0, 1, 2])
312        );
313        assert_eq!(
314            slice_example().slice(":2".parse::<JMESSlice>()?),
315            json!([0, 1])
316        );
317        assert_eq!(
318            slice_example().slice("::2".parse::<JMESSlice>()?),
319            json!([0, 2])
320        );
321        assert_eq!(
322            slice_example().slice("::-1".parse::<JMESSlice>()?),
323            json!([3, 2, 1, 0]),
324        );
325        assert_eq!(
326            slice_example().slice("-2:".parse::<JMESSlice>()?),
327            json!([2, 3])
328        );
329        assert_eq!(
330            slice_example().slice("100::-1".parse::<JMESSlice>()?),
331            json!([3, 2, 1, 0])
332        );
333        Ok(())
334    }
335
336    fn list_project_example() -> Value {
337        json!({
338          "people": [
339            {"first": "James", "last": "d"},
340            {"first": "Jacob", "last": "e"},
341            {"first": "Jayden", "last": "f"},
342            {"missing": "different"}
343          ],
344          "foo": {"bar": "baz"}
345        })
346    }
347
348    #[test]
349    fn list_projection() {
350        assert_eq!(
351            list_project_example()
352                .identify("people")
353                .list_project(|v| v.identify("first")),
354            json!(["James", "Jacob", "Jayden"])
355        );
356    }
357
358    #[test]
359    fn slice_projection() {
360        assert_eq!(
361            list_project_example()
362                .identify("people")
363                .slice_project(":2".parse::<JMESSlice>().unwrap(), |v| v.identify("first")),
364            json!(["James", "Jacob"])
365        );
366    }
367
368    fn object_projection_example() -> Value {
369        json!({
370          "ops": {
371            "functionA": {"numArgs": 2},
372            "functionB": {"numArgs": 3},
373            "functionC": {"variadic": true}
374          }
375        })
376    }
377
378    #[test]
379    fn object_projection() {
380        assert_eq!(
381            object_projection_example()
382                .identify("ops")
383                .object_project(|v| v.identify("numArgs")),
384            json!([2, 3])
385        )
386    }
387
388    fn flatten_projection_example() -> Value {
389        json!({
390          "reservations": [
391            {
392              "instances": [
393                {"state": "running"},
394                {"state": "stopped"}
395              ]
396            },
397            {
398              "instances": [
399                {"state": "terminated"},
400                {"state": "running"}
401              ]
402            }
403          ]
404        })
405    }
406
407    #[test]
408    fn flatten_projection() {
409        assert_eq!(
410            flatten_projection_example()
411                .identify("reservations")
412                .list_project(|v| v
413                    .identify("instances")
414                    .list_project(|v| v.identify("state"))), // reservations[*].instances[*].state
415            json!([["running", "stopped"], ["terminated", "running"]])
416        );
417        assert_eq!(
418            flatten_projection_example()
419                .identify("reservations")
420                .list_project(|v| v
421                    .identify("instances")
422                    .flatten_project(|v| v.identify("state"))), // reservations[*].instances[].state
423            json!(["running", "stopped", "terminated", "running"]),
424        );
425    }
426
427    fn nested_list_example() -> Value {
428        json!([[0, 1], 2, [3], 4, [5, [6, 7]]])
429    }
430
431    #[test]
432    fn flatten_project_nested_list() {
433        assert_eq!(
434            nested_list_example().flatten_project(|v| v),
435            json!([0, 1, 2, 3, 4, 5, [6, 7]])
436        );
437        assert_eq!(
438            nested_list_example()
439                .flatten_project(|v| v)
440                .flatten_project(|v| v), // Why isn't this nested?
441            json!([0, 1, 2, 3, 4, 5, 6, 7]),
442        )
443    }
444
445    fn objects_in_nested_list() -> Value {
446        json!([
447            {"name": "Seattle", "state": "WA"},
448            {"name": "New York", "state": "NY"},
449            [
450                {"name": "Bellevue", "state": "WA"},
451                {"name": "Olympia", "state": "WA"}
452            ]
453        ])
454    }
455
456    #[test]
457    fn test_flatten_objects_in_nested_list() {
458        assert_eq!(
459            objects_in_nested_list().flatten_project(|v| v.identify("name")),
460            json!(["Seattle", "New York", "Bellevue", "Olympia"])
461        )
462    }
463
464    #[test]
465    fn running() {
466        let program = JMESProgram::new("hello.world").unwrap();
467        assert_eq!(program.run(json!({"hello":{"world": 1}})), json!(1))
468    }
469}
470
471/// Only supports identify at the moment
472// TODO pest etc
473#[derive(Debug, Clone)]
474pub struct JMESProgram(String);
475
476impl JMESProgram {
477    pub fn new(program: impl AsRef<str>) -> anyhow::Result<Self> {
478        Ok(Self(program.as_ref().into()))
479    }
480
481    pub fn run(&self, mut input: Value) -> Value {
482        for path in self.0.split('.') {
483            input = input.identify(path)
484        }
485        input
486    }
487}
488
489// impl Fn(Value) -> Value for JMESProgram {
490//     extern "rust-call" fn call(&self, args: Args) -> Self::Output {
491//         todo!()
492//     }
493// }
494// Could also compile as Box<dyn Fn(Value) -> Value>