Add Regex predicate value

This commit is contained in:
Fabrice Reix 2022-01-26 22:10:48 +01:00 committed by Fabrice Reix
parent 45b15e8b7f
commit b83bbf67ed
23 changed files with 259 additions and 30 deletions

View File

@ -2,7 +2,9 @@
</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">"$.date1"</span> <span class="predicate-type">matches</span> <span class="string">"\\d{4}-\\d{2}-\\d{2}"</span></span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.date2"</span> <span class="predicate-type">matches</span> <span class="string">"\\d{4}-\\d{2}-\\d{2}"</span></span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.date1"</span> <span class="predicate-type">matches</span> <span class="string">"^\\d{4}-\\d{2}-\\d{2}$"</span></span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.date2"</span> <span class="not">not</span> <span class="predicate-type">matches</span> <span class="string">"^\\d{4}-\\d{2}-\\d{2}$"</span></span>
</span></span></code></pre>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.date1"</span> <span class="predicate-type">matches</span> <span class="regex">/\d{4}-\d{2}-\d{2}/</span></span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.date2"</span> <span class="predicate-type">matches</span> <span class="regex">/\d{4}-\d{2}-\d{2}/</span></span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.date1"</span> <span class="predicate-type">matches</span> <span class="regex">/^\d{4}-\d{2}-\d{2}$/</span></span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.date2"</span> <span class="not">not</span> <span class="predicate-type">matches</span> <span class="regex">/^\d{4}-\d{2}-\d{2}$/</span></span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.path1"</span> <span class="predicate-type">matches</span> <span class="regex">/aa\/bb/</span></span>
<span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.path2"</span> <span class="predicate-type">matches</span> <span class="regex">/aa\\bb/</span></span></span></span></code></pre>

View File

@ -2,6 +2,9 @@ GET http://localhost:8000/assert-match
HTTP/1.0 200
[Asserts]
jsonpath "$.date1" matches "\\d{4}-\\d{2}-\\d{2}"
jsonpath "$.date2" matches "\\d{4}-\\d{2}-\\d{2}"
jsonpath "$.date1" matches "^\\d{4}-\\d{2}-\\d{2}$"
jsonpath "$.date2" not matches "^\\d{4}-\\d{2}-\\d{2}$"
jsonpath "$.date1" matches /\d{4}-\d{2}-\d{2}/
jsonpath "$.date2" matches /\d{4}-\d{2}-\d{2}/
jsonpath "$.date1" matches /^\d{4}-\d{2}-\d{2}$/
jsonpath "$.date2" not matches /^\d{4}-\d{2}-\d{2}$/
jsonpath "$.path1" matches /aa\/bb/
jsonpath "$.path2" matches /aa\\bb/

View File

