diff --git a/packages/hurl/src/runner/mod.rs b/packages/hurl/src/runner/mod.rs index ca91ca36bb..fdbfbb0378 100644 --- a/packages/hurl/src/runner/mod.rs +++ b/packages/hurl/src/runner/mod.rs @@ -28,7 +28,7 @@ pub use self::number::Number; pub use self::output::Output; pub use self::result::{AssertResult, CaptureResult, EntryResult, HurlResult}; pub use self::runner_options::{RunnerOptions, RunnerOptionsBuilder}; -pub use self::value::Value; +pub use self::value::{EvalError, Value}; pub use self::variable::{Variable, VariableSet, Visibility}; mod assert; diff --git a/packages/hurl/src/runner/predicate.rs b/packages/hurl/src/runner/predicate.rs index a14450f0f0..1eb1d9c0ee 100644 --- a/packages/hurl/src/runner/predicate.rs +++ b/packages/hurl/src/runner/predicate.rs @@ -22,8 +22,7 @@ use std::cmp::Ordering; use crate::runner::error::RunnerError; use crate::runner::predicate_value::{eval_predicate_value, eval_predicate_value_template}; use crate::runner::result::PredicateResult; -use crate::runner::template::eval_template; -use crate::runner::value::Value; +use crate::runner::value::{EvalError, Value}; use crate::runner::{Number, RunnerErrorKind, VariableSet}; use crate::util::path::ContextDir; @@ -260,7 +259,13 @@ fn eval_predicate_func( } => eval_include(expected, variables, value, context_dir), PredicateFuncValue::Match { value: expected, .. - } => eval_match(expected, predicate_func.source_info, variables, value), + } => eval_match( + expected, + predicate_func.source_info, + variables, + value, + context_dir, + ), PredicateFuncValue::IsInteger => eval_is_integer(value), PredicateFuncValue::IsFloat => eval_is_float(value), PredicateFuncValue::IsBoolean => eval_is_boolean(value), @@ -351,20 +356,14 @@ fn eval_start_with( let expected = eval_predicate_value(expected, variables, context_dir)?; let expected_display = format!("starts with {}", expected.repr()); let actual_display = actual.repr(); - match (expected, actual) { - (Value::String(expected), Value::String(actual)) => Ok(AssertResult { - success: actual.as_str().starts_with(expected.as_str()), + match actual.starts_with(&expected) { + Ok(success) => Ok(AssertResult { + success, actual: actual_display, expected: expected_display, type_mismatch: false, }), - (Value::Bytes(expected), Value::Bytes(actual)) => Ok(AssertResult { - success: actual.starts_with(&expected), - actual: actual_display, - expected: expected_display, - type_mismatch: false, - }), - _ => Ok(AssertResult { + Err(_) => Ok(AssertResult { success: false, actual: actual_display, expected: expected_display, @@ -384,20 +383,14 @@ fn eval_end_with( let expected = eval_predicate_value(expected, variables, context_dir)?; let expected_display = format!("ends with {}", expected.repr()); let actual_display = actual.repr(); - match (expected, actual) { - (Value::String(expected), Value::String(actual)) => Ok(AssertResult { - success: actual.as_str().ends_with(expected.as_str()), + match actual.ends_with(&expected) { + Ok(success) => Ok(AssertResult { + success, actual: actual_display, expected: expected_display, type_mismatch: false, }), - (Value::Bytes(expected), Value::Bytes(actual)) => Ok(AssertResult { - success: actual.ends_with(&expected), - actual: actual_display, - expected: expected_display, - type_mismatch: false, - }), - _ => Ok(AssertResult { + Err(_) => Ok(AssertResult { success: false, actual: actual_display, expected: expected_display, @@ -417,15 +410,9 @@ fn eval_contain( let expected = eval_predicate_value(expected, variables, context_dir)?; let expected_display = format!("contains {}", expected.repr()); let actual_display = actual.repr(); - match (expected, actual) { - (Value::String(expected), Value::String(actual)) => Ok(AssertResult { - success: actual.as_str().contains(expected.as_str()), - actual: actual_display, - expected: expected_display, - type_mismatch: false, - }), - (Value::Bytes(expected), Value::Bytes(actual)) => Ok(AssertResult { - success: contains(actual.as_slice(), expected.as_slice()), + match actual.contains(&expected) { + Ok(success) => Ok(AssertResult { + success, actual: actual_display, expected: expected_display, type_mismatch: false, @@ -457,47 +444,36 @@ fn eval_match( source_info: SourceInfo, variables: &VariableSet, actual: &Value, + context_dir: &ContextDir, ) -> Result { - 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(RunnerError::new( - source_info, - RunnerErrorKind::InvalidRegex, - false, - )) - } - } - } - PredicateValue::Regex(regex) => regex.inner.clone(), - _ => panic!("expect a string predicate value"), // should have failed in parsing - }; + let expected = eval_predicate_value(expected, variables, context_dir)?; let actual_display = actual.repr(); - let expected_display = format!("matches regex <{regex}>"); - match actual { - Value::String(value) => Ok(AssertResult { - success: regex.is_match(value.as_str()), + let expected_display = format!("matches regex <{expected}>"); + match actual.is_match(&expected) { + Ok(success) => Ok(AssertResult { + success, actual: actual_display, expected: expected_display, type_mismatch: false, }), - _ => Ok(AssertResult { + Err(EvalError::Type) => Ok(AssertResult { success: false, actual: actual_display, expected: expected_display, type_mismatch: true, }), + Err(EvalError::InvalidRegex) => Err(RunnerError::new( + source_info, + RunnerErrorKind::InvalidRegex, + false, + )), } } /// Evaluates if an `actual` value is an integer. fn eval_is_integer(actual: &Value) -> Result { Ok(AssertResult { - success: matches!(actual, Value::Number(Number::Integer(_))) - || matches!(actual, Value::Number(Number::BigInteger(_))), + success: actual.is_integer(), actual: actual.repr(), expected: "integer".to_string(), type_mismatch: false, @@ -507,7 +483,7 @@ fn eval_is_integer(actual: &Value) -> Result { /// Evaluates if an `actual` value is a float. fn eval_is_float(actual: &Value) -> Result { Ok(AssertResult { - success: matches!(actual, Value::Number(Number::Float(_))), + success: actual.is_float(), actual: actual.repr(), expected: "float".to_string(), type_mismatch: false, @@ -517,7 +493,7 @@ fn eval_is_float(actual: &Value) -> Result { /// Evaluates if an `actual` value is a boolean. fn eval_is_boolean(actual: &Value) -> Result { Ok(AssertResult { - success: matches!(actual, Value::Bool(_)), + success: actual.is_boolean(), actual: actual.repr(), expected: "boolean".to_string(), type_mismatch: false, @@ -527,7 +503,7 @@ fn eval_is_boolean(actual: &Value) -> Result { /// Evaluates if an `actual` value is a string. fn eval_is_string(actual: &Value) -> Result { Ok(AssertResult { - success: matches!(actual, Value::String(_)), + success: actual.is_string(), actual: actual.repr(), expected: "string".to_string(), type_mismatch: false, @@ -537,10 +513,7 @@ fn eval_is_string(actual: &Value) -> Result { /// Evaluates if an `actual` value is a collection. fn eval_is_collection(actual: &Value) -> Result { Ok(AssertResult { - success: matches!(actual, Value::Bytes(_)) - || matches!(actual, Value::List(_)) - || matches!(actual, Value::Nodeset(_)) - || matches!(actual, Value::Object(_)), + success: actual.is_collection(), actual: actual.repr(), expected: "collection".to_string(), type_mismatch: false, @@ -550,7 +523,7 @@ fn eval_is_collection(actual: &Value) -> Result { /// Evaluates if an `actual` value is a date. fn eval_is_date(actual: &Value) -> Result { Ok(AssertResult { - success: matches!(actual, Value::Date(_)), + success: actual.is_date(), actual: actual.repr(), expected: "date".to_string(), type_mismatch: false, @@ -562,10 +535,10 @@ fn eval_is_date(actual: &Value) -> Result { /// [`eval_is_date`] performs type check (is the input of [`Value::Date`]), whereas [`eval_is_iso_date`] /// checks if a string conforms to a certain date-time format. fn eval_is_iso_date(actual: &Value) -> Result { - match actual { - Value::String(actual) => Ok(AssertResult { - success: chrono::DateTime::parse_from_rfc3339(actual).is_ok(), - actual: actual.clone(), + match actual.is_iso_date() { + Ok(success) => Ok(AssertResult { + success, + actual: actual.to_string(), expected: "string with format YYYY-MM-DDTHH:mm:ss.sssZ".to_string(), type_mismatch: false, }), @@ -601,37 +574,16 @@ fn eval_exist(actual: &Value) -> Result { /// Evaluates if an `actual` is empty. fn eval_is_empty(actual: &Value) -> Result { let expected_display = "count equals to 0".to_string(); - match actual { - Value::List(values) => Ok(AssertResult { - success: values.is_empty(), - actual: format!("count equals to {}", values.len()), - expected: expected_display, - type_mismatch: false, - }), - Value::String(data) => Ok(AssertResult { - success: data.is_empty(), - actual: format!("count equals to {}", data.len()), - expected: expected_display, - type_mismatch: false, - }), - Value::Nodeset(count) => Ok(AssertResult { - success: *count == 0, - actual: format!("count equals to {count}"), - expected: expected_display, - type_mismatch: false, - }), - Value::Object(props) => Ok(AssertResult { - success: props.is_empty(), - actual: format!("count equals to {}", props.len()), - expected: expected_display, - type_mismatch: false, - }), - Value::Bytes(data) => Ok(AssertResult { - success: data.is_empty(), - actual: format!("count equals to {}", data.len()), - expected: expected_display, - type_mismatch: false, - }), + match actual.count() { + Ok(count) => { + let actual_display = format!("count equals to {count}"); + Ok(AssertResult { + success: count == 0, + actual: actual_display, + expected: expected_display, + type_mismatch: false, + }) + } _ => Ok(AssertResult { success: false, actual: actual.repr(), @@ -644,7 +596,7 @@ fn eval_is_empty(actual: &Value) -> Result { /// Evaluates if an `actual` value is a number. fn eval_is_number(actual: &Value) -> Result { Ok(AssertResult { - success: matches!(actual, Value::Number(_)), + success: actual.is_number(), actual: actual.repr(), expected: "number".to_string(), type_mismatch: false, @@ -755,39 +707,24 @@ fn assert_values_less_or_equal(actual_value: &Value, expected_value: &Value) -> } fn assert_include(value: &Value, element: &Value) -> AssertResult { + let actual = value.repr(); let expected = format!("includes {}", element.repr()); - match value { - Value::List(values) => { - let mut success = false; - for v in values { - let result = assert_values_equal(v, element); - if result.success { - success = true; - break; - } - } - AssertResult { - success, - actual: value.repr(), - expected, - type_mismatch: false, - } - } - _ => AssertResult { + match value.includes(element) { + Ok(success) => AssertResult { + success, + actual, + expected, + type_mismatch: false, + }, + Err(_) => AssertResult { success: false, - actual: value.repr(), + actual, expected, type_mismatch: true, }, } } -fn contains(haystack: &[u8], needle: &[u8]) -> bool { - haystack - .windows(needle.len()) - .any(|window| window == needle) -} - #[cfg(test)] mod tests { use super::{AssertResult, *}; @@ -804,14 +741,6 @@ mod tests { } } - #[test] - fn test_contains() { - let haystack = [1, 2, 3]; - assert!(contains(&haystack, &[1])); - assert!(contains(&haystack, &[1, 2])); - assert!(!contains(&haystack, &[1, 3])); - } - #[test] fn test_predicate() { // `not == 10` with value `1` OK @@ -1441,6 +1370,9 @@ mod tests { #[test] fn test_predicate_match() { let variables = VariableSet::new(); + let current_dir = std::env::current_dir().unwrap(); + let file_root = Path::new("file_root"); + let context_dir = ContextDir::new(current_dir.as_path(), file_root); // predicate: `matches /a{3}/` // value: aa @@ -1449,11 +1381,12 @@ mod tests { }); let value = Value::String("aa".to_string()); let source_info = SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)); - let assert_result = eval_match(&expected, source_info, &variables, &value).unwrap(); + let assert_result = + eval_match(&expected, source_info, &variables, &value, &context_dir).unwrap(); assert!(!assert_result.success); assert!(!assert_result.type_mismatch); assert_eq!(assert_result.actual, "string "); - assert_eq!(assert_result.expected, "matches regex "); + assert_eq!(assert_result.expected, "matches regex "); } #[test] @@ -1464,41 +1397,6 @@ mod tests { assert!(!res.type_mismatch); assert_eq!(res.actual, "2020-03-09T22:18:26.625Z"); assert_eq!(res.expected, "string with format YYYY-MM-DDTHH:mm:ss.sssZ"); - - // Some values from - let value = Value::String("1985-04-12T23:20:50.52Z".to_string()); - let res = eval_is_iso_date(&value).unwrap(); - assert!(res.success); - - let value = Value::String("1996-12-19T16:39:57-08:00".to_string()); - let res = eval_is_iso_date(&value).unwrap(); - assert!(res.success); - - let value = Value::String("1990-12-31T23:59:60Z".to_string()); - let res = eval_is_iso_date(&value).unwrap(); - assert!(res.success); - - let value = Value::String("1990-12-31T15:59:60-08:00".to_string()); - let res = eval_is_iso_date(&value).unwrap(); - assert!(res.success); - - let value = Value::String("1937-01-01T12:00:27.87+00:20".to_string()); - let res = eval_is_iso_date(&value).unwrap(); - assert!(res.success); - - let value = Value::String("1978-01-15".to_string()); - let res = eval_is_iso_date(&value).unwrap(); - assert!(!res.success); - assert!(!res.type_mismatch); - assert_eq!(res.actual, "1978-01-15"); - assert_eq!(res.expected, "string with format YYYY-MM-DDTHH:mm:ss.sssZ"); - - let value = Value::Bool(true); - let res = eval_is_iso_date(&value).unwrap(); - assert!(!res.success); - assert!(res.type_mismatch); - assert_eq!(res.actual, "boolean "); - assert_eq!(res.expected, "string"); } #[test] diff --git a/packages/hurl/src/runner/value.rs b/packages/hurl/src/runner/value.rs index 4755e42c80..96d914357f 100644 --- a/packages/hurl/src/runner/value.rs +++ b/packages/hurl/src/runner/value.rs @@ -65,6 +65,12 @@ pub enum ValueKind { Unit, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EvalError { + Type, + InvalidRegex, +} + /// Equality of values /// as used in the predicate == /// diff --git a/packages/hurl/src/runner/value_impl.rs b/packages/hurl/src/runner/value_impl.rs index 2169f2de1b..2efeaab752 100644 --- a/packages/hurl/src/runner/value_impl.rs +++ b/packages/hurl/src/runner/value_impl.rs @@ -17,22 +17,192 @@ */ use std::cmp::Ordering; -use super::Value; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TypeError; +use super::{value::ValueKind, EvalError, Value}; impl Value { - /// Compare with another value + /// Compare with another value. /// - /// Returns TypeError if the values are not comparable - pub fn compare(&self, other: &Value) -> Result { + /// Returns a [`EvalError::Type`] if the given value types are not supported. + pub fn compare(&self, other: &Value) -> Result { match (self, other) { (Value::String(s1), Value::String(s2)) => Ok(s1.cmp(s2)), (Value::Number(n1), Value::Number(n2)) => Ok(n1.cmp_value(n2)), - _ => Err(TypeError), + _ => Err(EvalError::Type), } } + + /// Returns `true` if the value starts with the given prefix. + /// + /// Returns `false` if it does not. + /// + /// Returns a [`EvalError::Type`] if the given value types are not supported. + pub fn starts_with(&self, other: &Value) -> Result { + match (self, other) { + (Value::String(value), Value::String(prefix)) => Ok(value.starts_with(prefix)), + (Value::Bytes(value), Value::Bytes(prefix)) => Ok(value.starts_with(prefix)), + _ => Err(EvalError::Type), + } + } + + /// Returns `true` if the value ends with the given suffix. + /// + /// Returns `false` if it does not. + /// + /// Returns a [`EvalError::Type`] if the given value types are not supported. + pub fn ends_with(&self, other: &Value) -> Result { + match (self, other) { + (Value::String(value), Value::String(suffix)) => Ok(value.ends_with(suffix)), + (Value::Bytes(value), Value::Bytes(suffix)) => Ok(value.ends_with(suffix)), + _ => Err(EvalError::Type), + } + } + + /// Returns `true` if the value contains another value. + /// + /// Returns `false` if it does not. + /// + /// Returns a [`EvalError::Type`] if the given value types are not supported. + pub fn contains(&self, other: &Value) -> Result { + match (self, other) { + (Value::String(s), Value::String(substr)) => Ok(s.as_str().contains(substr.as_str())), + + (Value::Bytes(s), Value::Bytes(substr)) => { + Ok(contains(s.as_slice(), substr.as_slice())) + } + _ => Err(EvalError::Type), + } + } + + /// Returns `true` if the list value includes another value. + /// + /// Returns `false` if it does not. + /// + /// Returns a [`EvalError::Type`] if the given value types are not supported. + /// + /// TODO: deprecate method in favor of contains. + pub fn includes(&self, other: &Value) -> Result { + match self { + Value::List(values) => { + let mut included = false; + for v in values { + if v == other { + included = true; + break; + } + } + Ok(included) + } + _ => Err(EvalError::Type), + } + } + + /// Returns `true` the value is a boolean. + /// + /// Returns `false` if it is not. + pub fn is_boolean(&self) -> bool { + self.kind() == ValueKind::Bool + } + + /// Returns `true` the value is a collection. + /// + /// Returns `false` if it is not. + pub fn is_collection(&self) -> bool { + self.kind() == ValueKind::Bytes + || self.kind() == ValueKind::List + || self.kind() == ValueKind::Nodeset + || self.kind() == ValueKind::Object + } + + /// Returns `true` the value is a date. + /// + /// Returns `false` if it is not. + pub fn is_date(&self) -> bool { + self.kind() == ValueKind::Date + } + + /// Returns `true` the value is a float. + /// + /// Returns `false` if it is not. + pub fn is_float(&self) -> bool { + self.kind() == ValueKind::Float + } + + /// Returns `true` the value is an integer. + /// + /// Returns `false` if it is not. + pub fn is_integer(&self) -> bool { + self.kind() == ValueKind::Integer + } + + /// Returns `true` the value is a number. + /// + /// Returns `false` if it is not. + pub fn is_number(&self) -> bool { + self.kind() == ValueKind::Integer || self.kind() == ValueKind::Float + } + + /// Returns `true` the value is a String. + /// + /// Returns `false` if it is not. + pub fn is_string(&self) -> bool { + self.kind() == ValueKind::String || self.kind() == ValueKind::Secret + } + + /// Returns `true` the string value represents a RFC339 date (format YYYY-MM-DDTHH:mm:ss.sssZ). + /// + /// Returns `false` if it does not. + /// + /// Returns a [`EvalError::Type`] if the given value is not a String. + pub fn is_iso_date(&self) -> Result { + match self { + Value::String(value) => Ok(chrono::DateTime::parse_from_rfc3339(value).is_ok()), + _ => Err(EvalError::Type), + } + } + + /// Returns count of the value. + /// + /// Returns a [`EvalError::Type`] if the type of the value is not supported. + pub fn count(&self) -> Result { + match self { + Value::List(values) => Ok(values.len()), + Value::String(data) => Ok(data.len()), + Value::Nodeset(count) => Ok(*count), + Value::Object(props) => Ok(props.len()), + Value::Bytes(data) => Ok(data.len()), + _ => Err(EvalError::Type), + } + } + + /// Returns `true` if and only if there is a match for the regex anywhere in the value. + /// + /// Returns `false` otherwise. + /// + /// Returns a [`EvalError::Type`] if the type of the value is not supported. + /// + /// Returns an [`EvalError::InvalidRegex`] if the String is not a valid Regex. + pub fn is_match(&self, other: &Value) -> Result { + let regex = match other { + Value::String(s) => match regex::Regex::new(s.as_str()) { + Ok(re) => re, + Err(_) => return Err(EvalError::InvalidRegex), + }, + Value::Regex(re) => re.clone(), + _ => { + return Err(EvalError::Type); + } + }; + match self { + Value::String(value) => Ok(regex.is_match(value.as_str())), + _ => Err(EvalError::Type), + } + } +} + +fn contains(haystack: &[u8], needle: &[u8]) -> bool { + haystack + .windows(needle.len()) + .any(|window| window == needle) } #[cfg(test)] @@ -61,15 +231,117 @@ mod tests { .unwrap(), Ordering::Greater ); - } - #[test] - fn test_compare_error() { assert_eq!( Value::Number(Number::Integer(1)) .compare(&Value::Bool(true)) .unwrap_err(), - TypeError + EvalError::Type ); } + + #[test] + fn test_starts_with() { + assert!(Value::String("Hello".to_string()) + .starts_with(&Value::String("H".to_string())) + .unwrap()); + assert!(!Value::Bytes(vec![0, 1, 2]) + .starts_with(&Value::Bytes(vec![0, 2])) + .unwrap()); + } + + #[test] + fn test_ends_with() { + assert!(Value::String("Hello".to_string()) + .ends_with(&Value::String("o".to_string())) + .unwrap()); + assert!(!Value::Bytes(vec![0, 1, 2]) + .ends_with(&Value::Bytes(vec![0, 2])) + .unwrap()); + } + + #[test] + fn test_contains() { + let haystack = [1, 2, 3]; + assert!(contains(&haystack, &[1])); + assert!(contains(&haystack, &[1, 2])); + assert!(!contains(&haystack, &[1, 3])); + assert!(Value::String("abc".to_string()) + .contains(&Value::String("ab".to_string())) + .unwrap()); + } + + #[test] + fn test_include() { + let values = Value::List(vec![ + Value::Number(Number::Integer(0)), + Value::Number(Number::Integer(2)), + Value::Number(Number::Integer(3)), + ]); + assert!(values.includes(&Value::Number(Number::Integer(0))).unwrap()); + assert!(!values.includes(&Value::Number(Number::Integer(4))).unwrap()); + } + + #[test] + fn test_type() { + let value1 = Value::Bool(true); + assert!(value1.is_boolean()); + assert!(!value1.is_collection()); + + let value2 = Value::Number(Number::Integer(1)); + assert!(!value2.is_boolean()); + assert!(!value2.is_collection()); + assert!(value2.is_number()); + assert!(value2.is_integer()); + } + + #[test] + fn test_iso_date() { + // Some values from + assert!(Value::String("1985-04-12T23:20:50.52Z".to_string()) + .is_iso_date() + .unwrap()); + assert!(Value::String("1996-12-19T16:39:57-08:00".to_string()) + .is_iso_date() + .unwrap()); + assert!(Value::String("1990-12-31T23:59:60Z".to_string()) + .is_iso_date() + .unwrap()); + assert!(Value::String("1990-12-31T15:59:60-08:00".to_string()) + .is_iso_date() + .unwrap()); + assert!(Value::String("1937-01-01T12:00:27.87+00:20".to_string()) + .is_iso_date() + .unwrap()); + assert!(!Value::String("1978-01-15".to_string()) + .is_iso_date() + .unwrap()); + assert_eq!( + Value::Bool(true).is_iso_date().unwrap_err(), + EvalError::Type + ); + } + + #[test] + fn test_is_match() { + let value = Value::String("hello".to_string()); + + let regex1 = Value::String("he.*".to_string()); + assert!(value.is_match(®ex1).unwrap()); + + let regex2 = Value::Regex(regex::Regex::new("he.*").unwrap()); + assert!(value.is_match(®ex2).unwrap()); + + let regex3 = Value::String("HE.*".to_string()); + assert!(!value.is_match(®ex3).unwrap()); + + let regex4 = Value::String("?HE.*".to_string()); + assert_eq!( + value.is_match(®ex4).unwrap_err(), + EvalError::InvalidRegex + ); + + let regex5 = Value::Bool(true); + assert_eq!(value.is_match(®ex5).unwrap_err(), EvalError::Type); + } }