Skip to content

Commit 4784a7e

Browse files
authored
feat!: parsing filter expressions
This is mainly an `rsonpath-syntax` change – the selectors are parsed, but `rq` will give you an unsupported error and a link to #154 if you put them in a query.
1 parent e40f464 commit 4784a7e

26 files changed

+3088
-391
lines changed

.vscode/settings.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"proptests",
6161
"regs",
6262
"repr",
63+
"reprs",
6364
"rfind",
6465
"roadmap",
6566
"rposition",
@@ -100,5 +101,8 @@
100101
"./crates/rsonpath-benchmarks/Cargo.toml",
101102
"./crates/rsonpath-test/Cargo.toml",
102103
"./crates/rsonpath-test-codegen/Cargo.toml"
103-
]
104+
],
105+
"rust-analyzer.cargo.extraEnv": {
106+
"RUSTFLAGS": "-Clinker=clang -Clink-arg=-fuse-ld=lld"
107+
}
104108
}

Justfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ build-all profile="dev": (build-lib profile) (build-bin profile) (gen-tests)
5252

5353
# Build and open the library documentation.
5454
doc $RUSTDOCFLAGS="--cfg docsrs":
55-
cargo +nightly doc --open --package rsonpath-lib --all-features -Z rustdoc-scrape-examples
55+
cargo +nightly doc --open --all-features -Z rustdoc-scrape-examples
5656

5757
# Run the codegen for rsonpath-test, generating the E2E tests and JSONs.
5858
gen-tests:

crates/rsonpath-lib/src/automaton/nfa.rs

+2
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,15 @@ impl<'q> NondeterministicAutomaton<'q> {
7878
Selector::Index(Index::FromStart(index)) => Ok(Direct(Transition::Labelled((*index).into()))),
7979
Selector::Index(Index::FromEnd(_)) => Err(UnsupportedFeatureError::indexing_from_end().into()),
8080
Selector::Slice(_) => Err(UnsupportedFeatureError::slice_selector().into()),
81+
Selector::Filter(_) => Err(UnsupportedFeatureError::filter_selector().into()),
8182
},
8283
Segment::Descendant(selectors) if selectors.len() == 1 => match selectors.first() {
8384
Selector::Name(name) => Ok(Recursive(Transition::Labelled(name.into()))),
8485
Selector::Wildcard => Ok(Recursive(Transition::Wildcard)),
8586
Selector::Index(Index::FromStart(index)) => Ok(Recursive(Transition::Labelled((*index).into()))),
8687
Selector::Index(Index::FromEnd(_)) => Err(UnsupportedFeatureError::indexing_from_end().into()),
8788
Selector::Slice(_) => Err(UnsupportedFeatureError::slice_selector().into()),
89+
Selector::Filter(_) => Err(UnsupportedFeatureError::filter_selector().into()),
8890
},
8991
_ => Err(UnsupportedFeatureError::multiple_selectors().into()),
9092
})

crates/rsonpath-lib/src/error.rs

+8
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@ impl UnsupportedFeatureError {
154154
Self::tracked(152, "Slice Selector")
155155
}
156156

157+
/// Filter Selector &ndash; supporting filter selectors.
158+
/// <https://github.com/V0ldek/rsonpath/issues/154>
159+
#[must_use]
160+
#[inline(always)]
161+
pub fn filter_selector() -> Self {
162+
Self::tracked(154, "Filter Selector")
163+
}
164+
157165
/// Returns the issue number on GitHub corresponding to the unsupported feature.
158166
/// Is [`None`] if the feature is not planned.
159167
#[must_use]

crates/rsonpath-syntax/Cargo.toml

-4
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,3 @@ test-case = "3.3.1"
3333
default = []
3434
arbitrary = ["dep:arbitrary"]
3535
color = ["dep:owo-colors"]
36-
37-
[[bin]]
38-
name = "rsonpath-parse"
39-
path = "src/cli.rs"