@ -1 +1 @@
{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/assert-match"},"response":{"version":"HTTP/1.0","status":200,"asserts":[{"query":{"type":"jsonpath","expr":"$.date1"},"predicate":{"type":"match","value":"\\d{4}-\\d{2}-\\d{2}"}},{"query":{"type":"jsonpath","expr":"$.date2"},"predicate":{"type":"match","value":"\\d{4}-\\d{2}-\\d{2}"}},{"query":{"type":"jsonpath","expr":"$.date1"},"predicate":{"type":"match","value":"^\\d{4}-\\d{2}-\\d{2}$"}},{"query":{"type":"jsonpath","expr":"$.date2"},"predicate":{"not":true,"type":"match","value":"^\\d{4}-\\d{2}-\\d{2}$"}}]}}]}
{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/assert-match"},"response":{"version":"HTTP/1.0","status":200,"asserts":[{"query":{"type":"jsonpath","expr":"$.date1"},"predicate":{"type":"match","value":"\\d{4}-\\d{2}-\\d{2}"}},{"query":{"type":"jsonpath","expr":"$.date1"},"predicate":{"type":"match","value":"\\d{4}-\\d{2}-\\d{2}","encoding":"regex"}},{"query":{"type":"jsonpath","expr":"$.date2"},"predicate":{"type":"match","value":"\\d{4}-\\d{2}-\\d{2}","encoding":"regex"}},{"query":{"type":"jsonpath","expr":"$.date1"},"predicate":{"type":"match","value":"^\\d{4}-\\d{2}-\\d{2}$","encoding":"regex"}},{"query":{"type":"jsonpath","expr":"$.date2"},"predicate":{"not":true,"type":"match","value":"^\\d{4}-\\d{2}-\\d{2}$","encoding":"regex"}},{"query":{"type":"jsonpath","expr":"$.path1"},"predicate":{"type":"match","value":"aa/bb","encoding":"regex"}},{"query":{"type":"jsonpath","expr":"$.path2"},"predicate":{"type":"match","value":"aa\\\\bb","encoding":"regex"}}]}}]}

View File

@ -1,4 +1,6 @@
{
"date1": "2014-01-01",
"date2": "x2014-01-01"
"date2": "x2014-01-01",
"path1": "aa/bb",
"path2": "aa\\bb"
}

View File

@ -5,7 +5,9 @@ from flask import Response
def assert_match():
return Response('''{
"date1": "2014-01-01",
"date2": "x2014-01-01"
"date2": "x2014-01-01",
"path1": "aa/bb",
"path2": "aa\\\\bb"
}''', mimetype='application/json')

View File

@ -0,0 +1,7 @@
error: Parsing regex
--> tests_error_parser/invalid_regex.hurl:4:23
|
4 | jsonpath "$.data" == /aa{a}/
| ^ Invalid Regex expression: repetition quantifier expects a valid decimal
|

View File

@ -0,0 +1 @@
2

View File

@ -0,0 +1,4 @@
GET http://localhost:8000/unused
HTTP/1.1 200
[Asserts]
jsonpath "$.data" == /aa{a}/

View File

@ -51,6 +51,7 @@ impl Value {
serde_json::Value::String(encoded)
}
Value::Null => serde_json::Value::Null,
Value::Regex(value) => serde_json::Value::String(value.to_string()),
Value::Unit => todo!("how to serialize that in json?"),
}
}

View File

