diff --git a/integration/tests_failed/filter.err b/integration/tests_failed/filter.err index cfad5146d..8c423c237 100644 --- a/integration/tests_failed/filter.err +++ b/integration/tests_failed/filter.err @@ -19,3 +19,9 @@ error: Filter Error | ^^^^^ missing value to apply filter | +error: Filter Error + --> tests_failed/filter.hurl:7:19 + | + 7 | jsonpath "$.list" nth 5 == 3 + | ^^^^^ invalid filter input: Out of bound - size is 3 + | \ No newline at end of file diff --git a/integration/tests_failed/filter.hurl b/integration/tests_failed/filter.hurl index 97fe969c5..fb01cc91a 100644 --- a/integration/tests_failed/filter.hurl +++ b/integration/tests_failed/filter.hurl @@ -4,3 +4,4 @@ HTTP 200 jsonpath "$.id" toInt == 123 jsonpath "$.status" toInt == 0 jsonpath "$.unknown" toInt == 1 +jsonpath "$.list" nth 5 == 3 diff --git a/integration/tests_failed/filter.py b/integration/tests_failed/filter.py index c64b5eae8..2c0a0fc75 100644 --- a/integration/tests_failed/filter.py +++ b/integration/tests_failed/filter.py @@ -7,7 +7,8 @@ def error_filter(): return Response( """{ "id":"123x", - "status": true + "status": true, + "list": [1,2,3] } """, mimetype="application/json", diff --git a/integration/tests_ok/filter.html b/integration/tests_ok/filter.html index 34062bc44..bf464a53c 100644 --- a/integration/tests_ok/filter.html +++ b/integration/tests_ok/filter.html @@ -6,6 +6,7 @@ text: jsonpath "$.text" [Asserts] jsonpath "$.list" count == 3 +jsonpath "$.list" nth 1 == 2 jsonpath "$.message" regex /Hello (.*)!/ == "Bob" jsonpath "$.url" == "https://mozilla.org/?x=шеллы" jsonpath "$.url" urlEncode == "https%3A//mozilla.org/%3Fx%3D%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" @@ -19,6 +20,8 @@ variable "text" htmlEscape htmlUnescape == "{{text}}" 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" { "list": [1,2,3], "message": "Hello Bob!", @@ -31,6 +34,7 @@ "&#65 foo" ], "id": "123", - "score": 1.6 + "score": 1.6, + "ips": "192.168.2.1, 10.0.0.20, 10.0.0.10" } diff --git a/integration/tests_ok/filter.hurl b/integration/tests_ok/filter.hurl index eeaa69d61..26c7872e3 100644 --- a/integration/tests_ok/filter.hurl +++ b/integration/tests_ok/filter.hurl @@ -6,6 +6,7 @@ url: jsonpath "$.url" text: jsonpath "$.text" [Asserts] jsonpath "$.list" count == 3 +jsonpath "$.list" nth 1 == 2 jsonpath "$.message" regex /Hello (.*)!/ == "Bob" jsonpath "$.url" == "https://mozilla.org/?x=шеллы" jsonpath "$.url" urlEncode == "https%3A//mozilla.org/%3Fx%3D%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" @@ -19,6 +20,8 @@ jsonpath "$.escaped_html[2]" htmlUnescape == "A foo" variable "text" htmlEscape htmlUnescape == "{{text}}" 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" { "list": [1,2,3], "message": "Hello Bob!", @@ -31,5 +34,6 @@ jsonpath "$.score" toInt == 1 "A foo" ], "id": "123", - "score": 1.6 + "score": 1.6, + "ips": "192.168.2.1, 10.0.0.20, 10.0.0.10" } diff --git a/integration/tests_ok/filter.json b/integration/tests_ok/filter.json index 5076c33c2..68ba22df4 100644 --- a/integration/tests_ok/filter.json +++ b/integration/tests_ok/filter.json @@ -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":"$.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}}],"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}}}}]} +{"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"}}}}]} diff --git a/integration/tests_ok/filter.out b/integration/tests_ok/filter.out index dd0488d10..a522cd911 100644 --- a/integration/tests_ok/filter.out +++ b/integration/tests_ok/filter.out @@ -10,5 +10,6 @@ "A foo" ], "id": "123", - "score": 1.6 + "score": 1.6, + "ips": "192.168.2.1, 10.0.0.20, 10.0.0.10" } \ No newline at end of file diff --git a/integration/tests_ok/filter.py b/integration/tests_ok/filter.py index 7678c4734..9d3cff1b4 100644 --- a/integration/tests_ok/filter.py +++ b/integration/tests_ok/filter.py @@ -15,5 +15,6 @@ def filter(): "A foo" ], "id": "123", - "score": 1.6 + "score": 1.6, + "ips": "192.168.2.1, 10.0.0.20, 10.0.0.10" }""" diff --git a/packages/hurl/src/runner/filter.rs b/packages/hurl/src/runner/filter.rs index bfb849e01..d0b5312ae 100644 --- a/packages/hurl/src/runner/filter.rs +++ b/packages/hurl/src/runner/filter.rs @@ -15,13 +15,16 @@ * limitations under the License. * */ +use std::collections::HashMap; + +use percent_encoding::AsciiSet; + +use hurl_core::ast::{Filter, FilterValue, RegexValue, SourceInfo, Template}; + use crate::html; +use crate::runner::regex::eval_regex_value; use crate::runner::template::eval_template; use crate::runner::{Error, RunnerError, Value}; -use hurl_core::ast::{Filter, FilterValue, RegexValue, SourceInfo}; -use percent_encoding::AsciiSet; -use regex::Regex; -use std::collections::HashMap; // TODO: indicated whether you running the filter in an assert / this produce an "assert" error pub fn eval_filters( @@ -42,15 +45,22 @@ fn eval_filter( variables: &HashMap, ) -> Result { match &filter.value { + FilterValue::Count => eval_count(value, &filter.source_info), + FilterValue::HtmlEscape => eval_html_escape(value, &filter.source_info), + FilterValue::HtmlUnescape => eval_html_unescape(value, &filter.source_info), FilterValue::Regex { value: regex_value, .. } => eval_regex(value, regex_value, variables, &filter.source_info), - FilterValue::Count => eval_count(value, &filter.source_info), - FilterValue::UrlEncode => eval_url_encode(value, &filter.source_info), - FilterValue::UrlDecode => eval_url_decode(value, &filter.source_info), - FilterValue::HtmlEscape => eval_html_encode(value, &filter.source_info), - FilterValue::HtmlUnescape => eval_html_decode(value, &filter.source_info), + FilterValue::Nth { n, .. } => eval_nth(value, &filter.source_info, *n), + FilterValue::Replace { + old_value, + new_value, + .. + } => eval_replace(value, variables, &filter.source_info, old_value, new_value), + FilterValue::Split { sep, .. } => eval_split(value, variables, &filter.source_info, sep), FilterValue::ToInt => eval_to_int(value, &filter.source_info), + FilterValue::UrlDecode => eval_url_decode(value, &filter.source_info), + FilterValue::UrlEncode => eval_url_encode(value, &filter.source_info), } } @@ -60,23 +70,7 @@ fn eval_regex( variables: &HashMap, source_info: &SourceInfo, ) -> Result { - let re = match regex_value { - RegexValue::Template(t) => { - let value = eval_template(t, variables)?; - match Regex::new(value.as_str()) { - Ok(re) => re, - Err(_) => { - return Err(Error { - source_info: t.source_info.clone(), - inner: RunnerError::InvalidRegex(), - assert: false, - }); - } - } - } - RegexValue::Regex(re) => re.inner.clone(), - }; - + let re = eval_regex_value(regex_value, variables)?; match value { Value::String(s) => match re.captures(s.as_str()) { Some(captures) => match captures.get(1) { @@ -156,7 +150,28 @@ fn eval_url_decode(value: &Value, source_info: &SourceInfo) -> Result Result { +fn eval_nth(value: &Value, source_info: &SourceInfo, n: u64) -> Result { + match value { + Value::List(values) => match values.get(n as usize) { + None => Err(Error { + source_info: source_info.clone(), + inner: RunnerError::FilterInvalidInput(format!( + "Out of bound - size is {}", + values.len() + )), + assert: false, + }), + Some(value) => Ok(value.clone()), + }, + v => Err(Error { + source_info: source_info.clone(), + inner: RunnerError::FilterInvalidInput(v.display()), + assert: false, + }), + } +} + +fn eval_html_escape(value: &Value, source_info: &SourceInfo) -> Result { match value { Value::String(value) => { let encoded = html::html_escape(value); @@ -170,7 +185,7 @@ fn eval_html_encode(value: &Value, source_info: &SourceInfo) -> Result Result { +fn eval_html_unescape(value: &Value, source_info: &SourceInfo) -> Result { match value { Value::String(value) => { let decoded = html::html_unescape(value); @@ -184,6 +199,51 @@ fn eval_html_decode(value: &Value, source_info: &SourceInfo) -> Result, + source_info: &SourceInfo, + old_value: &RegexValue, + new_value: &Template, +) -> Result { + match value { + Value::String(v) => { + let re = eval_regex_value(old_value, variables)?; + let new_value = eval_template(new_value, variables)?; + let s = re.replace_all(v, new_value).to_string(); + Ok(Value::String(s)) + } + v => Err(Error { + source_info: source_info.clone(), + inner: RunnerError::FilterInvalidInput(v.display()), + assert: false, + }), + } +} + +fn eval_split( + value: &Value, + variables: &HashMap, + source_info: &SourceInfo, + sep: &Template, +) -> Result { + match value { + Value::String(s) => { + let sep = eval_template(sep, variables)?; + let values = s + .split(&sep) + .map(|v| Value::String(v.to_string())) + .collect(); + Ok(Value::List(values)) + } + v => Err(Error { + source_info: source_info.clone(), + inner: RunnerError::FilterInvalidInput(v.display()), + assert: false, + }), + } +} + fn eval_to_int(value: &Value, source_info: &SourceInfo) -> Result { match value { Value::Integer(v) => Ok(Value::Integer(*v)), @@ -206,9 +266,10 @@ fn eval_to_int(value: &Value, source_info: &SourceInfo) -> Result #[cfg(test)] pub mod tests { - use super::*; use hurl_core::ast::{FilterValue, SourceInfo, Template, TemplateElement, Whitespace}; + use super::*; + pub fn filter_count() -> Filter { Filter { source_info: SourceInfo::new(1, 1, 1, 6), @@ -468,4 +529,118 @@ pub mod tests { ); } } + + #[test] + pub fn eval_filter_nth() { + let variables = HashMap::new(); + let filter = Filter { + source_info: SourceInfo::new(1, 1, 1, 1), + value: FilterValue::Nth { + n: 2, + space0: Whitespace { + value: String::from(""), + source_info: SourceInfo::new(0, 0, 0, 0), + }, + }, + }; + + assert_eq!( + eval_filter( + &filter, + &Value::List(vec![ + Value::Integer(0), + Value::Integer(1), + Value::Integer(2), + Value::Integer(3) + ]), + &variables + ) + .unwrap(), + Value::Integer(2) + ); + assert_eq!( + eval_filter( + &filter, + &Value::List(vec![Value::Integer(0), Value::Integer(1)]), + &variables + ) + .err() + .unwrap(), + Error { + source_info: SourceInfo::new(1, 1, 1, 1), + inner: RunnerError::FilterInvalidInput("Out of bound - size is 2".to_string()), + assert: false + } + ); + } + + #[test] + pub fn eval_filter_replace() { + let variables = HashMap::new(); + let filter = Filter { + source_info: SourceInfo::new(1, 1, 1, 1), + value: FilterValue::Replace { + old_value: RegexValue::Template(Template { + delimiter: None, + elements: vec![TemplateElement::String { + value: "\\s+".to_string(), + encoded: ",".to_string(), + }], + source_info: SourceInfo::new(1, 7, 1, 20), + }), + new_value: Template { + delimiter: Some('"'), + elements: vec![TemplateElement::String { + value: ",".to_string(), + encoded: ",".to_string(), + }], + source_info: SourceInfo::new(0, 0, 0, 0), + }, + space0: Whitespace { + value: String::from(""), + source_info: SourceInfo::new(0, 0, 0, 0), + }, + space1: Whitespace { + value: String::from(""), + source_info: SourceInfo::new(0, 0, 0, 0), + }, + }, + }; + + assert_eq!( + eval_filter(&filter, &Value::String("1 2\t3 4".to_string()), &variables).unwrap(), + Value::String("1,2,3,4".to_string()) + ); + } + + #[test] + pub fn eval_filter_split() { + let variables = HashMap::new(); + let filter = Filter { + source_info: SourceInfo::new(1, 1, 1, 1), + value: FilterValue::Split { + sep: Template { + delimiter: Some('"'), + elements: vec![TemplateElement::String { + value: ",".to_string(), + encoded: ",".to_string(), + }], + source_info: SourceInfo::new(0, 0, 0, 0), + }, + space0: Whitespace { + value: String::from(""), + source_info: SourceInfo::new(0, 0, 0, 0), + }, + }, + }; + + assert_eq!( + eval_filter(&filter, &Value::String("1,2,3".to_string()), &variables).unwrap(), + Value::List(vec![ + Value::String("1".to_string()), + Value::String("2".to_string()), + Value::String("3".to_string()), + ]) + ); + } } diff --git a/packages/hurl/src/runner/mod.rs b/packages/hurl/src/runner/mod.rs index 1ac20fb25..a5c06f446 100644 --- a/packages/hurl/src/runner/mod.rs +++ b/packages/hurl/src/runner/mod.rs @@ -45,6 +45,7 @@ mod multipart; mod predicate; mod predicate_value; mod query; +mod regex; mod request; mod response; mod runner_options; diff --git a/packages/hurl/src/runner/regex.rs b/packages/hurl/src/runner/regex.rs new file mode 100644 index 000000000..babcd8507 --- /dev/null +++ b/packages/hurl/src/runner/regex.rs @@ -0,0 +1,46 @@ +/* + * Hurl (https://hurl.dev) + * Copyright (C) 2023 Orange + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +use std::collections::HashMap; + +use regex::Regex; + +use hurl_core::ast::RegexValue; + +use crate::runner::template::eval_template; +use crate::runner::{Error, RunnerError, Value}; + +pub fn eval_regex_value( + regex_value: &RegexValue, + variables: &HashMap, +) -> Result { + match regex_value { + RegexValue::Template(t) => { + let value = eval_template(t, variables)?; + match Regex::new(value.as_str()) { + Ok(re) => Ok(re), + Err(_) => Err(Error { + source_info: t.source_info.clone(), + inner: RunnerError::InvalidRegex(), + assert: false, + }), + } + } + RegexValue::Regex(re) => Ok(re.inner.clone()), + } +} diff --git a/packages/hurl_core/src/ast/core.rs b/packages/hurl_core/src/ast/core.rs index 4fb608c75..86bfa2bb9 100644 --- a/packages/hurl_core/src/ast/core.rs +++ b/packages/hurl_core/src/ast/core.rs @@ -872,13 +872,27 @@ pub struct Filter { #[derive(Clone, Debug, PartialEq, Eq)] pub enum FilterValue { Count, + HtmlEscape, + HtmlUnescape, + Nth { + space0: Whitespace, + n: u64, + }, Regex { space0: Whitespace, value: RegexValue, }, - UrlEncode, - UrlDecode, - HtmlEscape, - HtmlUnescape, + Replace { + space0: Whitespace, + old_value: RegexValue, + space1: Whitespace, + new_value: Template, + }, + Split { + space0: Whitespace, + sep: Template, + }, ToInt, + UrlDecode, + UrlEncode, } diff --git a/packages/hurl_core/src/format/html.rs b/packages/hurl_core/src/format/html.rs index 480fbe972..86a3c7674 100644 --- a/packages/hurl_core/src/format/html.rs +++ b/packages/hurl_core/src/format/html.rs @@ -1079,6 +1079,16 @@ impl Htmlable for FilterValue { fn to_html(&self) -> String { match self { FilterValue::Count => "count".to_string(), + FilterValue::HtmlEscape => "htmlEscape".to_string(), + FilterValue::HtmlUnescape => { + "htmlUnescape".to_string() + } + FilterValue::Nth { space0, n: value } => { + let mut buffer = "nth".to_string(); + buffer.push_str(space0.to_html().as_str()); + buffer.push_str(&value.to_string()); + buffer + } FilterValue::Regex { space0, value } => { let mut buffer = "".to_string(); buffer.push_str("regex"); @@ -1086,13 +1096,28 @@ impl Htmlable for FilterValue { buffer.push_str(value.to_html().as_str()); buffer } - FilterValue::UrlEncode => "urlEncode".to_string(), - FilterValue::UrlDecode => "urlDecode".to_string(), - FilterValue::HtmlEscape => "htmlEscape".to_string(), - FilterValue::HtmlUnescape => { - "htmlUnescape".to_string() + FilterValue::Replace { + space0, + old_value, + space1, + new_value, + } => { + let mut buffer = "replace".to_string(); + buffer.push_str(space0.to_html().as_str()); + buffer.push_str(old_value.to_html().as_str()); + buffer.push_str(space1.to_html().as_str()); + buffer.push_str(new_value.to_html().as_str()); + buffer + } + FilterValue::Split { space0, sep } => { + let mut buffer = "split".to_string(); + buffer.push_str(space0.to_html().as_str()); + buffer.push_str(sep.to_html().as_str()); + buffer } FilterValue::ToInt => "toInt".to_string(), + FilterValue::UrlDecode => "urlDecode".to_string(), + FilterValue::UrlEncode => "urlEncode".to_string(), } } } diff --git a/packages/hurl_core/src/parser/filter.rs b/packages/hurl_core/src/parser/filter.rs index 047637551..2e875da4e 100644 --- a/packages/hurl_core/src/parser/filter.rs +++ b/packages/hurl_core/src/parser/filter.rs @@ -17,8 +17,9 @@ */ use crate::ast::{Filter, FilterValue, SourceInfo, Whitespace}; use crate::parser::combinators::choice; -use crate::parser::primitives::{one_or_more_spaces, try_literal, zero_or_more_spaces}; +use crate::parser::primitives::{natural, one_or_more_spaces, try_literal, zero_or_more_spaces}; use crate::parser::query::regex_value; +use crate::parser::string::quoted_template; use crate::parser::{Error, ParseError, ParseResult, Reader}; pub fn filters(reader: &mut Reader) -> ParseResult<'static, Vec<(Whitespace, Filter)>> { @@ -51,12 +52,15 @@ pub fn filter(reader: &mut Reader) -> ParseResult<'static, Filter> { let value = choice( &[ count_filter, - regex_filter, - url_encode_filter, - url_decode_filter, - html_encode_filter, html_decode_filter, + html_encode_filter, + nth_filter, + regex_filter, + replace_filter, + split_filter, to_int_filter, + url_decode_filter, + url_encode_filter, ], reader, ) @@ -83,23 +87,6 @@ fn count_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { Ok(FilterValue::Count) } -fn regex_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { - try_literal("regex", reader)?; - let space0 = one_or_more_spaces(reader)?; - let value = regex_value(reader)?; - Ok(FilterValue::Regex { space0, value }) -} - -fn url_encode_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { - try_literal("urlEncode", reader)?; - Ok(FilterValue::UrlEncode) -} - -fn url_decode_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { - try_literal("urlDecode", reader)?; - Ok(FilterValue::UrlDecode) -} - fn html_encode_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { try_literal("htmlEscape", reader)?; Ok(FilterValue::HtmlEscape) @@ -110,11 +97,56 @@ fn html_decode_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> Ok(FilterValue::HtmlUnescape) } +fn nth_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { + try_literal("nth", reader)?; + let space0 = one_or_more_spaces(reader)?; + let n = natural(reader)?; + Ok(FilterValue::Nth { space0, n }) +} + +fn regex_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { + try_literal("regex", reader)?; + let space0 = one_or_more_spaces(reader)?; + let value = regex_value(reader)?; + Ok(FilterValue::Regex { space0, value }) +} + +fn replace_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { + try_literal("replace", reader)?; + let space0 = one_or_more_spaces(reader)?; + let old_value = regex_value(reader)?; + let space1 = one_or_more_spaces(reader)?; + let new_value = quoted_template(reader).map_err(|e| e.non_recoverable())?; + Ok(FilterValue::Replace { + space0, + old_value, + space1, + new_value, + }) +} + +fn split_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { + try_literal("split", reader)?; + let space0 = one_or_more_spaces(reader)?; + let sep = quoted_template(reader).map_err(|e| e.non_recoverable())?; + Ok(FilterValue::Split { space0, sep }) +} + fn to_int_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { try_literal("toInt", reader)?; Ok(FilterValue::ToInt) } +fn url_encode_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { + try_literal("urlEncode", reader)?; + Ok(FilterValue::UrlEncode) +} + +fn url_decode_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { + try_literal("urlDecode", reader)?; + Ok(FilterValue::UrlDecode) +} + #[cfg(test)] mod tests { use crate::ast::Pos; diff --git a/packages/hurlfmt/src/format/json.rs b/packages/hurlfmt/src/format/json.rs index 0370c88bb..b38a80553 100644 --- a/packages/hurlfmt/src/format/json.rs +++ b/packages/hurlfmt/src/format/json.rs @@ -16,9 +16,10 @@ * */ -use super::serialize_json::*; use hurl_core::ast::*; +use super::serialize_json::*; + pub fn format(hurl_file: HurlFile) -> String { hurl_file.to_json().format() } @@ -526,18 +527,12 @@ impl ToJson for FilterValue { fn to_json(&self) -> JValue { let mut attributes = vec![]; match self { - FilterValue::Regex { value, .. } => { - attributes.push(("type".to_string(), JValue::String("regex".to_string()))); - attributes.push(("expr".to_string(), value.to_json())); - } FilterValue::Count => { attributes.push(("type".to_string(), JValue::String("count".to_string()))); } - FilterValue::UrlEncode => { - attributes.push(("type".to_string(), JValue::String("urlEncode".to_string()))); - } - FilterValue::UrlDecode => { - attributes.push(("type".to_string(), JValue::String("urlDecode".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()))); } FilterValue::HtmlEscape => { attributes.push(("type".to_string(), JValue::String("htmlEscape".to_string()))); @@ -548,6 +543,32 @@ impl ToJson for FilterValue { JValue::String("htmlUnescape".to_string()), )); } + FilterValue::Regex { value, .. } => { + attributes.push(("type".to_string(), JValue::String("regex".to_string()))); + attributes.push(("expr".to_string(), value.to_json())); + } + FilterValue::Replace { + old_value, + new_value, + .. + } => { + attributes.push(("type".to_string(), JValue::String("replace".to_string()))); + attributes.push(("old_value".to_string(), old_value.to_json())); + attributes.push(( + "new_value".to_string(), + JValue::String(new_value.to_string()), + )); + } + FilterValue::UrlEncode => { + attributes.push(("type".to_string(), JValue::String("urlEncode".to_string()))); + } + FilterValue::UrlDecode => { + attributes.push(("type".to_string(), JValue::String("urlDecode".to_string()))); + } + FilterValue::Split { sep, .. } => { + attributes.push(("type".to_string(), JValue::String("split".to_string()))); + attributes.push(("sep".to_string(), JValue::String(sep.to_string()))); + } FilterValue::ToInt => { attributes.push(("type".to_string(), JValue::String("toInt".to_string()))); } diff --git a/packages/hurlfmt/src/format/token.rs b/packages/hurlfmt/src/format/token.rs index fdda3620e..c5977e6b3 100644 --- a/packages/hurlfmt/src/format/token.rs +++ b/packages/hurlfmt/src/format/token.rs @@ -1143,19 +1143,43 @@ impl Tokenizable for VeryVerboseOption { impl Tokenizable for Filter { fn tokenize(&self) -> Vec { match self.value.clone() { + FilterValue::Count => vec![Token::FilterType(String::from("count"))], + FilterValue::HtmlEscape => vec![Token::FilterType(String::from("htmlEscape"))], + FilterValue::HtmlUnescape => { + vec![Token::FilterType(String::from("htmlUnescape"))] + } + FilterValue::Nth { space0, n } => { + let mut tokens: Vec = vec![Token::FilterType(String::from("nth"))]; + tokens.append(&mut space0.tokenize()); + tokens.push(Token::Number(n.to_string())); + tokens + } FilterValue::Regex { space0, value } => { - let mut tokens: Vec = vec![]; - tokens.push(Token::FilterType(String::from("regex"))); + let mut tokens: Vec = vec![Token::FilterType(String::from("regex"))]; tokens.append(&mut space0.tokenize()); tokens.append(&mut value.tokenize()); tokens } - FilterValue::Count => vec![Token::FilterType(String::from("count"))], + FilterValue::Replace { + space0, + old_value, + space1, + new_value, + } => { + let mut tokens: Vec = vec![Token::FilterType(String::from("replace"))]; + tokens.append(&mut space0.tokenize()); + tokens.append(&mut old_value.tokenize()); + tokens.append(&mut space1.tokenize()); + tokens.append(&mut new_value.tokenize()); + tokens + } FilterValue::UrlEncode => vec![Token::FilterType(String::from("urlEncode"))], FilterValue::UrlDecode => vec![Token::FilterType(String::from("urlDecode"))], - FilterValue::HtmlEscape => vec![Token::FilterType(String::from("htmlEscape"))], - FilterValue::HtmlUnescape => { - vec![Token::FilterType(String::from("htmlUnescape"))] + FilterValue::Split { space0, sep } => { + let mut tokens: Vec = vec![Token::FilterType(String::from("split"))]; + tokens.append(&mut space0.tokenize()); + tokens.append(&mut sep.tokenize()); + tokens } FilterValue::ToInt => vec![Token::FilterType(String::from("toInt"))], }