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 {