crates/rsonpath-syntax/README.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,18 @@ There are two optional features:
2727
- `arbitrary`, which enables a dependency on the [`arbitrary` crate](https://docs.rs/arbitrary/latest/arbitrary/) to provide `Arbitrary` implementations on query types; this is used e.g. for fuzzing.
2828
- `color`, which enables a dependency on the [`owo_colors` crate](https://docs.rs/owo-colors/latest/owo_colors/) to provide colorful `Display` representations of `ParseError` with the `colored` function.
2929

30-
## Binary
30+
## Examples
3131

32-
A small CLI tool, `rsonpath-parse` is attached. It takes one argument, a query to parse, and prints a debug representation of the result query, or an error message. This is useful for debugging.
32+
There are two examples programs, [`builder`](./examples/builder.rs) showcases usage of the `JsonPathQueryBuilder`
33+
struct; [`cli`](./examples/cli.rs) is a small CLI tool that takes one argument, a query to parse, and prints a debug representation of the result query, or an error message &ndash; this is useful for debugging when developing the crate itself.
3334

3435
## State of the crate
3536

3637
This is an in-development version that supports only name, index, and wildcard selectors.
3738
However, these are fully supported, tested, and fuzzed. The planned roadmap is:
3839

3940
- [x] support slices
40-
- [ ] support filters (without functions)
41+
- [x] support filters (without functions)
4142
- [ ] support functions (including type check)
4243
- [ ] polish the API
4344
- [ ] 1.0.0 stable release

crates/rsonpath-syntax/examples/builder.rs

+196-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use rsonpath_syntax::builder::JsonPathQueryBuilder;
1+
use rsonpath_syntax::{
2+
builder::JsonPathQueryBuilder,
3+
num::{JsonFloat, JsonInt},
4+
str::JsonString,
5+
};
26
use std::{error::Error, process::ExitCode};
37

48
fn main() -> Result<ExitCode, Box<dyn Error>> {
@@ -69,20 +73,208 @@ fn main() -> Result<ExitCode, Box<dyn Error>> {
6973
println!("{query}");
7074

7175
// $..book[:2]
76+
let query = JsonPathQueryBuilder::new()
77+
.descendant_name("book")
78+
.child_slice(|s| s.with_end(2))
79+
.to_query();
80+
println!("{query}");
7281

7382
// $..book[[email protected]]
83+
let query = JsonPathQueryBuilder::new()
84+
.descendant_name("book")
85+
.child(|x| x.filter(|x| x.test_relative(|x| x.child_name("isbn"))))
86+
.to_query();
87+
println!("{query}");
7488

7589
// $..book[[email protected]<10]
90+
let query = JsonPathQueryBuilder::new()
91+
.descendant_name("book")
92+
.child(|x| {
93+
x.filter(|x| {
94+
x.comparison(|x| {
95+
x.query_relative(|x| x.name("price"))
96+
.less_than()
97+
.literal(JsonInt::from(10))
98+
})
99+
})
100+
})
101+
.to_query();
102+
println!("{query}");
76103

77104
// $..*
78105
let query = JsonPathQueryBuilder::new().descendant_wildcard().to_query();
79106
println!("{query}");
107+
println!();
108+
109+
// === Examples from the filter selector section: https://www.ietf.org/archive/id/draft-ietf-jsonpath-base-21.html#name-examples-6
110+
111+
// $.a[[email protected] == 'kilo']
112+
let query = JsonPathQueryBuilder::new()
113+
.child_name("a")
114+
.child_filter(|x| {
115+
x.comparison(|x| {
116+
x.query_relative(|q| q.name("b"))
117+
.equal_to()
118+
.literal(JsonString::from("kilo"))
119+
})
120+
})
121+
.to_query();
122+
println!("{query}");
123+
124+
// $.a[?@>3.5]
125+
let query = JsonPathQueryBuilder::new()
126+
.child_name("a")
127+
.child_filter(|x| {
128+
x.comparison(|x| {
129+
x.query_relative(|q| q)
130+
.greater_than()
131+
.literal(JsonFloat::try_from(3.5).unwrap())
132+
})
133+
})
134+
.to_query();
135+
println!("{query}");
136+
137+
138+
let query = JsonPathQueryBuilder::new()
139+
.child_name("a")
140+
.child_filter(|x| x.test_relative(|x| x.child_name("b")))
141+
.to_query();
142+
println!("{query}");
143+
144+
// $[?@.*]
145+
let query = JsonPathQueryBuilder::new()
146+
.child_filter(|x| x.test_relative(|x| x.child_wildcard()))
147+
.to_query();
148+
println!("{query}");
149+
150+
151+
let query = JsonPathQueryBuilder::new()
152+
.child_filter(|x| x.test_relative(|x| x.child_filter(|x| x.test_relative(|x| x.child_name("b")))))
153+
.to_query();
154+
println!("{query}");
155+
156+
// $.o[?@<3, ?@<3]
157+
let query = JsonPathQueryBuilder::new()
158+
.child_name("o")
159+
.child(|x| {
160+
x.filter(|x| x.comparison(|x| x.query_relative(|x| x).less_than().literal(JsonInt::from(3))))
161+
.filter(|x| x.comparison(|x| x.query_relative(|x| x).less_than().literal(JsonInt::from(3))))
162+
})
163+
.to_query();
164+
println!("{query}");
165+
166+
// $.a[?@<2 || @.b == "k"]
167+
let query = JsonPathQueryBuilder::new()
168+
.child_name("a")
169+
.child_filter(|x| {
170+
x.comparison(|x| x.query_relative(|x| x).less_than().literal(JsonInt::from(2)))
171+
.or(|x| {
172+
x.comparison(|x| {
173+
x.query_relative(|x| x.name("b"))
174+
.equal_to()
175+
.literal(JsonString::from("k"))
176+
})
177+
})
178+
})
179+
.to_query();
180+
println!("{query}");
181+
182+
// $.o[?@>1 && @<4]
183+
let query = JsonPathQueryBuilder::new()
184+
.child_name("o")
185+
.child_filter(|x| {
186+
x.comparison(|x| x.query_relative(|x| x).greater_than().literal(JsonInt::from(1)))
187+
.and(|x| x.comparison(|x| x.query_relative(|x| x).less_than().literal(JsonInt::from(4))))
188+
})
189+
.to_query();
190+
println!("{query}");
191+
192+
// $.o[[email protected] || @.x]
193+
let query = JsonPathQueryBuilder::new()
194+
.child_name("o")
195+
.child_filter(|x| {
196+
x.test_relative(|x| x.child_name("u"))
197+
.or(|x| x.test_relative(|x| x.child_name("x")))
198+
})
199+
.to_query();
200+
println!("{query}");
201+
202+
// $.a[[email protected] == $.x]
203+
let query = JsonPathQueryBuilder::new()
204+
.child_name("a")
205+
.child_filter(|x| {
206+
x.comparison(|x| {
207+
x.query_relative(|x| x.name("b"))
208+
.equal_to()
209+
.query_absolute(|x| x.name("x"))
210+
})
211+
})
212+
.to_query();
213+
println!("{query}");
214+
215+
// $.a[?@ == @]
216+
let query = JsonPathQueryBuilder::new()
217+
.child_name("a")
218+
.child_filter(|x| x.comparison(|x| x.query_relative(|x| x).equal_to().query_absolute(|x| x)))
219+
.to_query();
220+
println!("{query}");
221+
222+
println!();
80223

81224
// === Build a query showcasing all syntactic elements. ===
82225
// $.['store\t1']
83-
// ..[3, -5, ::3, ::-5, :7:2, 3::2, 3:7:, 3:7:2, -3:-7:-2]
84-
// .*
85-
// .
226+
// ..[3, -5, ::3, ::-5, :7:2, 3::2, 3:7, 3:7:2, -3:-7:-2]
227+
// [*]
228+
// [? @['name'] == "Best" && @['book'][[email protected] > 30.3] ||
229+
// $['global'] != false && (@['year'] < 2023 || $[0].year >= 2000) ||
230+
// !($..['invalid'])]
231+
let query = JsonPathQueryBuilder::new()
232+
.child_name("store\t1")
233+
.descendant(|x| {
234+
x.index(3)
235+
.index(-5)
236+
.slice(|x| x.with_step(3))
237+
.slice(|x| x.with_step(-5))
238+
.slice(|x| x.with_end(7).with_step(2))
239+
.slice(|x| x.with_start(3).with_step(2))
240+
.slice(|x| x.with_start(3).with_end(7))
241+
.slice(|x| x.with_start(3).with_end(7).with_step(2))
242+
.slice(|x| x.with_start(-3).with_end(-7).with_step(-2))
243+
})
244+
.child_wildcard()
245+
.child_filter(|f| {
246+
f.comparison(|c| c.query_relative(|q| q.name("name")).equal_to().literal("Best"))
247+
.and(|f| {
248+
f.test_relative(|t| {
249+
t.child_name("book").child_filter(|f2| {
250+
f2.comparison(|c| {
251+
c.query_relative(|q| q.name("price"))
252+
.greater_than()
253+
.literal(JsonFloat::try_from(30.3).unwrap())
254+
})
255+
})
256+
})
257+
})
258+
.or(|f| {
259+
f.comparison(|c| c.query_absolute(|q| q.name("global")).not_equal_to().literal(false))
260+
.and(|f| {
261+
f.comparison(|c| {
262+
c.query_relative(|q| q.name("year"))
263+
.less_than()
264+
.literal(JsonInt::from(2023))
265+
})
266+
.or(|f| {
267+
f.comparison(|c| {
268+
c.query_absolute(|q| q.index(0).name("year"))
269+
.greater_or_equal_to()
270+
.literal(JsonInt::from(2000))
271+
})
272+
})
273+
})
274+
})
275+
.or(|f| f.not(|f| f.test_absolute(|q| q.descendant_name("invalid"))))
276+
})
277+
.to_query();
86278

87279
println!("{query}");
88280

File renamed without changes.

0 commit comments

Comments
 (0)