Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jsonpath): add exists filter expression #48

Merged
merged 4 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/jsonpath/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,44 @@ fn expr_atom(input: &[u8], root_predicate: bool) -> IResult<&[u8], Expr<'_>> {
),
|expr| expr,
),
map(filter_func, Expr::FilterFunc),
))(input)
}

fn filter_func(input: &[u8]) -> IResult<&[u8], FilterFunc<'_>> {
alt((exists,))(input)
}

fn exists(input: &[u8]) -> IResult<&[u8], FilterFunc<'_>> {
preceded(
tag("exists"),
preceded(
multispace0,
delimited(
terminated(char('('), multispace0),
map(exists_paths, FilterFunc::Exists),
preceded(multispace0, char(')')),
),
),
)(input)
}

fn exists_paths(input: &[u8]) -> IResult<&[u8], Vec<Path<'_>>> {
map(
pair(
alt((
value(Path::Root, char('$')),
value(Path::Current, char('@')),
)),
many0(path),
),
|(pre, mut paths)| {
paths.insert(0, pre);
paths
},
)(input)
}

fn expr_and(input: &[u8], root_predicate: bool) -> IResult<&[u8], Expr<'_>> {
map(
separated_list1(delimited(multispace0, tag("&&"), multispace0), |i| {
Expand Down
17 changes: 17 additions & 0 deletions src/jsonpath/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ pub enum Expr<'a> {
left: Box<Expr<'a>>,
right: Box<Expr<'a>>,
},
/// Filter function, returns a boolean value.
FilterFunc(FilterFunc<'a>),
}

/// Represents filter function, returns a boolean value.
#[derive(Debug, Clone, PartialEq)]
pub enum FilterFunc<'a> {
Exists(Vec<Path<'a>>),
}

impl<'a> Display for JsonPath<'a> {
Expand Down Expand Up @@ -312,6 +320,15 @@ impl<'a> Display for Expr<'a> {
write!(f, "{right}")?;
}
}
Expr::FilterFunc(func) => match func {
FilterFunc::Exists(paths) => {
f.write_str("exists(")?;
for path in paths {
write!(f, "{path}")?;
}
f.write_str(")")?;
}
},
}
Ok(())
}
Expand Down
35 changes: 27 additions & 8 deletions src/jsonpath/selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use crate::constants::*;
use crate::jsonpath::ArrayIndex;
use crate::jsonpath::BinaryOperator;
use crate::jsonpath::Expr;
use crate::jsonpath::FilterFunc;
use crate::jsonpath::Index;
use crate::jsonpath::JsonPath;
use crate::jsonpath::Path;
Expand Down Expand Up @@ -74,7 +75,7 @@ impl<'a> Selector<'a> {
}

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

