use serde::Deserialize;
#[derive(Clone, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BodyOperator {
Equal,
EqualString,
Contains,
StartsWith,
EndsWith,
Regex,
EqualTyped,
EqualNumber,
GreaterThan,
LessThan,
GreaterOrEqual,
LessOrEqual,
Exists,
Absent,
ArrayLengthEqual,
ArrayLengthAtLeast,
ArrayContains,
}
impl Default for BodyOperator {
fn default() -> Self {
Self::Equal
}
}
impl std::fmt::Display for BodyOperator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Equal => write!(f, " == "),
Self::EqualString => write!(f, " == (string) "),
Self::Contains => write!(f, " contains "),
Self::StartsWith => write!(f, " starts_with "),
Self::EndsWith => write!(f, " ends_with "),
Self::Regex => write!(f, " matches regex "),
Self::EqualTyped => write!(f, " == (typed) "),
Self::EqualNumber => write!(f, " == (number) "),
Self::GreaterThan => write!(f, " > "),
Self::LessThan => write!(f, " < "),
Self::GreaterOrEqual => write!(f, " >= "),
Self::LessOrEqual => write!(f, " <= "),
Self::Exists => write!(f, " exists"),
Self::Absent => write!(f, " absent"),
Self::ArrayLengthEqual => write!(f, " array_length == "),
Self::ArrayLengthAtLeast => write!(f, " array_length >= "),
Self::ArrayContains => write!(f, " array_contains "),
}
}
}
impl BodyOperator {
pub fn is_match(
&self,
resolved: &serde_json::Value,
configured_value: &str,
) -> bool {
use serde_json::Value;
match self {
Self::Equal | Self::EqualString => {
let lhs = value_as_string(resolved);
lhs == configured_value
}
Self::Contains => value_as_string(resolved).contains(configured_value),
Self::StartsWith => value_as_string(resolved).starts_with(configured_value),
Self::EndsWith => value_as_string(resolved).ends_with(configured_value),
Self::Regex => {
let text = value_as_string(resolved);
regex_is_match(configured_value, &text)
}
Self::EqualTyped => {
let expected: Value = match serde_json::from_str(configured_value) {
Ok(v) => v,
Err(_) => return false,
};
resolved == &expected
}
Self::EqualNumber => match (to_f64(resolved), parse_f64(configured_value)) {
(Some(l), Some(r)) => (l - r).abs() < f64::EPSILON,
_ => false,
},
Self::GreaterThan => match (to_f64(resolved), parse_f64(configured_value)) {
(Some(l), Some(r)) => l > r,
_ => false,
},
Self::LessThan => match (to_f64(resolved), parse_f64(configured_value)) {
(Some(l), Some(r)) => l < r,
_ => false,
},
Self::GreaterOrEqual => match (to_f64(resolved), parse_f64(configured_value)) {
(Some(l), Some(r)) => l >= r,
_ => false,
},
Self::LessOrEqual => match (to_f64(resolved), parse_f64(configured_value)) {
(Some(l), Some(r)) => l <= r,
_ => false,
},
Self::Exists => true,
Self::Absent => false,
Self::ArrayLengthEqual => match resolved {
Value::Array(arr) => {
parse_usize(configured_value).map_or(false, |n| arr.len() == n)
}
_ => false,
},
Self::ArrayLengthAtLeast => match resolved {
Value::Array(arr) => {
parse_usize(configured_value).map_or(false, |n| arr.len() >= n)
}
_ => false,
},
Self::ArrayContains => match resolved {
Value::Array(arr) => {
let expected: Value = match serde_json::from_str(configured_value) {
Ok(v) => v,
Err(_) => Value::String(configured_value.to_owned()),
};
arr.contains(&expected)
}
_ => false,
},
}
}
}
fn value_as_string(v: &serde_json::Value) -> String {
match v {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
}
}
fn to_f64(v: &serde_json::Value) -> Option<f64> {
match v {
serde_json::Value::Number(n) => n.as_f64(),
serde_json::Value::String(s) => s.parse::<f64>().ok(),
_ => None,
}
}
fn parse_f64(s: &str) -> Option<f64> {
s.parse::<f64>().ok()
}
fn parse_usize(s: &str) -> Option<usize> {
s.parse::<usize>().ok()
}
fn regex_is_match(pattern: &str, text: &str) -> bool {
text.contains(pattern)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn equal_string_coercion() {
let op = BodyOperator::Equal;
assert!(op.is_match(&json!("hello"), "hello"));
assert!(op.is_match(&json!(42), "42"));
assert!(!op.is_match(&json!("hello"), "world"));
}
#[test]
fn equal_string_explicit() {
let op = BodyOperator::EqualString;
assert!(op.is_match(&json!("hello"), "hello"));
assert!(op.is_match(&json!(42), "42"));
}
#[test]
fn equal_typed_distinguishes_types() {
let op = BodyOperator::EqualTyped;
assert!(op.is_match(&json!(42), "42"));
assert!(!op.is_match(&json!("42"), "42")); assert!(op.is_match(&json!("42"), "\"42\"")); assert!(op.is_match(&json!(true), "true"));
}
#[test]
fn numeric_operators() {
assert!(BodyOperator::EqualNumber.is_match(&json!(42), "42"));
assert!(BodyOperator::GreaterThan.is_match(&json!(43), "42"));
assert!(!BodyOperator::GreaterThan.is_match(&json!(41), "42"));
assert!(BodyOperator::LessThan.is_match(&json!(41), "42"));
assert!(BodyOperator::GreaterOrEqual.is_match(&json!(42), "42"));
assert!(BodyOperator::LessOrEqual.is_match(&json!(42), "42"));
}
#[test]
fn numeric_string_coercion() {
assert!(BodyOperator::EqualNumber.is_match(&json!("42"), "42"));
assert!(BodyOperator::GreaterThan.is_match(&json!("100"), "42"));
}
#[test]
fn numeric_non_number_returns_false() {
assert!(!BodyOperator::GreaterThan.is_match(&json!("hello"), "42"));
assert!(!BodyOperator::EqualNumber.is_match(&json!(null), "0"));
}
#[test]
fn exists_always_true() {
assert!(BodyOperator::Exists.is_match(&json!("anything"), "ignored"));
assert!(BodyOperator::Exists.is_match(&json!(null), "ignored"));
}
#[test]
fn absent_always_false() {
assert!(!BodyOperator::Absent.is_match(&json!("anything"), "ignored"));
}
#[test]
fn array_length_equal() {
assert!(BodyOperator::ArrayLengthEqual.is_match(&json!([1, 2, 3]), "3"));
assert!(!BodyOperator::ArrayLengthEqual.is_match(&json!([1, 2]), "3"));
assert!(!BodyOperator::ArrayLengthEqual.is_match(&json!("not_array"), "1"));
}
#[test]
fn array_length_at_least() {
assert!(BodyOperator::ArrayLengthAtLeast.is_match(&json!([1, 2, 3]), "3"));
assert!(BodyOperator::ArrayLengthAtLeast.is_match(&json!([1, 2, 3, 4]), "3"));
assert!(!BodyOperator::ArrayLengthAtLeast.is_match(&json!([1, 2]), "3"));
}
#[test]
fn array_contains() {
assert!(BodyOperator::ArrayContains.is_match(&json!([1, 2, 3]), "2"));
assert!(!BodyOperator::ArrayContains.is_match(&json!([1, 2, 3]), "4"));
assert!(BodyOperator::ArrayContains.is_match(&json!(["a", "b"]), "\"a\""));
assert!(!BodyOperator::ArrayContains.is_match(&json!("not_array"), "1"));
}
}