Add filters urlEncode/urlDecode

This commit is contained in:
Fabrice Reix 2022-10-28 07:58:18 +02:00
parent 0e6765b9e9
commit 67d48bde22
No known key found for this signature in database
GPG Key ID: BF5213154B2E7155
16 changed files with 158 additions and 61 deletions

View File

@ -409,16 +409,16 @@ variable-name: [A-Za-z] [A-Za-z_-0-9]*
filter:
regex-filter
| count-filter
| escapeurl-filter
| unescapeurl-filter
| url-encode-filter
| url-decode-filter
regex-filter: "regex" sp (quoted-string | regex)
count-filter: "count"
escapeurl-filter: "escapeUrl"
url-encode-filter: "urlEncode"
unescapeurl-filter: "unescapeUrl"
url-decode-filter: "urlDecode"
# Lexical Grammar

View File

@ -0,0 +1,19 @@
<pre><code class="language-hurl"><span class="hurl-entry"><span class="request"><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/filter</span></span>
</span><span class="response"><span class="line"></span>
<span class="line"><span class="version">HTTP/1.0</span> <span class="number">200</span></span>
<span class="line section-header">[Captures]</span>
<span class="line"><span class="name">url</span><span>:</span> <span class="query-type">jsonpath</span> <span class="string">"$.url"</span></span>
<span class="line section-header">[Asserts]</span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.list"</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">"$.message"</span> <span class="filter-type">regex</span> <span class="regex">/Hello (.*)!/</span> <span class="predicate-type">==</span> <span class="string">"Bob"</span></span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.url"</span> <span class="predicate-type">==</span> <span class="string">"https://mozilla.org/?x=шеллы"</span></span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.url"</span> <span class="filter-type">urlEncode</span> <span class="predicate-type">==</span> <span class="string">"https%3A//mozilla.org/%3Fx%3D%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"</span></span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.encoded_url"</span> <span class="filter-type">urlDecode</span> <span class="predicate-type">==</span> <span class="string">"https://mozilla.org/?x=шеллы"</span></span>
<span class="line"><span class="query-type">variable</span> <span class="string">"url"</span> <span class="filter-type">urlEncode</span> <span class="filter-type">urlDecode</span> <span class="predicate-type">==</span> <span class="string">"{{url}}"</span></span>
<span class="json"><span class="line">{</span>
<span class="line"> "list": [1,2,3],</span>
<span class="line"> "message": "Hello Bob!",</span>
<span class="line"> "url": "https://mozilla.org/?x=шеллы",</span>
<span class="line"> "encoded_url": "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"</span>
<span class="line">}</span></span>
</span></span></code></pre>

View File

@ -0,0 +1,18 @@
GET http://localhost:8000/filter
HTTP/1.0 200
[Captures]
url: jsonpath "$.url"
[Asserts]
jsonpath "$.list" count == 3
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"
jsonpath "$.encoded_url" urlDecode == "https://mozilla.org/?x=шеллы"
variable "url" urlEncode urlDecode == "{{url}}"
{
"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"
}

View File

@ -0,0 +1 @@
{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/filter"},"response":{"version":"HTTP/1.0","status":200,"captures":[{"name":"url","query":{"type":"jsonpath","expr":"$.url"}}],"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}}"}}],"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"}}}}]}

View File

@ -0,0 +1,11 @@
from app import app
@app.route("/filter")
def filter():
return """{
"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"
}"""

View File

@ -1,8 +0,0 @@
<pre><code class="language-hurl"><span class="hurl-entry"><span class="request"><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/filter-count</span></span>
</span><span class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="number">200</span></span>
<span class="line section-header">[Asserts]</span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.users"</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">"$.users"</span> <span class="filter-type">count</span> <span class="predicate-type">&gt;</span> <span class="number">1</span></span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.users"</span> <span class="filter-type">count</span> <span class="predicate-type">&lt;=</span> <span class="number">10</span></span>
</span></span><span class="line"></span>
</code></pre>

View File

@ -1,7 +0,0 @@
GET http://localhost:8000/filter-count
HTTP/1.0 200
[Asserts]
jsonpath "$.users" count == 3
jsonpath "$.users" count > 1
jsonpath "$.users" count <= 10

