mirror of
https://github.com/Orange-OpenSource/hurl.git
synced 2024-09-11 05:35:53 +03:00
Add jsonpath filter
This commit is contained in:
parent
f750931f15
commit
74edc18bd2
@ -424,6 +424,7 @@ filter:
|
||||
| format-filter
|
||||
| html-escape-filter
|
||||
| html-unescape-filter
|
||||
| json-filter
|
||||
| nth-filter
|
||||
| regex-filter
|
||||
| replace-filter
|
||||
|
@ -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>
|
||||
|
@ -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\"}"
|
||||
}
|
||||
|
@ -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 > b && a < 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 > b && a < c","Foo © bar 𝌆 baz ☃ qux","A 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 > b && a < 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 > b && a < c","Foo © bar 𝌆 baz ☃ qux","A foo"],"id":"123","score":1.6,"ips":"192.168.2.1, 10.0.0.20, 10.0.0.10","json":"{\"message\": \"Hello\"}"}}}}]}
|
||||
|
@ -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\"}"
|
||||
}
|
@ -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\\"}"
|
||||
}"""
|
||||
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -864,6 +864,10 @@ pub enum FilterValue {
|
||||
},
|
||||
HtmlEscape,
|
||||
HtmlUnescape,
|
||||
JsonPath {
|
||||
space0: Whitespace,
|
||||
expr: Template,
|
||||
},
|
||||
Nth {
|
||||
space0: Whitespace,
|
||||
n: u64,
|
||||
|
@ -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);
|
||||
|
@ -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)?;
|
||||
|
@ -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())));
|
||||
|
@ -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());
|
||||
|
Loading…
Reference in New Issue
Block a user