Add jsonpath filter

This commit is contained in:
Fabrice Reix 2023-11-08 13:40:14 +01:00
parent f750931f15
commit 74edc18bd2
No known key found for this signature in database
GPG Key ID: BF5213154B2E7155
13 changed files with 141 additions and 47 deletions

View File

@ -424,6 +424,7 @@ filter:
| format-filter
| html-escape-filter
| html-unescape-filter
| json-filter
| nth-filter
| regex-filter
| replace-filter

View File

@ -22,6 +22,7 @@
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.score"</span> <span class="filter-type">toInt</span> <span class="predicate-type">==</span> <span class="number">1</span></span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.ips"</span> <span class="filter-type">split</span> <span class="string">", "</span> <span class="filter-type">count</span> <span class="predicate-type">==</span> <span class="number">3</span></span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.ips"</span> <span class="filter-type">replace</span> <span class="string">", "</span> <span class="string">"|"</span> <span class="predicate-type">==</span> <span class="string">"192.168.2.1|10.0.0.20|10.0.0.10"</span></span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.json"</span> <span class="filter-type">jsonpath</span> <span class="string">"$.message"</span> <span class="predicate-type">==</span> <span class="string">"Hello"</span></span>
<span class="json"><span class="line">{</span>
<span class="line"> "list": [1,2,3],</span>
<span class="line"> "message": "Hello Bob!",</span>
@ -35,6 +36,7 @@
<span class="line"> ],</span>
<span class="line"> "id": "123",</span>
<span class="line"> "score": 1.6,</span>
<span class="line"> "ips": "192.168.2.1, 10.0.0.20, 10.0.0.10"</span>
<span class="line"> "ips": "192.168.2.1, 10.0.0.20, 10.0.0.10",</span>
<span class="line"> "json": "{\"message\": \"Hello\"}"</span>
<span class="line">}</span></span>
</span></span></code></pre>

View File

@ -22,6 +22,7 @@ jsonpath "$.id" toInt == 123
jsonpath "$.score" toInt == 1
jsonpath "$.ips" split ", " count == 3
jsonpath "$.ips" replace ", " "|" == "192.168.2.1|10.0.0.20|10.0.0.10"
jsonpath "$.json" jsonpath "$.message" == "Hello"
{
"list": [1,2,3],
"message": "Hello Bob!",
@ -35,5 +36,6 @@ jsonpath "$.ips" replace ", " "|" == "192.168.2.1|10.0.0.20|10.0.0.10"
],
"id": "123",
"score": 1.6,
"ips": "192.168.2.1, 10.0.0.20, 10.0.0.10"
"ips": "192.168.2.1, 10.0.0.20, 10.0.0.10",
"json": "{\"message\": \"Hello\"}"
}

View File