View File

@ -1 +0,0 @@
{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/filter-count"},"response":{"version":"HTTP/1.0","status":200,"asserts":[{"query":{"type":"jsonpath","expr":"$.users"},"filters":[{"type":"count"}],"predicate":{"type":"equal","value":3}},{"query":{"type":"jsonpath","expr":"$.users"},"filters":[{"type":"count"}],"predicate":{"type":"greater","value":1}},{"query":{"type":"jsonpath","expr":"$.users"},"filters":[{"type":"count"}],"predicate":{"type":"less-or-equal","value":10}}]}}]}

View File

@ -1,12 +0,0 @@
from app import app
from flask import Response
@app.route("/filter-count")
def subquery_count():
return Response(
"""{
"users": ["Bob", "Bill", "Bruce"]
}""",
mimetype="application/json",
)

View File

@ -18,6 +18,7 @@
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;
@ -43,8 +44,8 @@ fn eval_filter(
value: regex_value, ..
} => eval_regex(value, regex_value, variables, &filter.source_info),
FilterValue::Count {} => eval_count(value, &filter.source_info),
FilterValue::EscapeUrl { .. } => todo!(),
FilterValue::UnEscapeUrl { .. } => todo!(),
FilterValue::UrlEncode { .. } => eval_url_encode(value, &filter.source_info),
FilterValue::UrlDecode { .. } => eval_url_decode(value, &filter.source_info),
}
}
@ -108,6 +109,48 @@ fn eval_count(value: &Value, source_info: &SourceInfo) -> Result<Value, Error> {
}
}
// does not encopde "/"
// like Jinja template (https://jinja.palletsprojects.com/en/3.1.x/templates/#jinja-filters.urlencode)
fn eval_url_encode(value: &Value, source_info: &SourceInfo) -> Result<Value, Error> {
match value {
Value::String(value) => {
const FRAGMENT: &AsciiSet = &percent_encoding::NON_ALPHANUMERIC
.remove(b'-')
.remove(b'.')
.remove(b'_')
.remove(b'~')
.remove(b'/');
let encoded = percent_encoding::percent_encode(value.as_bytes(), FRAGMENT).to_string();
Ok(Value::String(encoded))
}
v => Err(Error {
source_info: source_info.clone(),
inner: RunnerError::FilterInvalidInput(v._type()),
assert: false,
}),
}
}
fn eval_url_decode(value: &Value, source_info: &SourceInfo) -> Result<Value, Error> {
match value {
Value::String(value) => {
match percent_encoding::percent_decode(value.as_bytes()).decode_utf8() {
Ok(decoded) => Ok(Value::String(decoded.to_string())),
Err(_) => Err(Error {
source_info: source_info.clone(),
inner: RunnerError::FilterInvalidInput("Invalid UTF8 stream".to_string()),
assert: false,
}),
}
}
v => Err(Error {
source_info: source_info.clone(),
inner: RunnerError::FilterInvalidInput(v._type()),
assert: false,
}),
}
}
#[cfg(test)]
pub mod tests {
use super::*;
@ -240,4 +283,42 @@ pub mod tests {
assert_eq!(error.source_info, SourceInfo::new(1, 7, 1, 20));
assert_eq!(error.inner, RunnerError::InvalidRegex {});
}
#[test]
pub fn eval_filter_url_encode() {
let variables = HashMap::new();
let filter = Filter {
source_info: SourceInfo::new(0, 0, 0, 0),
value: FilterValue::UrlEncode {},
};
assert_eq!(
eval_filter(
&filter,
&Value::String("https://mozilla.org/?x=шеллы".to_string()),
&variables,
)
.unwrap(),
Value::String(
"https%3A//mozilla.org/%3Fx%3D%D1%88%D0%B5%D0%BB%D0%BB%D1%8B".to_string()
)
);
}
#[test]
pub fn eval_filter_url_decode() {
let variables = HashMap::new();
let filter = Filter {
source_info: SourceInfo::new(0, 0, 0, 0),
value: FilterValue::UrlDecode {},
};
assert_eq!(
eval_filter(
&filter,
&Value::String("https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B".to_string()),
&variables,
)
.unwrap(),
Value::String("https://mozilla.org/?x=шеллы".to_string())
);
}
}