@ -17,7 +17,7 @@
*/
use std::collections::HashMap;
use regex::Regex;
use regex;
use hurl_core::ast::*;
@ -109,6 +109,7 @@ impl Value {
Value::Bytes(value) => format!("byte array <{}>", hex::encode(value)),
Value::Null => "null".to_string(),
Value::Unit => "unit".to_string(),
Value::Regex(value) => format!("regex <{}>", value.as_str()),
}
}
}
@ -145,6 +146,7 @@ impl Value {
Value::Object(values) => format!("list of size {}", values.len()),
Value::String(value) => format!("string <{}>", value),
Value::Unit => "something".to_string(),
Value::Regex(value) => format!("regex <{}>", value),
}
}
}
@ -409,33 +411,34 @@ fn eval_something(
PredicateFuncValue::Match {
value: expected, ..
} => {
let template = if let PredicateValue::String(template) = expected {
template
} else {
panic!("expect a string predicate value")
};
let expected = eval_template(&template, variables)?;
let regex = match Regex::new(expected.as_str()) {
Ok(re) => re,
Err(_) => {
return Err(Error {
source_info: predicate_func.source_info.clone(),
inner: RunnerError::InvalidRegex(),
assert: false,
});
let regex = match expected {
PredicateValue::String(template) => {
let expected = eval_template(&template, variables)?;
match regex::Regex::new(expected.as_str()) {
Ok(re) => re,
Err(_) => {
return Err(Error {
source_info: predicate_func.source_info.clone(),
inner: RunnerError::InvalidRegex(),
assert: false,
});
}
}
}
PredicateValue::Regex(regex) => regex.inner,
_ => panic!("expect a string predicate value"), // should have failed in parsing
};
match value.clone() {
Value::String(actual) => Ok(AssertResult {
success: regex.is_match(actual.as_str()),
actual: value.display(),
expected: format!("matches regex <{}>", expected),
expected: format!("matches regex <{}>", regex),
type_mismatch: false,
}),
_ => Ok(AssertResult {
success: false,
actual: value.display(),
expected: format!("matches regex <{}>", expected),
expected: format!("matches regex <{}>", regex),
type_mismatch: true,
}),
}
@ -1556,4 +1559,32 @@ mod tests {
let variables = HashMap::new();
assert!(eval_predicate(predicate, &variables, None).is_ok());
}
#[test]
fn test_predicate_match() {
let variables = HashMap::new();
let whitespace = Whitespace {
value: String::from(" "),
source_info: SourceInfo::init(0, 0, 0, 0),
};
// // a float can be equals to an int (but the reverse)
let assert_result = eval_something(
PredicateFunc {
value: PredicateFuncValue::Match {
space0: whitespace,
value: PredicateValue::Regex(Regex {
inner: regex::Regex::new(r#"a{3}"#).unwrap(),
}),
},
source_info: SourceInfo::init(0, 0, 0, 0),
},
&variables,
Value::String("aa".to_string()),
)
.unwrap();
assert!(!assert_result.success);
assert!(!assert_result.type_mismatch);
assert_eq!(assert_result.actual.as_str(), "string <aa>");
assert_eq!(assert_result.expected.as_str(), "matches regex <a{3}>");
}
}

View File

@ -47,5 +47,6 @@ pub fn eval_predicate_value(
let value = eval_expr(expr, variables)?;
Ok(value)
}
PredicateValue::Regex(regex) => Ok(Value::Regex(regex.inner)),
}
}

View File

@ -33,6 +33,7 @@ pub enum Value {
Object(Vec<(String, Value)>),
String(String),
Unit,
Regex(regex::Regex),
}
// You must implement it yourself because of the Float
@ -53,6 +54,7 @@ impl PartialEq for Value {
}
}
}
impl Eq for Value {}
impl fmt::Display for Value {
@ -71,6 +73,10 @@ impl fmt::Display for Value {
Value::Bytes(v) => format!("hex, {};", hex::encode(v)),
Value::Null => "null".to_string(),
Value::Unit => "Unit".to_string(),
Value::Regex(x) => {
let s = str::replace(x.as_str(), "/", "\\/");
format!("/{}/", s)
}
};
write!(f, "{}", value)
}
@ -97,6 +103,7 @@ impl Value {
Value::Bytes(_) => "bytes".to_string(),
Value::Null => "null".to_string(),
Value::Unit => "unit".to_string(),
Value::Regex(_) => "regex".to_string(),
}
}

View File