@ -1 +1 @@
{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/filter"},"response":{"status":200,"captures":[{"name":"url","query":{"type":"jsonpath","expr":"$.url"}},{"name":"text","query":{"type":"jsonpath","expr":"$.text"}}],"asserts":[{"query":{"type":"jsonpath","expr":"$.list"},"filters":[{"type":"count"}],"predicate":{"type":"equal","value":3}},{"query":{"type":"jsonpath","expr":"$.list"},"filters":[{"type":"nth","n":1}],"predicate":{"type":"equal","value":2}},{"query":{"type":"jsonpath","expr":"$.message"},"filters":[{"type":"regex","expr":{"type":"regex","value":"Hello (.*)!"}}],"predicate":{"type":"equal","value":"Bob"}},{"query":{"type":"jsonpath","expr":"$.url"},"predicate":{"type":"equal","value":"https://mozilla.org/?x=шеллы"}},{"query":{"type":"jsonpath","expr":"$.url"},"filters":[{"type":"urlEncode"}],"predicate":{"type":"equal","value":"https%3A//mozilla.org/%3Fx%3D%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"}},{"query":{"type":"jsonpath","expr":"$.encoded_url"},"filters":[{"type":"urlDecode"}],"predicate":{"type":"equal","value":"https://mozilla.org/?x=шеллы"}},{"query":{"type":"variable","name":"url"},"filters":[{"type":"urlEncode"},{"type":"urlDecode"}],"predicate":{"type":"equal","value":"{{url}}"}},{"query":{"type":"jsonpath","expr":"$.text"},"predicate":{"type":"equal","value":"a > b && a < c"}},{"query":{"type":"jsonpath","expr":"$.text"},"filters":[{"type":"htmlEscape"}],"predicate":{"type":"equal","value":"a &gt; b &amp;&amp; a &lt; c"}},{"query":{"type":"jsonpath","expr":"$.escaped_html[0]"},"filters":[{"type":"htmlUnescape"}],"predicate":{"type":"equal","value":"a > b && a < c"}},{"query":{"type":"jsonpath","expr":"$.escaped_html[1]"},"filters":[{"type":"htmlUnescape"}],"predicate":{"type":"equal","value":"Foo © bar 𝌆 baz ☃ qux"}},{"query":{"type":"jsonpath","expr":"$.escaped_html[2]"},"filters":[{"type":"htmlUnescape"}],"predicate":{"type":"equal","value":"A foo"}},{"query":{"type":"variable","name":"text"},"filters":[{"type":"htmlEscape"},{"type":"htmlUnescape"}],"predicate":{"type":"equal","value":"{{text}}"}},{"query":{"type":"jsonpath","expr":"$.id"},"filters":[{"type":"toInt"}],"predicate":{"type":"equal","value":123}},{"query":{"type":"jsonpath","expr":"$.score"},"filters":[{"type":"toInt"}],"predicate":{"type":"equal","value":1}},{"query":{"type":"jsonpath","expr":"$.ips"},"filters":[{"type":"split","sep":", "},{"type":"count"}],"predicate":{"type":"equal","value":3}},{"query":{"type":"jsonpath","expr":"$.ips"},"filters":[{"type":"replace","old_value":", ","new_value":"|"}],"predicate":{"type":"equal","value":"192.168.2.1|10.0.0.20|10.0.0.10"}}],"body":{"type":"json","value":{"list":[1,2,3],"message":"Hello Bob!","url":"https://mozilla.org/?x=шеллы","encoded_url":"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B","text":"a > b && a < c","escaped_html":["a &gt; b &amp;&amp; a &lt; c","Foo &#xA9; bar &#x1D306; baz &#x2603; qux","&#65 foo"],"id":"123","score":1.6,"ips":"192.168.2.1, 10.0.0.20, 10.0.0.10"}}}}]}
{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/filter"},"response":{"status":200,"captures":[{"name":"url","query":{"type":"jsonpath","expr":"$.url"}},{"name":"text","query":{"type":"jsonpath","expr":"$.text"}}],"asserts":[{"query":{"type":"jsonpath","expr":"$.list"},"filters":[{"type":"count"}],"predicate":{"type":"equal","value":3}},{"query":{"type":"jsonpath","expr":"$.list"},"filters":[{"type":"nth","n":1}],"predicate":{"type":"equal","value":2}},{"query":{"type":"jsonpath","expr":"$.message"},"filters":[{"type":"regex","expr":{"type":"regex","value":"Hello (.*)!"}}],"predicate":{"type":"equal","value":"Bob"}},{"query":{"type":"jsonpath","expr":"$.url"},"predicate":{"type":"equal","value":"https://mozilla.org/?x=шеллы"}},{"query":{"type":"jsonpath","expr":"$.url"},"filters":[{"type":"urlEncode"}],"predicate":{"type":"equal","value":"https%3A//mozilla.org/%3Fx%3D%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"}},{"query":{"type":"jsonpath","expr":"$.encoded_url"},"filters":[{"type":"urlDecode"}],"predicate":{"type":"equal","value":"https://mozilla.org/?x=шеллы"}},{"query":{"type":"variable","name":"url"},"filters":[{"type":"urlEncode"},{"type":"urlDecode"}],"predicate":{"type":"equal","value":"{{url}}"}},{"query":{"type":"jsonpath","expr":"$.text"},"predicate":{"type":"equal","value":"a > b && a < c"}},{"query":{"type":"jsonpath","expr":"$.text"},"filters":[{"type":"htmlEscape"}],"predicate":{"type":"equal","value":"a &gt; b &amp;&amp; a &lt; c"}},{"query":{"type":"jsonpath","expr":"$.escaped_html[0]"},"filters":[{"type":"htmlUnescape"}],"predicate":{"type":"equal","value":"a > b && a < c"}},{"query":{"type":"jsonpath","expr":"$.escaped_html[1]"},"filters":[{"type":"htmlUnescape"}],"predicate":{"type":"equal","value":"Foo © bar 𝌆 baz ☃ qux"}},{"query":{"type":"jsonpath","expr":"$.escaped_html[2]"},"filters":[{"type":"htmlUnescape"}],"predicate":{"type":"equal","value":"A foo"}},{"query":{"type":"variable","name":"text"},"filters":[{"type":"htmlEscape"},{"type":"htmlUnescape"}],"predicate":{"type":"equal","value":"{{text}}"}},{"query":{"type":"jsonpath","expr":"$.id"},"filters":[{"type":"toInt"}],"predicate":{"type":"equal","value":123}},{"query":{"type":"jsonpath","expr":"$.score"},"filters":[{"type":"toInt"}],"predicate":{"type":"equal","value":1}},{"query":{"type":"jsonpath","expr":"$.ips"},"filters":[{"type":"split","sep":", "},{"type":"count"}],"predicate":{"type":"equal","value":3}},{"query":{"type":"jsonpath","expr":"$.ips"},"filters":[{"type":"replace","old_value":", ","new_value":"|"}],"predicate":{"type":"equal","value":"192.168.2.1|10.0.0.20|10.0.0.10"}},{"query":{"type":"jsonpath","expr":"$.json"},"filters":[{"type":"jsonpath","expr":"$.message"}],"predicate":{"type":"equal","value":"Hello"}}],"body":{"type":"json","value":{"list":[1,2,3],"message":"Hello Bob!","url":"https://mozilla.org/?x=шеллы","encoded_url":"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B","text":"a > b && a < c","escaped_html":["a &gt; b &amp;&amp; a &lt; c","Foo &#xA9; bar &#x1D306; baz &#x2603; qux","&#65 foo"],"id":"123","score":1.6,"ips":"192.168.2.1, 10.0.0.20, 10.0.0.10","json":"{\"message\": \"Hello\"}"}}}}]}

View File

@ -11,5 +11,6 @@
],
"id": "123",
"score": 1.6,
"ips": "192.168.2.1, 10.0.0.20, 10.0.0.10"
"ips": "192.168.2.1, 10.0.0.20, 10.0.0.10",
"json": "{\"message\": \"Hello\"}"
}