View File

@ -810,6 +810,6 @@ pub enum FilterValue {
space0: Whitespace,
value: RegexValue,
},
EscapeUrl {},
UnEscapeUrl {},
UrlEncode {},
UrlDecode {},
}

View File

@ -1038,10 +1038,8 @@ impl Htmlable for FilterValue {
buffer.push_str(value.to_html().as_str());
buffer
}
FilterValue::EscapeUrl {} => "<span class=\"filter-type\">escapeUrl</span>".to_string(),
FilterValue::UnEscapeUrl {} => {
"<span class=\"filter-type\">unescapeUrl</span>".to_string()
}
FilterValue::UrlEncode {} => "<span class=\"filter-type\">urlEncode</span>".to_string(),
FilterValue::UrlDecode {} => "<span class=\"filter-type\">urlDecode</span>".to_string(),
}
}
}

View File

@ -52,8 +52,8 @@ pub fn filter(reader: &mut Reader) -> ParseResult<'static, Filter> {
vec![
count_filter,
regex_filter,
escape_url_filter,
unescape_url_filter,
url_encode_filter,
url_decode_filter,
],
reader,
)
@ -87,14 +87,14 @@ fn regex_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> {
Ok(FilterValue::Regex { space0, value })
}
fn escape_url_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> {
try_literal("escapeUrl", reader)?;
Ok(FilterValue::EscapeUrl {})
fn url_encode_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> {
try_literal("urlEncode", reader)?;
Ok(FilterValue::UrlEncode {})
}
fn unescape_url_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> {
try_literal("unescapeUrl", reader)?;
Ok(FilterValue::UnEscapeUrl {})
fn url_decode_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> {
try_literal("urlDecode", reader)?;
Ok(FilterValue::UrlDecode {})
}
#[cfg(test)]

View File

@ -353,7 +353,7 @@ mod tests {
#[test]
fn test_query_with_filters() {
let mut reader = Reader::init("body unescapeUrl ");
let mut reader = Reader::init("body urlDecode ");
assert_eq!(
query(&mut reader).unwrap(),
Query {
@ -369,11 +369,11 @@ mod tests {
source_info: SourceInfo::new(1, 5, 1, 6)
},
Filter {
source_info: SourceInfo::new(1, 6, 1, 17),
value: FilterValue::UnEscapeUrl {},
source_info: SourceInfo::new(1, 6, 1, 15),
value: FilterValue::UrlDecode {},
}
)]
);
assert_eq!(reader.state.cursor, 16);
assert_eq!(reader.state.cursor, 14);
}
}

View File

@ -517,14 +517,11 @@ impl ToJson for FilterValue {
FilterValue::Count { .. } => {
attributes.push(("type".to_string(), JValue::String("count".to_string())));
}
FilterValue::EscapeUrl { .. } => {
attributes.push(("type".to_string(), JValue::String("escapeUrl".to_string())));
FilterValue::UrlEncode { .. } => {
attributes.push(("type".to_string(), JValue::String("urlEncode".to_string())));
}
FilterValue::UnEscapeUrl { .. } => {
attributes.push((
"type".to_string(),
JValue::String("unescapeUrl".to_string()),
));
FilterValue::UrlDecode { .. } => {
attributes.push(("type".to_string(), JValue::String("urlDecode".to_string())));
}
}
JValue::Object(attributes)

View File

@ -1086,8 +1086,8 @@ impl Tokenizable for Filter {
tokens
}
FilterValue::Count { .. } => vec![Token::FilterType(String::from("count"))],
FilterValue::EscapeUrl { .. } => vec![Token::FilterType(String::from("escapeUrl"))],
FilterValue::UnEscapeUrl { .. } => vec![Token::FilterType(String::from("unescapeUrl"))],
FilterValue::UrlEncode { .. } => vec![Token::FilterType(String::from("urlEncode"))],
FilterValue::UrlDecode { .. } => vec![Token::FilterType(String::from("urlDecode"))],
}
}
}