diff --git a/Cargo.toml b/Cargo.toml
index 5ae10c3..856f7e0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,3 +11,4 @@ pest = "2.1.3"
pest_derive = "2.1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.57"
+slyce = "0.3.0"
diff --git a/DEVELOPING.md b/DEVELOPING.md
index 6d00eb4..3e5f214 100644
--- a/DEVELOPING.md
+++ b/DEVELOPING.md
@@ -47,9 +47,25 @@ test to `true`, for example:
When one or more tests are focussed in this way, the test suite will fail with the message
-"testcase(s) still focussed" even if all the tests pass.
+"testcase(s) still focussed" if and only if all the focussed tests pass.
This prevents pull requests being merged in which tests are accidentally left focussed.
+To skip one or more tests, edit [cts.json](tests/cts.json) and set the `skip` property of the relevant
+test to `true`, for example:
+
+ }, {
+ "name": "wildcarded child",
+ "skip": true,
+ "selector": "$.*",
+ "document": {"a" : "A", "b" : "B"},
+ "result": ["A", "B"]
+ }, {
+
+
+When one or more tests are skipped in this way, the test suite will fail with the message
+"testcase(s) still skipped" if and only if all the tests pass and none are focussed.
+This prevents pull requests being merged in which tests are accidentally left skipped.
+
To see details of which tests run, use:
```
cargo test -- --show-output
diff --git a/src/ast.rs b/src/ast.rs
index 4e9d9a7..c2fd948 100644
--- a/src/ast.rs
+++ b/src/ast.rs
@@ -5,6 +5,8 @@
*/
use serde_json::Value;
+use slyce::Slice;
+use std::iter;
/// A path is a tree of selector nodes.
///
@@ -57,6 +59,7 @@ pub enum Selector {
#[derive(Debug)]
pub enum UnionElement {
Name(String),
+ Slice(Slice),
Index(i64),
}
@@ -89,6 +92,13 @@ impl UnionElement {
pub fn get<'a>(&self, v: &'a Value) -> Iter<'a> {
match self {
UnionElement::Name(name) => Box::new(v.get(name).into_iter()),
+ UnionElement::Slice(slice) => {
+ if let Value::Array(arr) = v {
+ Box::new(slice.apply(arr))
+ } else {
+ Box::new(iter::empty())
+ }
+ }
UnionElement::Index(num) => Box::new(v.get(abs_index(*num, v)).into_iter()),
}
}
diff --git a/src/grammar.pest b/src/grammar.pest
index 76f46f1..51f7638 100644
--- a/src/grammar.pest
+++ b/src/grammar.pest
@@ -3,7 +3,7 @@ selector = _{ SOI ~ rootSelector ~ matchers ~ EOI }
matchers = ${ matcher* }
rootSelector = { "$" }
-matcher = { dotChild | union }
+matcher = !{ dotChild | union }
dotChild = _{ wildcardedDotChild | namedDotChild }
wildcardedDotChild = { ".*" }
@@ -18,9 +18,14 @@ char = {
}
union = { "[" ~ unionElement ~ ("," ~ unionElement)* ~ "]" }
-unionElement = _{ unionChild | unionArrayIndex } // TODO: add unionArraySlice
-unionChild = { doubleQuotedString | singleQuotedString }
-unionArrayIndex = { "-" ? ~ ( "0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* ) }
+unionElement = _{ unionChild | unionArraySlice | unionArrayIndex }
+unionChild = ${ doubleQuotedString | singleQuotedString }
+unionArrayIndex = @{ integer }
+integer = _{ "-" ? ~ ( "0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* ) }
+unionArraySlice = { sliceStart ? ~ ":" ~ sliceEnd ? ~ ( ":" ~ sliceStep ? ) ? }
+sliceStart = @{ integer }
+sliceEnd = @{ integer }
+sliceStep = @{ integer }
doubleQuotedString = _{ "\"" ~ doubleInner ~ "\"" }
doubleInner = @{ doubleChar* }
diff --git a/src/jsonpath.rs b/src/jsonpath.rs
index 2182a44..9d2069b 100644
--- a/src/jsonpath.rs
+++ b/src/jsonpath.rs
@@ -28,6 +28,7 @@ pub fn parse(selector: &str) -> Result {
Ok(Path(p))
}
+#[derive(Debug)]
pub struct Path(ast::Path);
impl Path {
diff --git a/src/parser.rs b/src/parser.rs
index 2ebdfbe..57d3302 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -6,6 +6,8 @@
pub use crate::ast::*;
use crate::pest::Parser;
+use slyce::Slice;
+use std::num::ParseIntError;
#[derive(Parser)]
#[grammar = "grammar.pest"]
@@ -17,23 +19,26 @@ pub fn parse(selector: &str) -> Result {
.nth(1)
.unwrap();
- Ok(selector_rule
+ selector_rule
.into_inner()
- .fold(Path::Root, |prev, r| match r.as_rule() {
- Rule::matcher => Path::Sel(Box::new(prev), parse_selector(r)),
+ .fold(Ok(Path::Root), |prev, r| match r.as_rule() {
+ Rule::matcher => Ok(Path::Sel(
+ Box::new(prev?),
+ parse_selector(r).map_err(|e| format!("{}", e))?,
+ )),
_ => panic!("invalid parse tree {:?}", r),
- }))
+ })
}
-fn parse_selector(matcher_rule: pest::iterators::Pair) -> Selector {
+fn parse_selector(matcher_rule: pest::iterators::Pair) -> Result {
let r = matcher_rule.into_inner().next().unwrap();
- match r.as_rule() {
+ Ok(match r.as_rule() {
Rule::wildcardedDotChild => Selector::DotWildcard,
Rule::namedDotChild => Selector::DotName(parse_child_name(r)),
- Rule::union => Selector::Union(parse_union_indices(r)),
+ Rule::union => Selector::Union(parse_union_indices(r)?),
_ => panic!("invalid parse tree {:?}", r),
- }
+ })
}
fn parse_child_name(matcher_rule: pest::iterators::Pair) -> String {
@@ -45,15 +50,18 @@ fn parse_child_name(matcher_rule: pest::iterators::Pair) -> String {
}
}
-fn parse_union_indices(matcher_rule: pest::iterators::Pair) -> Vec {
+fn parse_union_indices(
+ matcher_rule: pest::iterators::Pair,
+) -> Result, ParseIntError> {
matcher_rule
.into_inner()
.map(|r| match r.as_rule() {
- Rule::unionChild => parse_union_child(r),
+ Rule::unionChild => Ok(parse_union_child(r)),
+ Rule::unionArraySlice => parse_union_array_slice(r),
Rule::unionArrayIndex => parse_union_array_index(r),
_ => panic!("invalid parse tree {:?}", r),
})
- .collect()
+ .collect::, ParseIntError>>()
}
fn parse_union_child(matcher_rule: pest::iterators::Pair) -> UnionElement {
@@ -66,9 +74,42 @@ fn parse_union_child(matcher_rule: pest::iterators::Pair) -> UnionElement
})
}
-fn parse_union_array_index(matcher_rule: pest::iterators::Pair) -> UnionElement {
- let i = matcher_rule.as_str().parse().unwrap();
- UnionElement::Index(i)
+fn parse_union_array_index(
+ matcher_rule: pest::iterators::Pair,
+) -> Result {
+ let i = matcher_rule.as_str().parse()?;
+ Ok(UnionElement::Index(i))
+}
+
+fn parse_union_array_slice(
+ matcher_rule: pest::iterators::Pair,
+) -> Result {
+ let mut start: Option = None;
+ let mut end: Option = None;
+ let mut step: Option = None;
+ for r in matcher_rule.into_inner() {
+ match r.as_rule() {
+ Rule::sliceStart => {
+ start = Some(r.as_str().parse()?);
+ }
+
+ Rule::sliceEnd => {
+ end = Some(r.as_str().parse()?);
+ }
+
+ Rule::sliceStep => {
+ step = Some(r.as_str().parse()?);
+ }
+
+ _ => panic!("invalid parse tree {:?}", r),
+ }
+ }
+
+ Ok(UnionElement::Slice(Slice {
+ start: start.into(),
+ end: end.into(),
+ step,
+ }))
}
fn unescape(contents: &str) -> String {
diff --git a/tests/cts.json b/tests/cts.json
index e0aceb9..b2a796a 100644
--- a/tests/cts.json
+++ b/tests/cts.json
@@ -481,6 +481,20 @@
"name": "union child, single quotes, incomplete escape",
"selector": "$['\\']",
"invalid_selector": true
+ }, {
+ "name": "union",
+ "selector": "$[0,2]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [0, 2]
+ }, {
+ "name": "union with whitespace",
+ "selector": "$[ 0 , 1 ]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [0, 1]
+ }, {
+ "name": "empty union",
+ "selector": "$[]",
+ "invalid_selector": true
}, {
"name": "union array access",
"selector": "$[0]",
@@ -496,6 +510,10 @@
"selector": "$[2]",
"document": ["first", "second"],
"result": []
+ }, {
+ "name": "union array access, overflowing index",
+ "selector": "$[231584178474632390847141970017375815706539969331281128078915168015826259279872]",
+ "invalid_selector": true
}, {
"name": "union array access, negative",
"selector": "$[-1]",
@@ -523,6 +541,208 @@
}, {
"name": "union array access, leading -0",
"selector": "$[-01]",
+ "invalid_selector": true
+ }, {
+ "name": "union array slice",
+ "selector": "$[1:3]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [1, 2]
+ }, {
+ "name": "union array slice with step",
+ "selector": "$[1:6:2]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [1, 3, 5]
+ }, {
+ "name": "union array slice with everything omitted, short form",
+ "selector": "$[:]",
+ "document": [0, 1, 2, 3],
+ "result": [0, 1, 2, 3]
+ }, {
+ "name": "union array slice with everything omitted, long form",
+ "selector": "$[::]",
+ "document": [0, 1, 2, 3],
+ "result": [0, 1, 2, 3]
+ }, {
+ "name": "union array slice with start omitted",
+ "selector": "$[:2]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [0, 1]
+ }, {
+ "name": "union array slice with start and end omitted",
+ "selector": "$[::2]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [0, 2, 4, 6, 8]
+ }, {
+ "name": "union array slice, last index",
+ "selector": "$[-1]",
+ "document": [0, 1, 2, 3],
+ "result": [3]
+ }, {
+ "name": "union array slice, overflowed index",
+ "selector": "$[4]",
+ "document": [0, 1, 2, 3],
+ "result": []
+ }, {
+ "name": "union array slice, underflowed index",
+ "selector": "$[-5]",
+ "document": [0, 1, 2, 3],
+ "result": []
+ }, {
+ "name": "union array slice, negative step with default start and end",
+ "selector": "$[::-1]",
+ "document": [0, 1, 2, 3],
+ "result": [3, 2, 1, 0]
+ }, {
+ "name": "union array slice, negative step with default start",
+ "selector": "$[:0:-1]",
+ "document": [0, 1, 2, 3],
+ "result": [3, 2, 1]
+ }, {
+ "name": "union array slice, negative step with default end",
+ "selector": "$[2::-1]",
+ "document": [0, 1, 2, 3],
+ "result": [2, 1, 0]
+ }, {
+ "name": "union array slice, larger negative step",
+ "selector": "$[::-2]",
+ "document": [0, 1, 2, 3],
+ "result": [3, 1]
+ }, {
+ "name": "union array slice, negative range with default step",
+ "selector": "$[-1:-3]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": []
+ }, {
+ "name": "union array slice, negative range with negative step",
+ "selector": "$[-1:-3:-1]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [9, 8]
+ }, {
+ "name": "union array slice, negative range with larger negative step",
+ "selector": "$[-1:-6:-2]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [9, 7, 5]
+ }, {
+ "name": "union array slice, larger negative range with larger negative step",
+ "selector": "$[-1:-7:-2]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [9, 7, 5]
+ }, {
+ "name": "union array slice, negative from, positive to",
+ "selector": "$[-5:7]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [5, 6]
+ }, {
+ "name": "union array slice, negative from",
+ "selector": "$[-2:]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [8, 9]
+ }, {
+ "name": "union array slice, positive from, negative to",
+ "selector": "$[1:-1]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [1, 2, 3, 4, 5, 6, 7, 8]
+ }, {
+ "name": "union array slice, negative from, positive to, negative step",
+ "selector": "$[-1:1:-1]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [9, 8, 7, 6, 5, 4, 3, 2]
+ }, {
+ "name": "union array slice, positive from, negative to, negative step",
+ "selector": "$[7:-5:-1]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [7, 6]
+ }, {
+ "name": "union array slice, too many colons",
+ "selector": "$[1:2:3:4]",
+ "invalid_selector": true
+ }, {
+ "name": "union array slice, non-integer array index",
+ "selector": "$[1:2:a]",
+ "invalid_selector": true
+ }, {
+ "name": "union array slice, zero step",
+ "selector": "$[1:2:0]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": []
+ }, {
+ "name": "union array slice, empty range",
+ "selector": "$[2:2]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": []
+ }, {
+ "name": "union array slice, default indices with empty array",
+ "selector": "$[:]",
+ "document": [],
+ "result": []
+ }, {
+ "name": "union array slice, negative step with empty array",
+ "selector": "$[::-1]",
+ "document": [],
+ "result": []
+ }, {
+ "name": "union array slice, maximal range with positive step",
+ "selector": "$[0:10]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+ }, {
+ "name": "union array slice, maximal range with negative step",
+ "selector": "$[9:0:-1]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [9, 8, 7, 6, 5, 4, 3, 2, 1]
+ }, {
+ "name": "union array slice, excessively large to value",
+ "selector": "$[2:113667776004]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [2, 3, 4, 5, 6, 7, 8, 9]
+ }, {
+ "name": "union array slice, excessively small from value",
+ "selector": "$[-113667776004:1]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [0]
+ }, {
+ "name": "union array slice, excessively large from value with negative step",
+ "selector": "$[113667776004:0:-1]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [9, 8, 7, 6, 5, 4, 3, 2, 1]
+ }, {
+ "name": "union array slice, excessively small to value with negative step",
+ "selector": "$[3:-113667776004:-1]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [3, 2, 1, 0]
+ }, {
+ "name": "union array slice, excessively large step",
+ "selector": "$[1:10:113667776004]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [1]
+ }, {
+ "name": "union array slice, excessively small step",
+ "selector": "$[-1:-10:-113667776004]",
+ "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "result": [9]
+ }, {
+ "name": "union array slice, overflowing to value",
+ "selector": "$[2:231584178474632390847141970017375815706539969331281128078915168015826259279872]",
+ "invalid_selector": true
+ }, {
+ "name": "union array slice, underflowing from value",
+ "selector": "$[-231584178474632390847141970017375815706539969331281128078915168015826259279872:1]",
+ "invalid_selector": true
+ }, {
+ "name": "union array slice, overflowing from value with negative step",
+ "selector": "$[231584178474632390847141970017375815706539969331281128078915168015826259279872:0:-1]",
+ "invalid_selector": true
+ }, {
+ "name": "union array slice, underflowing to value with negative step",
+ "selector": "$[3:-231584178474632390847141970017375815706539969331281128078915168015826259279872:-1]",
+ "invalid_selector": true
+ }, {
+ "name": "union array slice, overflowing step",
+ "selector": "$[1:10:231584178474632390847141970017375815706539969331281128078915168015826259279872]",
+ "invalid_selector": true
+ }, {
+ "name": "union array slice, underflowing step",
+ "selector": "$[-1:-10:-231584178474632390847141970017375815706539969331281128078915168015826259279872]",
"invalid_selector": true
}
]}
diff --git a/tests/cts.rs b/tests/cts.rs
index 30b5de9..6b174a4 100644
--- a/tests/cts.rs
+++ b/tests/cts.rs
@@ -32,6 +32,9 @@ mod tests {
#[serde(default)]
focus: bool, // if true, run only tests with focus set to true
+
+ #[serde(default)]
+ skip: bool, // if true, do not run this test
}
#[test]
@@ -42,21 +45,22 @@ mod tests {
serde_json::from_str(&cts_json).expect("failed to deserialize cts.json");
let focussed = (&suite.tests).iter().find(|t| t.focus).is_some();
+ let skipped = (&suite.tests).iter().find(|t| t.skip).is_some();
let mut errors: Vec = Vec::new();
for t in suite.tests {
- if focussed && !t.focus {
+ if t.skip || (focussed && !t.focus) {
continue;
}
let result = panic::catch_unwind(|| {
if t.invalid_selector {
println!(
- "testcase name = `{}`, selector = `{}`, expected invalid selector.",
+ "testcase name = `{}`, selector = `{}`, expecting invalid selector.",
t.name, t.selector
);
} else {
println!(
- "testcase name = `{}`, selector = `{}`, document:\n{:#}\nexpected result = `{}`.",
+ "testcase name = `{}`, selector = `{}`, document:\n{:#}\nexpecting result = `{}`.",
t.name, t.selector, t.document, t.result
);
}
@@ -100,10 +104,13 @@ mod tests {
errors.push(format!("{:?}", err));
}
}
- assert!(errors.is_empty());
+ assert!(errors.is_empty(), "testcase(s) failed, see above");
if focussed {
assert!(false, "testcase(s) still focussed")
}
+ if skipped {
+ assert!(false, "testcase(s) still skipped")
+ }
}
fn equal(actual: &Vec<&serde_json::Value>, expected: Vec) -> bool {