View File

@ -16,5 +16,6 @@ def filter():
],
"id": "123",
"score": 1.6,
"ips": "192.168.2.1, 10.0.0.20, 10.0.0.10"
"ips": "192.168.2.1, 10.0.0.20, 10.0.0.10",
"json": "{\\"message\\": \\"Hello\\"}"
}"""

View File

@ -23,11 +23,11 @@ use encoding::DecoderTrap;
use hurl_core::ast::{Filter, FilterValue, RegexValue, SourceInfo, Template};
use percent_encoding::AsciiSet;
use crate::html;
use crate::runner::regex::eval_regex_value;
use crate::runner::template::eval_template;
use crate::runner::xpath;
use crate::runner::{Error, RunnerError, Value};
use crate::{html, jsonpath};
/// Apply successive `filters` to an input `value`.
/// Specify whether they are executed `in_assert` or not.
@ -70,6 +70,9 @@ fn eval_filter(
}
FilterValue::HtmlEscape => eval_html_escape(value, &filter.source_info, in_assert),
FilterValue::HtmlUnescape => eval_html_unescape(value, &filter.source_info, in_assert),
FilterValue::JsonPath { expr, .. } => {
eval_jsonpath(value, expr, variables, &filter.source_info, in_assert)
}
FilterValue::Regex {
value: regex_value, ..
} => eval_regex(
@ -451,6 +454,62 @@ fn eval_to_int(
}
}
pub fn eval_jsonpath(
value: &Value,
expr: &Template,
variables: &HashMap<String, Value>,
source_info: &SourceInfo,
assert: bool,
) -> Result<Option<Value>, Error> {
match value {
Value::String(json) => eval_jsonpath_string(json, expr, variables, source_info),
v => Err(Error {
source_info: source_info.clone(),
inner: RunnerError::FilterInvalidInput(v._type()),
assert,
}),
}
}
pub fn eval_jsonpath_string(
json: &str,
expr: &Template,
variables: &HashMap<String, Value>,
source_info: &SourceInfo,
) -> Result<Option<Value>, Error> {
let value = eval_template(expr, variables)?;
let expr_source_info = &expr.source_info;
let jsonpath_query = match jsonpath::parse(value.as_str()) {
Ok(q) => q,
Err(_) => {
return Err(Error {
source_info: expr_source_info.clone(),
inner: RunnerError::QueryInvalidJsonpathExpression { value },
assert: false,
});
}
};
let value = match serde_json::from_str(json) {
Err(_) => {
return Err(Error {
source_info: source_info.clone(),
inner: RunnerError::QueryInvalidJson,
assert: false,
});
}
Ok(v) => v,
};
let results = jsonpath_query.eval(&value);
match results {
None => Ok(None),
Some(jsonpath::JsonpathResult::SingleEntry(value)) => Ok(Some(Value::from_json(&value))),
Some(jsonpath::JsonpathResult::Collection(values)) => {
Ok(Some(Value::from_json(&serde_json::Value::Array(values))))
}
}
}
pub fn eval_xpath(
value: &Value,
expr: &Template,
@ -1111,4 +1170,38 @@ pub mod tests {
Value::Date(datetime_utc)
);
}
#[test]
pub fn eval_filter_jsonpath() {
let variables = HashMap::new();
let filter = Filter {
source_info: SourceInfo::new(1, 1, 1, 1),
value: FilterValue::JsonPath {
expr: Template {
delimiter: Some('"'),
elements: vec![TemplateElement::String {
value: "$.message".to_string(),
encoded: "$.message".to_string(),
}],
source_info: SourceInfo::new(0, 0, 0, 0),
},
space0: Whitespace {
value: String::new(),
source_info: SourceInfo::new(0, 0, 0, 0),
},
},
};
assert_eq!(
eval_filter(
&filter,
&Value::String(r#"{"message":"Hello"}"#.to_string()),
&variables,
false
)
.unwrap()
.unwrap(),
Value::String("Hello".to_string())
);
}
}

View File

@ -26,7 +26,7 @@ use crate::runner::filter;
use crate::runner::template::eval_template;
use crate::runner::value::Value;
use crate::{http, jsonpath};
use crate::http;
pub type QueryResult = Result<Option<Value>, Error>;
@ -151,46 +151,13 @@ fn eval_query_jsonpath(
variables: &HashMap<String, Value>,
query_source_info: &SourceInfo,
) -> QueryResult {
let value = eval_template(expr, variables)?;
let expr_source_info = &expr.source_info;
let jsonpath_query = match jsonpath::parse(value.as_str()) {
Ok(q) => q,
Err(_) => {
return Err(Error {
source_info: expr_source_info.clone(),
inner: RunnerError::QueryInvalidJsonpathExpression { value },
assert: false,
});
}
};
let json = match response.text() {
Err(inner) => {
return Err(Error {
source_info: query_source_info.clone(),
inner: RunnerError::from(inner),
assert: false,
});
}
Ok(v) => v,
};
let value = match serde_json::from_str(json.as_str()) {
Err(_) => {
return Err(Error {
source_info: query_source_info.clone(),
inner: RunnerError::QueryInvalidJson,
assert: false,
});
}
Ok(v) => v,
};
let results = jsonpath_query.eval(&value);
match results {
None => Ok(None),
Some(jsonpath::JsonpathResult::SingleEntry(value)) => Ok(Some(Value::from_json(&value))),
Some(jsonpath::JsonpathResult::Collection(values)) => {
Ok(Some(Value::from_json(&serde_json::Value::Array(values))))
}
match response.text() {
Err(inner) => Err(Error {
source_info: query_source_info.clone(),
inner: RunnerError::from(inner),
assert: false,
}),
Ok(json) => filter::eval_jsonpath_string(&json, expr, variables, query_source_info),
}
}

View File

@ -864,6 +864,10 @@ pub enum FilterValue {
},
HtmlEscape,
HtmlUnescape,
JsonPath {
space0: Whitespace,
expr: Template,
},
Nth {
space0: Whitespace,
n: u64,

View File

@ -782,6 +782,11 @@ impl HtmlFormatter {
}
FilterValue::HtmlEscape => self.fmt_span("filter-type", "htmlEscape"),
FilterValue::HtmlUnescape => self.fmt_span("filter-type", "htmlUnescape"),
FilterValue::JsonPath { space0, expr } => {
self.fmt_span("filter-type", "jsonpath");
self.fmt_space(space0);
self.fmt_template(expr);
}
FilterValue::Nth { space0, n: value } => {
self.fmt_span("filter-type", "nth");
self.fmt_space(space0);

View File

@ -58,6 +58,7 @@ pub fn filter(reader: &mut Reader) -> ParseResult<Filter> {
format_filter,
html_decode_filter,
html_encode_filter,
jsonpath_filter,
nth_filter,
regex_filter,
replace_filter,
@ -127,6 +128,13 @@ fn html_decode_filter(reader: &mut Reader) -> ParseResult<FilterValue> {
Ok(FilterValue::HtmlUnescape)
}
fn jsonpath_filter(reader: &mut Reader) -> ParseResult<FilterValue> {
try_literal("jsonpath", reader)?;
let space0 = one_or_more_spaces(reader)?;
let expr = quoted_template(reader).map_err(|e| e.non_recoverable())?;
Ok(FilterValue::JsonPath { space0, expr })
}
fn nth_filter(reader: &mut Reader) -> ParseResult<FilterValue> {
try_literal("nth", reader)?;
let space0 = one_or_more_spaces(reader)?;

View File

@ -639,6 +639,10 @@ impl ToJson for FilterValue {
attributes.push(("type".to_string(), JValue::String("format".to_string())));
attributes.push(("fmt".to_string(), JValue::String(fmt.to_string())));
}
FilterValue::JsonPath { expr, .. } => {
attributes.push(("type".to_string(), JValue::String("jsonpath".to_string())));
attributes.push(("expr".to_string(), JValue::String(expr.to_string())));
}
FilterValue::Nth { n, .. } => {
attributes.push(("type".to_string(), JValue::String("nth".to_string())));
attributes.push(("n".to_string(), JValue::Number(n.to_string())));

View File

@ -987,6 +987,12 @@ impl Tokenizable for Filter {
FilterValue::HtmlUnescape => {
vec![Token::FilterType(String::from("htmlUnescape"))]
}
FilterValue::JsonPath { space0, expr } => {
let mut tokens: Vec<Token> = vec![Token::FilterType(String::from("jsonpath"))];
tokens.append(&mut space0.tokenize());
tokens.append(&mut expr.tokenize());
tokens
}
FilterValue::Nth { space0, n } => {
let mut tokens: Vec<Token> = vec![Token::FilterType(String::from("nth"))];
tokens.append(&mut space0.tokenize());