Implement functions with Value

This commit is contained in:
Fabrice Reix 2025-01-29 13:41:36 +01:00 committed by hurl-bot
parent 7265f3007f
commit 583cf901bc
No known key found for this signature in database
GPG Key ID: 1283A2B4A0DCAF8D
4 changed files with 359 additions and 183 deletions

View File

@ -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;

View File

@ -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<AssertResult, RunnerError> {
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<AssertResult, RunnerError> {
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<AssertResult, RunnerError> {
/// Evaluates if an `actual` value is a float.
fn eval_is_float(actual: &Value) -> Result<AssertResult, RunnerError> {
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<AssertResult, RunnerError> {
/// Evaluates if an `actual` value is a boolean.
fn eval_is_boolean(actual: &Value) -> Result<AssertResult, RunnerError> {
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<AssertResult, RunnerError> {
/// Evaluates if an `actual` value is a string.
fn eval_is_string(actual: &Value) -> Result<AssertResult, RunnerError> {
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<AssertResult, RunnerError> {
/// Evaluates if an `actual` value is a collection.
fn eval_is_collection(actual: &Value) -> Result<AssertResult, RunnerError> {
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<AssertResult, RunnerError> {
/// Evaluates if an `actual` value is a date.
fn eval_is_date(actual: &Value) -> Result<AssertResult, RunnerError> {
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<AssertResult, RunnerError> {
/// [`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<AssertResult, RunnerError> {
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<AssertResult, RunnerError> {
/// Evaluates if an `actual` is empty.
fn eval_is_empty(actual: &Value) -> Result<AssertResult, RunnerError> {
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<AssertResult, RunnerError> {
/// Evaluates if an `actual` value is a number.
fn eval_is_number(actual: &Value) -> Result<AssertResult, RunnerError> {
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 <aa>");
assert_eq!(assert_result.expected, "matches regex <a{3}>");
assert_eq!(assert_result.expected, "matches regex </a{3}/>");
}
#[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 <https://datatracker.ietf.org/doc/html/rfc3339>
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 <true>");
assert_eq!(res.expected, "string");
}
#[test]

View File

@ -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 ==
///

View File

@ -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<Ordering, TypeError> {
/// Returns a [`EvalError::Type`] if the given value types are not supported.
pub fn compare(&self, other: &Value) -> Result<Ordering, EvalError> {
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<bool, EvalError> {
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<bool, EvalError> {
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<bool, EvalError> {
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<bool, EvalError> {
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<bool, EvalError> {
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<usize, EvalError> {
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<bool, EvalError> {
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 <https://datatracker.ietf.org/doc/html/rfc3339>
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(&regex1).unwrap());
let regex2 = Value::Regex(regex::Regex::new("he.*").unwrap());
assert!(value.is_match(&regex2).unwrap());
let regex3 = Value::String("HE.*".to_string());
assert!(!value.is_match(&regex3).unwrap());
let regex4 = Value::String("?HE.*".to_string());
assert_eq!(
value.is_match(&regex4).unwrap_err(),
EvalError::InvalidRegex
);
let regex5 = Value::Bool(true);
assert_eq!(value.is_match(&regex5).unwrap_err(), EvalError::Type);
}
}