use crate::numeric::value_to_f64;
use serde_json::Value;
pub(super) fn compare_numeric<F>(
actual: &Value,
expected: Option<&Value>,
cmp: F,
) -> Result<bool, &'static str>
where
F: Fn(f64, f64) -> bool,
{
let actual_num = value_to_f64(actual).ok_or("actual value is not numeric")?;
let expected_num = expected
.and_then(value_to_f64)
.ok_or("expected value is missing or not numeric")?;
Ok(cmp(actual_num, expected_num))
}
pub(super) fn compare_equal(
actual: &Value,
expected: Option<&Value>,
) -> Result<bool, &'static str> {
let expected = expected.ok_or("expected value is missing")?;
if let (Some(a), Some(b)) = (actual.as_str(), expected.as_str()) {
return Ok(a == b);
}
if let (Some(a), Some(b)) = (value_to_f64(actual), value_to_f64(expected)) {
return Ok((a - b).abs() < f64::EPSILON);
}
Ok(actual == expected)
}
pub(super) fn compare_in(
actual: &Value,
expected: Option<&Vec<Value>>,
) -> Result<bool, &'static str> {
let list = expected.ok_or("expected values list is missing")?;
for item in list {
if compare_equal(actual, Some(item)).unwrap_or(false) {
return Ok(true);
}
}
Ok(false)
}
pub(super) fn compare_contains(
actual: &Value,
expected: Option<&Value>,
) -> Result<bool, &'static str> {
let expected = expected.ok_or("expected value is missing")?;
match actual {
Value::String(s) => {
let needle = expected
.as_str()
.ok_or("expected value must be a string for string contains checks")?;
Ok(s.contains(needle))
}
Value::Array(arr) => {
for item in arr {
if compare_equal(item, Some(expected)).unwrap_or(false) {
return Ok(true);
}
}
Ok(false)
}
_ => Err("contains is only valid for string or array actual values"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn compare_numeric_gt_true_when_actual_exceeds_expected() {
let result = compare_numeric(&json!(10), Some(&json!(5)), |a, b| a > b);
assert_eq!(result, Ok(true));
}
#[test]
fn compare_numeric_gt_false_when_actual_equals_expected() {
let result = compare_numeric(&json!(5), Some(&json!(5)), |a, b| a > b);
assert_eq!(result, Ok(false));
}
#[test]
fn compare_numeric_gte_true_at_boundary() {
let result = compare_numeric(&json!(5), Some(&json!(5)), |a, b| a >= b);
assert_eq!(result, Ok(true));
}
#[test]
fn compare_numeric_lt_true_when_actual_below_expected() {
let result = compare_numeric(&json!(3), Some(&json!(5)), |a, b| a < b);
assert_eq!(result, Ok(true));
}
#[test]
fn compare_numeric_lte_true_at_boundary() {
let result = compare_numeric(&json!(5), Some(&json!(5)), |a, b| a <= b);
assert_eq!(result, Ok(true));
}
#[test]
fn compare_numeric_coerces_numeric_strings() {
let result = compare_numeric(&json!("12.5"), Some(&json!(10)), |a, b| a > b);
assert_eq!(result, Ok(true));
}
#[test]
fn compare_numeric_errors_when_actual_not_numeric() {
let result = compare_numeric(&json!("abc"), Some(&json!(1)), |a, b| a > b);
assert_eq!(result, Err("actual value is not numeric"));
}
#[test]
fn compare_numeric_errors_when_expected_missing() {
let result = compare_numeric(&json!(1), None, |a, b| a > b);
assert_eq!(result, Err("expected value is missing or not numeric"));
}
#[test]
fn compare_numeric_errors_when_expected_not_numeric() {
let result = compare_numeric(&json!(1), Some(&json!(true)), |a, b| a > b);
assert_eq!(result, Err("expected value is missing or not numeric"));
}
#[test]
fn compare_numeric_nan_string_breaks_ordering() {
let nan = json!("NaN");
assert_eq!(
compare_numeric(&nan, Some(&json!(1)), |a, b| a > b),
Ok(false)
);
assert_eq!(
compare_numeric(&nan, Some(&json!(1)), |a, b| a < b),
Ok(false)
);
assert_eq!(
compare_numeric(&nan, Some(&json!(1)), |a, b| (a - b).abs() < f64::EPSILON),
Ok(false)
);
}
#[test]
fn compare_equal_strings_match_case_sensitively() {
assert_eq!(compare_equal(&json!("Foo"), Some(&json!("Foo"))), Ok(true));
assert_eq!(compare_equal(&json!("Foo"), Some(&json!("foo"))), Ok(false));
}
#[test]
fn compare_equal_special_numeric_strings_compared_as_strings() {
assert_eq!(compare_equal(&json!("inf"), Some(&json!("inf"))), Ok(true));
assert_eq!(compare_equal(&json!("nan"), Some(&json!("nan"))), Ok(true));
assert_eq!(compare_equal(&json!("inf"), Some(&json!("INF"))), Ok(false));
}
#[test]
fn compare_equal_numeric_int_float_mismatch_is_equal() {
assert_eq!(compare_equal(&json!(42), Some(&json!(42.0))), Ok(true));
}
#[test]
fn compare_equal_numeric_strings_coerce() {
assert_eq!(compare_equal(&json!("42"), Some(&json!(42))), Ok(true));
}
#[test]
fn compare_equal_epsilon_boundary_is_strict() {
let a = 1.0_f64;
let b = a + f64::EPSILON;
assert_eq!(
compare_equal(&json!(a), Some(&json!(b))),
Ok(false),
"difference of exactly EPSILON must not be treated as equal"
);
}
#[test]
fn compare_equal_falls_back_to_json_equality_for_arrays() {
assert_eq!(
compare_equal(&json!([1, 2, 3]), Some(&json!([1, 2, 3]))),
Ok(true)
);
assert_eq!(
compare_equal(&json!([1, 2, 3]), Some(&json!([3, 2, 1]))),
Ok(false)
);
}
#[test]
fn compare_equal_falls_back_to_json_equality_for_objects() {
assert_eq!(
compare_equal(&json!({"a": 1}), Some(&json!({"a": 1}))),
Ok(true)
);
assert_eq!(
compare_equal(&json!({"a": 1}), Some(&json!({"a": 2}))),
Ok(false)
);
}
#[test]
fn compare_equal_booleans_match_via_json_equality() {
assert_eq!(compare_equal(&json!(true), Some(&json!(true))), Ok(true));
assert_eq!(compare_equal(&json!(true), Some(&json!(false))), Ok(false));
}
#[test]
fn compare_equal_null_matches_null() {
assert_eq!(compare_equal(&json!(null), Some(&json!(null))), Ok(true));
}
#[test]
fn compare_equal_type_mismatch_returns_false_not_error() {
assert_eq!(
compare_equal(&json!("foo"), Some(&json!({"foo": 1}))),
Ok(false)
);
}
#[test]
fn compare_equal_errors_when_expected_missing() {
assert_eq!(
compare_equal(&json!(1), None),
Err("expected value is missing")
);
}
#[test]
fn compare_in_finds_string_member() {
let list = vec![json!("MIT"), json!("Apache-2.0")];
assert_eq!(compare_in(&json!("MIT"), Some(&list)), Ok(true));
}
#[test]
fn compare_in_returns_false_for_non_member() {
let list = vec![json!("MIT"), json!("Apache-2.0")];
assert_eq!(compare_in(&json!("GPL"), Some(&list)), Ok(false));
}
#[test]
fn compare_in_empty_list_is_never_member() {
let list: Vec<Value> = vec![];
assert_eq!(compare_in(&json!("anything"), Some(&list)), Ok(false));
}
#[test]
fn compare_in_errors_when_list_missing() {
assert_eq!(
compare_in(&json!("MIT"), None),
Err("expected values list is missing")
);
}
#[test]
fn compare_in_finds_numeric_member_with_coercion() {
let list = vec![json!(1), json!(2), json!(3)];
assert_eq!(compare_in(&json!(2.0), Some(&list)), Ok(true));
}
#[test]
fn compare_in_skips_uncomparable_items_without_error() {
let list = vec![json!({"unexpected": "object"}), json!("target")];
assert_eq!(compare_in(&json!("target"), Some(&list)), Ok(true));
}
#[test]
fn compare_contains_substring_in_string() {
assert_eq!(
compare_contains(&json!("hello world"), Some(&json!("world"))),
Ok(true)
);
}
#[test]
fn compare_contains_string_misses() {
assert_eq!(
compare_contains(&json!("hello world"), Some(&json!("nope"))),
Ok(false)
);
}
#[test]
fn compare_contains_empty_substring_matches_any_string() {
assert_eq!(compare_contains(&json!("abc"), Some(&json!(""))), Ok(true));
}
#[test]
fn compare_contains_array_member_match() {
let arr = json!(["a", "b", "c"]);
assert_eq!(compare_contains(&arr, Some(&json!("b"))), Ok(true));
}
#[test]
fn compare_contains_array_member_miss() {
let arr = json!(["a", "b", "c"]);
assert_eq!(compare_contains(&arr, Some(&json!("z"))), Ok(false));
}
#[test]
fn compare_contains_string_actual_with_non_string_needle_errors() {
let result = compare_contains(&json!("abc"), Some(&json!(1)));
assert_eq!(
result,
Err("expected value must be a string for string contains checks")
);
}
#[test]
fn compare_contains_errors_for_non_string_non_array_actual() {
let result = compare_contains(&json!(42), Some(&json!(1)));
assert_eq!(
result,
Err("contains is only valid for string or array actual values")
);
}
#[test]
fn compare_contains_errors_when_expected_missing() {
assert_eq!(
compare_contains(&json!("abc"), None),
Err("expected value is missing")
);
}
#[test]
fn compare_contains_array_with_mixed_types_uses_compare_equal() {
let arr = json!([1, 2, 3]);
assert_eq!(compare_contains(&arr, Some(&json!(2.0))), Ok(true));
}
}