@ -422,6 +422,7 @@ pub enum PredicateValue {
Hex(Hex),
Base64(Base64),
Expression(Expr),
Regex(Regex),
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -600,6 +601,19 @@ pub struct Hex {
pub space1: Whitespace,
}
// Literal Regex
#[derive(Clone, Debug)]
pub struct Regex {
pub inner: regex::Regex,
}
impl PartialEq for Regex {
fn eq(&self, other: &Self) -> bool {
self.inner.to_string() == other.inner.to_string()
}
}
impl Eq for Regex {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Pos {
pub line: usize,

View File

@ -137,6 +137,12 @@ impl fmt::Display for Hex {
}
}
impl fmt::Display for Regex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.inner)
}
}
impl PredicateFuncValue {
pub fn name(&self) -> String {
match self {

View File

@ -93,7 +93,7 @@ impl Error for parser::Error {
ParseError::Json { .. } => "json error".to_string(),
ParseError::Predicate { .. } => "expecting a predicate".to_string(),
ParseError::PredicateValue { .. } => "invalid predicate value".to_string(),
ParseError::RegexExpr { .. } => "Invalid Regex expression".to_string(),
ParseError::RegexExpr { message } => format!("Invalid Regex expression: {}", message),
ParseError::DuplicateSection { .. } => "The section is already defined".to_string(),
ParseError::RequestSection { .. } => {
"This is not a valid section for a request".to_string()

View File

@ -636,6 +636,7 @@ impl Htmlable for PredicateValue {
PredicateValue::Base64(value) => value.to_html(),
PredicateValue::Expression(value) => value.to_html(),
PredicateValue::Null {} => "<span class=\"null\">null</span>".to_string(),
PredicateValue::Regex(value) => value.to_html(),
}
}
}
@ -797,6 +798,12 @@ impl Htmlable for Hex {
}
}
impl Htmlable for Regex {
fn to_html(&self) -> String {
let s = str::replace(self.inner.as_str(), "/", "\\/");
format!("<span class=\"regex\">/{}/</span>", s)
}
}
impl Htmlable for EncodedString {
fn to_html(&self) -> String {
format!("<span class=\"string\">{}</span>", self.encoded)

View File

@ -43,7 +43,7 @@ pub enum ParseError {
Xml {},
Predicate,
PredicateValue,
RegexExpr,
RegexExpr { message: String },
Unexpected { character: String },
Eof {},

View File

@ -311,7 +311,7 @@ fn match_predicate(reader: &mut Reader) -> ParseResult<'static, PredicateFuncVal
let space0 = one_or_more_spaces(reader)?;
let save = reader.state.clone();
let value = predicate_value(reader)?;
if !value.is_string() {
if !matches!(value, PredicateValue::String(_)) && !matches!(value, PredicateValue::Regex(_)) {
return Err(Error {
pos: save.pos,
recoverable: false,

View File

@ -65,6 +65,10 @@ pub fn predicate_value(reader: &mut Reader) -> ParseResult<'static, PredicateVal
Ok(value) => Ok(PredicateValue::Raw(value)),
Err(e) => Err(e),
},
|p1| match regex(p1) {
Ok(value) => Ok(PredicateValue::Regex(value)),
Err(e) => Err(e),
},
],
reader,
)

View File

@ -332,6 +332,71 @@ pub fn hex(reader: &mut Reader) -> ParseResult<'static, Hex> {
})
}
pub fn regex(reader: &mut Reader) -> ParseResult<'static, Regex> {
try_literal("/", reader)?;
let start = reader.state.pos.clone();
let mut s = String::from("");
// Hurl escaping /
// in order to avoid terminating the regex
// eg. \a\b/
//
// Other escaped sequences such as \* are part of the regex expression
// They are not part of the syntax of Hurl itself.
loop {
match reader.read() {
None => {
return Err(Error {
pos: reader.state.pos.clone(),
recoverable: false,
inner: ParseError::Eof {},
})
}
Some('/') => break,
Some('\\') => {
if let Some('/') = reader.peek() {
reader.read();
s.push('/');
} else {
s.push('\\');
}
}
Some(c) => s.push(c),
}
}
match regex::Regex::new(s.as_str()) {
Ok(inner) => Ok(Regex { inner }),
Err(e) => {
let message = match e {
regex::Error::Syntax(s) => {
// The regex syntax error from the crate returns a multiline String
// For example
// regex parse error:
// x{a}
// ^
// error: repetition quantifier expects a valid decimal
//
// To fit nicely in Hurl Error reporting, you need an error message string that does not spread on multiple lines
// You will assume that the error most relevant description is on the last line
let lines = s.split('\n').clone().collect::<Vec<&str>>();
let last_line = lines.last().expect("at least one line");
last_line
.strip_prefix("error: ")
.unwrap_or(last_line)
.to_string()
}
regex::Error::CompiledTooBig(_) => "Size limit exceeded".to_string(),
_ => "unknown".to_string(),
};
Err(Error {
pos: start,
recoverable: false,
inner: ParseError::RegexExpr { message },
})
}
}
}
pub fn null(reader: &mut Reader) -> ParseResult<'static, ()> {
try_literal("null", reader)
}
@ -1332,6 +1397,66 @@ mod tests {
assert_eq!(error.inner, ParseError::OddNumberOfHexDigits {});
}
#[test]
fn test_regex() {
let mut reader = Reader::init(r#"/a{3}/"#);
assert_eq!(
regex(&mut reader).unwrap(),
Regex {
inner: regex::Regex::new(r#"a{3}"#).unwrap()
}
);
let mut reader = Reader::init(r#"/a\/b/"#);
assert_eq!(
regex(&mut reader).unwrap(),
Regex {
inner: regex::Regex::new(r#"a/b"#).unwrap()
}
);
let mut reader = Reader::init(r#"/a\.b/"#);
assert_eq!(
regex(&mut reader).unwrap(),
Regex {
inner: regex::Regex::new(r#"a\.b"#).unwrap()
}
);
let mut reader = Reader::init(r#"/\d{4}-\d{2}-\d{2}/"#);
assert_eq!(
regex(&mut reader).unwrap(),
Regex {
inner: regex::Regex::new(r#"\d{4}-\d{2}-\d{2}"#).unwrap()
}
);
}
#[test]
fn test_regex_error() {
let mut reader = Reader::init("xxx");
let error = regex(&mut reader).err().unwrap();
assert_eq!(error.pos, Pos { line: 1, column: 1 });
assert!(error.recoverable);
let mut reader = Reader::init("/xxx");
let error = regex(&mut reader).err().unwrap();
assert_eq!(error.pos, Pos { line: 1, column: 5 });
assert!(!error.recoverable);
assert_eq!(error.inner, ParseError::Eof {});
let mut reader = Reader::init("/x{a}/");
let error = regex(&mut reader).err().unwrap();
assert_eq!(error.pos, Pos { line: 1, column: 2 });
assert!(!error.recoverable);
assert_eq!(
error.inner,
ParseError::RegexExpr {
message: "repetition quantifier expects a valid decimal".to_string()
}
);
}
#[test]
fn test_file() {
let mut reader = Reader::init("file,data.xml;");

View File

@ -465,6 +465,9 @@ fn json_predicate_value(predicate_value: PredicateValue) -> (JValue, Option<Stri
Some("base64".to_string()),
),
PredicateValue::Expression(value) => (JValue::String(value.to_string()), None),
PredicateValue::Regex(value) => {
(JValue::String(value.to_string()), Some("regex".to_string()))
}
}
}

View File

@ -653,6 +653,7 @@ impl Tokenizable for PredicateValue {
PredicateValue::Hex(value) => vec![Token::String(value.to_string())],
PredicateValue::Base64(value) => value.tokenize(),
PredicateValue::Expression(value) => value.tokenize(),
PredicateValue::Regex(value) => value.tokenize(),
}
}
}
@ -735,6 +736,13 @@ impl Tokenizable for Expr {
}
}
impl Tokenizable for Regex {
fn tokenize(&self) -> Vec<Token> {
let s = str::replace(self.inner.as_str(), "/", "\\/");
vec![Token::String(format!("/{}/", s))]
}
}
impl Tokenizable for LineTerminator {
fn tokenize(&self) -> Vec<Token> {
let mut tokens: Vec<Token> = vec![];

View File

@ -508,6 +508,7 @@ impl Lintable<PredicateValue> for PredicateValue {
PredicateValue::Hex(value) => PredicateValue::Hex(value.lint()),
PredicateValue::Base64(value) => PredicateValue::Base64(value.lint()),
PredicateValue::Expression(value) => PredicateValue::Expression(value.clone()),
PredicateValue::Regex(value) => PredicateValue::Regex(value.clone()),
}
}
}