Skip to content

Commit eda5b29

Browse files
authored
Merge pull request #48 from akoshchiy/12-json-path-support
feat(jsonpath): add exists filter expression
2 parents 3fe3acd + 7dc93b3 commit eda5b29

File tree

6 files changed

+254
-11
lines changed

6 files changed

+254
-11
lines changed

src/jsonpath/parser.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,9 +361,44 @@ fn expr_atom(input: &[u8], root_predicate: bool) -> IResult<&[u8], Expr<'_>> {
361361
),
362362
|expr| expr,
363363
),
364+
map(filter_func, Expr::FilterFunc),
364365
))(input)
365366
}
366367

368+
fn filter_func(input: &[u8]) -> IResult<&[u8], FilterFunc<'_>> {
369+
alt((exists,))(input)
370+
}
371+
372+
fn exists(input: &[u8]) -> IResult<&[u8], FilterFunc<'_>> {
373+
preceded(
374+
tag("exists"),
375+
preceded(
376+
multispace0,
377+
delimited(
378+
terminated(char('('), multispace0),
379+
map(exists_paths, FilterFunc::Exists),
380+
preceded(multispace0, char(')')),
381+
),
382+
),
383+
)(input)
384+
}
385+
386+
fn exists_paths(input: &[u8]) -> IResult<&[u8], Vec<Path<'_>>> {
387+
map(
388+
pair(
389+
alt((
390+
value(Path::Root, char('$')),
391+
value(Path::Current, char('@')),
392+
)),
393+
many0(path),
394+
),
395+
|(pre, mut paths)| {
396+
paths.insert(0, pre);
397+
paths
398+
},
399+
)(input)
400+
}
401+
367402
fn expr_and(input: &[u8], root_predicate: bool) -> IResult<&[u8], Expr<'_>> {
368403
map(
369404
separated_list1(delimited(multispace0, tag("&&"), multispace0), |i| {

src/jsonpath/path.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ pub enum Expr<'a> {
133133
left: Box<Expr<'a>>,
134134
right: Box<Expr<'a>>,
135135
},
136+
/// Filter function, returns a boolean value.
137+
FilterFunc(FilterFunc<'a>),
138+
}
139+
140+
/// Represents filter function, returns a boolean value.
141+
#[derive(Debug, Clone, PartialEq)]
142+
pub enum FilterFunc<'a> {
143+
Exists(Vec<Path<'a>>),
136144
}
137145

138146
impl<'a> Display for JsonPath<'a> {
@@ -312,6 +320,15 @@ impl<'a> Display for Expr<'a> {
312320
write!(f, "{right}")?;
313321
}
314322
}
323+
Expr::FilterFunc(func) => match func {
324+
FilterFunc::Exists(paths) => {
325+
f.write_str("exists(")?;
326+
for path in paths {
327+
write!(f, "{path}")?;
328+
}
329+
f.write_str(")")?;
330+
}
331+
},
315332
}
316333
Ok(())
317334
}

src/jsonpath/selector.rs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::constants::*;
2323
use crate::jsonpath::ArrayIndex;
2424
use crate::jsonpath::BinaryOperator;
2525
use crate::jsonpath::Expr;
26+
use crate::jsonpath::FilterFunc;
2627
use crate::jsonpath::Index;
2728
use crate::jsonpath::JsonPath;
2829
use crate::jsonpath::Path;
@@ -74,7 +75,7 @@ impl<'a> Selector<'a> {
7475
}
7576

