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 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 }
108 }
109 _ => Null,
110 }
111 }
112
113 fn slice(self, slice: impl Into<JMESSlice>) -> Self {
114 use slyce::{Index, Slice}; 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 Array(current) => {
190 let mut results = Vec::new();
192 for element in current {
194 match element {
195 Array(mut a_list) => results.append(&mut a_list),
197 other => results.push(other),
199 }
200 }
201 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"))), 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"))), 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), 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#[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