if self.json_path.is_predicate() {
Self::build_predicate_result(&mut poses, data);
Expand Down Expand Up @@ -102,28 +103,38 @@ impl<'a> Selector<'a> {
if self.json_path.is_predicate() {
return true;
}
let poses = self.find_positions(root);
let poses = self.find_positions(root, None, &self.json_path.paths);
!poses.is_empty()
}

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

fn find_positions(&'a self, root: &'a [u8]) -> VecDeque<Position> {
fn find_positions(
&'a self,
root: &'a [u8],
current: Option<&Position>,
paths: &[Path<'a>],
) -> VecDeque<Position> {
let mut poses = VecDeque::new();
poses.push_back(Position::Container((0, root.len())));

for path in self.json_path.paths.iter() {
let start_pos = if let Some(Path::Current) = paths.first() {
current.expect("missing current position").clone()
} else {
Position::Container((0, root.len()))
};
poses.push_back(start_pos);

for path in paths.iter() {
match path {
&Path::Root => {
&Path::Root | &Path::Current => {
continue;
}
&Path::Current => unreachable!(),
Path::FilterExpr(expr) | Path::Predicate(expr) => {
let len = poses.len();
for _ in 0..len {
Expand Down Expand Up @@ -453,10 +464,18 @@ impl<'a> Selector<'a> {
self.compare(op, &lhs, &rhs)
}
},
Expr::FilterFunc(filter_expr) => match filter_expr {
FilterFunc::Exists(paths) => self.eval_exists(root, pos, paths),
},
_ => todo!(),
}
}

fn eval_exists(&'a self, root: &'a [u8], pos: &Position, paths: &[Path<'a>]) -> bool {
let poses = self.find_positions(root, Some(pos), paths);
!poses.is_empty()
}

fn convert_expr_val(&'a self, root: &'a [u8], pos: &Position, expr: Expr<'a>) -> ExprValue<'a> {
match expr {
Expr::Value(value) => ExprValue::Value(value.clone()),
Expand Down
74 changes: 71 additions & 3 deletions tests/it/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ use jsonb::{
array_length, array_values, as_bool, as_null, as_number, as_str, build_array, build_object,
compare, concat, contains, convert_to_comparable, delete_by_index, delete_by_keypath,
delete_by_name, exists_all_keys, exists_any_keys, from_slice, get_by_index, get_by_keypath,
get_by_name, get_by_path, is_array, is_object, keypath::parse_key_paths, object_each,
object_keys, parse_value, path_exists, path_match, strip_nulls, to_bool, to_f64, to_i64,
to_pretty_string, to_serde_json, to_serde_json_object, to_str, to_string, to_u64,
get_by_name, get_by_path, get_by_path_array, is_array, is_object, keypath::parse_key_paths,
object_each, object_keys, parse_value, path_exists, path_match, strip_nulls, to_bool, to_f64,
to_i64, to_pretty_string, to_serde_json, to_serde_json_object, to_str, to_string, to_u64,
traverse_check_string, type_of, Error, Number, Object, Value,
};

Expand Down Expand Up @@ -175,6 +175,74 @@ fn test_path_exists() {
}
}

#[test]
fn test_path_exists_expr() {
let source = r#"{"items": [
{"id": 0, "name": "Andrew", "car": "Volvo"},
{"id": 1, "name": "Fred", "car": "BMW"},
{"id": 2, "name": "James"},
{"id": 3, "name": "Ken"}
]}"#;
let paths = vec![
(
"$.items[*]?(exists($.items))",
r#"[
{"id": 0, "name": "Andrew", "car": "Volvo"},
{"id": 1, "name": "Fred", "car": "BMW"},
{"id": 2, "name": "James"},
{"id": 3, "name": "Ken"}
]"#,
),
(
"$.items[*]?(exists(@.car))",
r#"[
{"id": 0, "name": "Andrew", "car": "Volvo"},
{"id": 1, "name": "Fred", "car": "BMW"}
]"#,
),
(
r#"$.items[*]?(exists(@.car?(@ == "Volvo")))"#,
r#"[
{"id": 0, "name": "Andrew", "car": "Volvo"}
]"#,
),
(
r#"$.items[*]?(exists(@.car) && @.id >= 1)"#,
r#"[
{"id": 1, "name": "Fred", "car": "BMW"}
]"#,
),
(
r#"$ ? (exists(@.items[*]?(exists(@.car))))"#,
r#"[{"items": [
{"id": 0, "name": "Andrew", "car": "Volvo"},
{"id": 1, "name": "Fred", "car": "BMW"},
{"id": 2, "name": "James"},
{"id": 3, "name": "Ken"}
]}]"#,
),
(
r#"$ ? (exists(@.items[*]?(exists(@.car) && @.id == 5)))"#,
r#"[]"#,
),
];

let mut buf: Vec<u8> = Vec::new();
let value = parse_value(source.as_bytes()).unwrap();
value.write_to_vec(&mut buf);

for (path, expected) in paths {
let mut out_buf: Vec<u8> = Vec::new();
let mut out_offsets: Vec<u64> = Vec::new();
let json_path = parse_json_path(path.as_bytes()).unwrap();

get_by_path_array(&buf, json_path, &mut out_buf, &mut out_offsets);
let expected_buf = parse_value(expected.as_bytes()).unwrap().to_vec();

assert_eq!(out_buf, expected_buf);
}
}

#[test]
fn test_get_by_path() {
let source = r#"{"name":"Fred","phones":[{"type":"home","number":3720453},{"type":"work","number":5062051}],"car_no":123,"测试\"\uD83D\uDC8E":"ab"}"#;
Expand Down
3 changes: 3 additions & 0 deletions tests/it/jsonpath_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ fn test_json_path() {
r#"$[*] > 1"#,
r#"$.a > $.b"#,
r#"$.price > 10 || $.category == "reference""#,
// exists expression
r#"$.store.book?(exists(@.price?(@ > 20)))"#,
r#"$.store?(exists(@.book?(exists(@.category?(@ == "fiction")))))"#,
];

for case in cases {
Expand Down
101 changes: 101 additions & 0 deletions tests/it/testdata/json_path.txt
Original file line number Diff line number Diff line change
Expand Up @@ -828,3 +828,104 @@ JsonPath {
}


---------- Input ----------
$.store.book?(exists(@.price?(@ > 20)))
---------- Output ---------
$.store.book?(exists(@.price?(@ > 20)))
---------- AST ------------
JsonPath {
paths: [
Root,
DotField(
"store",
),
DotField(
"book",
),
FilterExpr(
FilterFunc(
Exists(
[
Current,
DotField(
"price",
),
FilterExpr(
BinaryOp {
op: Gt,
left: Paths(
[
Current,
],
),
right: Value(
Number(
UInt64(
20,
),
),
),
},
),
],
),
),
),
],
}


---------- Input ----------
$.store?(exists(@.book?(exists(@.category?(@ == "fiction")))))
---------- Output ---------
$.store?(exists(@.book?(exists(@.category?(@ == "fiction")))))
---------- AST ------------
JsonPath {
paths: [
Root,
DotField(
"store",
),
FilterExpr(
FilterFunc(
Exists(
[
Current,
DotField(
"book",
),
FilterExpr(
FilterFunc(
Exists(
[
Current,
DotField(
"category",
),
FilterExpr(
BinaryOp {
op: Eq,
left: Paths(
[
Current,
],
),
right: Value(
String(
"fiction",
),
),
},
),
],
),
),
),
],
),
),
),
],
}


Loading