7677
pub fn select(&'a self, root: &'a [u8], data: &mut Vec<u8>, offsets: &mut Vec<u64>) {
77-
let mut poses = self.find_positions(root);
78+
let mut poses = self.find_positions(root, None, &self.json_path.paths);
7879

7980
if self.json_path.is_predicate() {
8081
Self::build_predicate_result(&mut poses, data);
@@ -102,28 +103,38 @@ impl<'a> Selector<'a> {
102103
if self.json_path.is_predicate() {
103104
return true;
104105
}
105-
let poses = self.find_positions(root);
106+
let poses = self.find_positions(root, None, &self.json_path.paths);
106107
!poses.is_empty()
107108
}
108109

109110
pub fn predicate_match(&'a self, root: &'a [u8]) -> Result<bool, Error> {
110111
if !self.json_path.is_predicate() {
111112
return Err(Error::InvalidJsonPathPredicate);
112113
}
113-
let poses = self.find_positions(root);
114+
let poses = self.find_positions(root, None, &self.json_path.paths);
114115
Ok(!poses.is_empty())
115116
}
116117

117-
fn find_positions(&'a self, root: &'a [u8]) -> VecDeque<Position> {
118+
fn find_positions(
119+
&'a self,
120+
root: &'a [u8],
121+
current: Option<&Position>,
122+
paths: &[Path<'a>],
123+
) -> VecDeque<Position> {
118124
let mut poses = VecDeque::new();
119-
poses.push_back(Position::Container((0, root.len())));
120125

121-
for path in self.json_path.paths.iter() {
126+
let start_pos = if let Some(Path::Current) = paths.first() {
127+
current.expect("missing current position").clone()
128+
} else {
129+
Position::Container((0, root.len()))
130+
};
131+
poses.push_back(start_pos);
132+
133+
for path in paths.iter() {
122134
match path {
123-
&Path::Root => {
135+
&Path::Root | &Path::Current => {
124136
continue;
125137
}
126-
&Path::Current => unreachable!(),
127138
Path::FilterExpr(expr) | Path::Predicate(expr) => {
128139
let len = poses.len();
129140
for _ in 0..len {
@@ -453,10 +464,18 @@ impl<'a> Selector<'a> {
453464
self.compare(op, &lhs, &rhs)
454465
}
455466
},
467+
Expr::FilterFunc(filter_expr) => match filter_expr {
468+
FilterFunc::Exists(paths) => self.eval_exists(root, pos, paths),
469+
},
456470
_ => todo!(),
457471
}
458472
}
459473

474+
fn eval_exists(&'a self, root: &'a [u8], pos: &Position, paths: &[Path<'a>]) -> bool {
475+
let poses = self.find_positions(root, Some(pos), paths);
476+
!poses.is_empty()
477+
}
478+
460479
fn convert_expr_val(&'a self, root: &'a [u8], pos: &Position, expr: Expr<'a>) -> ExprValue<'a> {
461480
match expr {
462481
Expr::Value(value) => ExprValue::Value(value.clone()),

tests/it/functions.rs

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ use jsonb::{
2020
array_length, array_values, as_bool, as_null, as_number, as_str, build_array, build_object,
2121
compare, concat, contains, convert_to_comparable, delete_by_index, delete_by_keypath,
2222
delete_by_name, exists_all_keys, exists_any_keys, from_slice, get_by_index, get_by_keypath,
23-
get_by_name, get_by_path, is_array, is_object, keypath::parse_key_paths, object_each,
24-
object_keys, parse_value, path_exists, path_match, strip_nulls, to_bool, to_f64, to_i64,
25-
to_pretty_string, to_serde_json, to_serde_json_object, to_str, to_string, to_u64,
23+
get_by_name, get_by_path, get_by_path_array, is_array, is_object, keypath::parse_key_paths,
24+
object_each, object_keys, parse_value, path_exists, path_match, strip_nulls, to_bool, to_f64,
25+
to_i64, to_pretty_string, to_serde_json, to_serde_json_object, to_str, to_string, to_u64,
2626
traverse_check_string, type_of, Error, Number, Object, Value,
2727
};
2828

@@ -175,6 +175,74 @@ fn test_path_exists() {
175175
}
176176
}
177177

178+
#[test]
179+
fn test_path_exists_expr() {
180+
let source = r#"{"items": [
181+
{"id": 0, "name": "Andrew", "car": "Volvo"},
182+
{"id": 1, "name": "Fred", "car": "BMW"},
183+
{"id": 2, "name": "James"},
184+
{"id": 3, "name": "Ken"}
185+
]}"#;
186+
let paths = vec![
187+
(
188+
"$.items[*]?(exists($.items))",
189+
r#"[
190+
{"id": 0, "name": "Andrew", "car": "Volvo"},
191+
{"id": 1, "name": "Fred", "car": "BMW"},
192+
{"id": 2, "name": "James"},
193+
{"id": 3, "name": "Ken"}
194+
]"#,
195+
),
196+
(
197+
"$.items[*]?(exists(@.car))",
198+
r#"[
199+
{"id": 0, "name": "Andrew", "car": "Volvo"},
200+
{"id": 1, "name": "Fred", "car": "BMW"}
201+
]"#,
202+
),
203+
(
204+
r#"$.items[*]?(exists(@.car?(@ == "Volvo")))"#,
205+
r#"[
206+
{"id": 0, "name": "Andrew", "car": "Volvo"}
207+
]"#,
208+
),
209+
(
210+
r#"$.items[*]?(exists(@.car) && @.id >= 1)"#,
211+
r#"[
212+
{"id": 1, "name": "Fred", "car": "BMW"}
213+
]"#,
214+
),
215+
(
216+
r#"$ ? (exists(@.items[*]?(exists(@.car))))"#,
217+
r#"[{"items": [
218+
{"id": 0, "name": "Andrew", "car": "Volvo"},
219+
{"id": 1, "name": "Fred", "car": "BMW"},
220+
{"id": 2, "name": "James"},
221+
{"id": 3, "name": "Ken"}
222+
]}]"#,
223+
),
224+
(
225+
r#"$ ? (exists(@.items[*]?(exists(@.car) && @.id == 5)))"#,
226+
r#"[]"#,
227+
),
228+
];
229+
230+
let mut buf: Vec<u8> = Vec::new();
231+
let value = parse_value(source.as_bytes()).unwrap();
232+
value.write_to_vec(&mut buf);
233+
234+
for (path, expected) in paths {
235+
let mut out_buf: Vec<u8> = Vec::new();
236+
let mut out_offsets: Vec<u64> = Vec::new();
237+
let json_path = parse_json_path(path.as_bytes()).unwrap();
238+
239+
get_by_path_array(&buf, json_path, &mut out_buf, &mut out_offsets);
240+
let expected_buf = parse_value(expected.as_bytes()).unwrap().to_vec();
241+
242+
assert_eq!(out_buf, expected_buf);
243+
}
244+
}
245+
178246
#[test]
179247
fn test_get_by_path() {
180248
let source = r#"{"name":"Fred","phones":[{"type":"home","number":3720453},{"type":"work","number":5062051}],"car_no":123,"测试\"\uD83D\uDC8E":"ab"}"#;

tests/it/jsonpath_parser.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ fn test_json_path() {
5050
r#"$[*] > 1"#,
5151
r#"$.a > $.b"#,
5252
r#"$.price > 10 || $.category == "reference""#,
53+
// exists expression
54+
r#"$.store.book?(exists(@.price?(@ > 20)))"#,
55+
r#"$.store?(exists(@.book?(exists(@.category?(@ == "fiction")))))"#,
5356
];
5457

5558
for case in cases {

tests/it/testdata/json_path.txt

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,3 +828,104 @@ JsonPath {
828828
}
829829

830830

831+
---------- Input ----------
832+
$.store.book?(exists(@.price?(@ > 20)))
833+
---------- Output ---------
834+
$.store.book?(exists(@.price?(@ > 20)))
835+
---------- AST ------------
836+
JsonPath {
837+
paths: [
838+
Root,
839+
DotField(
840+
"store",
841+
),
842+
DotField(
843+
"book",
844+
),
845+
FilterExpr(
846+
FilterFunc(
847+
Exists(
848+
[
849+
Current,
850+
DotField(
851+
"price",
852+
),
853+
FilterExpr(
854+
BinaryOp {
855+
op: Gt,
856+
left: Paths(
857+
[
858+
Current,
859+
],
860+
),
861+
right: Value(
862+
Number(
863+
UInt64(
864+
20,
865+
),
866+
),
867+
),
868+
},
869+
),
870+
],
871+
),
872+
),
873+
),
874+
],
875+
}
876+
877+
878+
---------- Input ----------
879+
$.store?(exists(@.book?(exists(@.category?(@ == "fiction")))))
880+
---------- Output ---------
881+
$.store?(exists(@.book?(exists(@.category?(@ == "fiction")))))
882+
---------- AST ------------
883+
JsonPath {
884+
paths: [
885+
Root,
886+
DotField(
887+
"store",
888+
),
889+
FilterExpr(
890+
FilterFunc(
891+
Exists(
892+
[
893+
Current,
894+
DotField(
895+
"book",
896+
),
897+
FilterExpr(
898+
FilterFunc(
899+
Exists(
900+
[
901+
Current,
902+
DotField(
903+
"category",
904+
),
905+
FilterExpr(
906+
BinaryOp {
907+
op: Eq,
908+
left: Paths(
909+
[
910+
Current,
911+
],
912+
),
913+
right: Value(
914+
String(
915+
"fiction",
916+
),
917+
),
918+
},
919+
),
920+
],
921+
),
922+
),
923+
),
924+
],
925+
),
926+
),
927+
),
928+
],
929+
}
930+
931+

0 commit comments

Comments
 (0)