use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrderedValue(pub Value);
impl PartialOrd for OrderedValue {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OrderedValue {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
value_rank(&self.0)
.cmp(&value_rank(&other.0))
.then_with(|| compare_same_kind(&self.0, &other.0))
}
}
fn value_rank(v: &Value) -> u8 {
match v {
Value::Null => 0,
Value::Bool(_) => 1,
Value::Number(_) => 2,
Value::String(_) => 3,
Value::Array(_) => 4,
Value::Object(_) => 5,
}
}
fn compare_same_kind(a: &Value, b: &Value) -> std::cmp::Ordering {
match (a, b) {
(Value::Null, Value::Null) => std::cmp::Ordering::Equal,
(Value::Bool(x), Value::Bool(y)) => x.cmp(y),
(Value::Number(x), Value::Number(y)) => {
let xf = x.as_f64().unwrap_or(f64::NEG_INFINITY);
let yf = y.as_f64().unwrap_or(f64::NEG_INFINITY);
xf.partial_cmp(&yf).unwrap_or(std::cmp::Ordering::Equal)
}
(Value::String(x), Value::String(y)) => x.cmp(y),
_ => std::cmp::Ordering::Equal,
}
}
fn coerce(v: &Value) -> Value {
if let Value::String(s) = v {
if let Ok(n) = s.parse::<i64>() {
return Value::Number(n.into());
}
if let Ok(f) = s.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(f) {
return Value::Number(n);
}
}
}
v.clone()
}
#[derive(Debug)]
pub enum SortCondition {
Eq(Value),
Ne(Value),
Lt(Value),
Lte(Value),
Gt(Value),
Gte(Value),
Between(Value, Value),
BeginsWith(String),
Contains(String),
}
impl SortCondition {
pub fn evaluate(&self, input: &Value) -> bool {
if input.is_null() {
return false;
}
let coerced_input = coerce(input);
match self {
SortCondition::Eq(v) => OrderedValue(coerced_input) == OrderedValue(coerce(v)),
SortCondition::Ne(v) => OrderedValue(coerced_input) != OrderedValue(coerce(v)),
SortCondition::Lt(v) => OrderedValue(coerced_input) < OrderedValue(coerce(v)),
SortCondition::Lte(v) => OrderedValue(coerced_input) <= OrderedValue(coerce(v)),
SortCondition::Gt(v) => OrderedValue(coerced_input) > OrderedValue(coerce(v)),
SortCondition::Gte(v) => OrderedValue(coerced_input) >= OrderedValue(coerce(v)),
SortCondition::Between(low, high) => {
let ov = OrderedValue(coerced_input);
ov >= OrderedValue(coerce(low)) && ov <= OrderedValue(coerce(high))
}
SortCondition::BeginsWith(prefix) => match input {
Value::String(s) => s.starts_with(prefix.as_str()),
_ => false,
},
SortCondition::Contains(substr) => match input {
Value::String(s) => s.contains(substr.as_str()),
_ => false,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn ordered_null_null_equal() {
assert_eq!(
OrderedValue(Value::Null).cmp(&OrderedValue(Value::Null)),
std::cmp::Ordering::Equal
);
}
#[test]
fn ordered_null_lt_bool() {
assert!(OrderedValue(Value::Null) < OrderedValue(json!(false)));
}
#[test]
fn ordered_bool_false_lt_true() {
assert!(OrderedValue(json!(false)) < OrderedValue(json!(true)));
}
#[test]
fn ordered_bool_equal() {
assert_eq!(OrderedValue(json!(true)), OrderedValue(json!(true)));
}
#[test]
fn ordered_numbers() {
assert!(OrderedValue(json!(1)) < OrderedValue(json!(2)));
assert_eq!(OrderedValue(json!(5)), OrderedValue(json!(5)));
assert!(OrderedValue(json!(10)) > OrderedValue(json!(9)));
}
#[test]
fn ordered_strings() {
assert!(OrderedValue(json!("apple")) < OrderedValue(json!("banana")));
assert_eq!(OrderedValue(json!("same")), OrderedValue(json!("same")));
}
#[test]
fn ordered_number_lt_string() {
assert!(OrderedValue(json!(42)) < OrderedValue(json!("hello")));
}
#[test]
fn ordered_array_gt_string() {
assert!(OrderedValue(json!([])) > OrderedValue(json!("z")));
}
#[test]
fn ordered_object_gt_array() {
assert!(OrderedValue(json!({})) > OrderedValue(json!([])));
}
#[test]
fn ordered_mixed_kinds_equal_within_catch_all() {
assert_eq!(
OrderedValue(json!([1])).cmp(&OrderedValue(json!([2]))),
std::cmp::Ordering::Equal
);
}
#[test]
fn coerce_integer_string() {
assert_eq!(coerce(&json!("42")), json!(42));
}
#[test]
fn coerce_float_string() {
let result = coerce(&json!("1.5"));
assert!(result.is_number());
}
#[test]
fn coerce_non_numeric_string() {
assert_eq!(coerce(&json!("hello")), json!("hello"));
}
#[test]
fn coerce_non_string_unchanged() {
assert_eq!(coerce(&json!(99)), json!(99));
}
#[test]
fn coerce_null_unchanged() {
assert_eq!(coerce(&Value::Null), Value::Null);
}
#[test]
fn eq_matches_equal_string() {
let cond = SortCondition::Eq(json!("alice"));
assert!(cond.evaluate(&json!("alice")));
assert!(!cond.evaluate(&json!("bob")));
}
#[test]
fn eq_matches_equal_number() {
let cond = SortCondition::Eq(json!(42));
assert!(cond.evaluate(&json!(42)));
assert!(!cond.evaluate(&json!(43)));
}
#[test]
fn eq_null_returns_false() {
assert!(!SortCondition::Eq(json!("x")).evaluate(&Value::Null));
}
#[test]
fn ne_matches() {
let cond = SortCondition::Ne(json!("alice"));
assert!(!cond.evaluate(&json!("alice")));
assert!(cond.evaluate(&json!("bob")));
}
#[test]
fn ne_null_returns_false() {
assert!(!SortCondition::Ne(json!("x")).evaluate(&Value::Null));
}
#[test]
fn lt_matches() {
let cond = SortCondition::Lt(json!(10));
assert!(cond.evaluate(&json!(5)));
assert!(!cond.evaluate(&json!(10)));
assert!(!cond.evaluate(&json!(15)));
}
#[test]
fn lt_null_returns_false() {
assert!(!SortCondition::Lt(json!(10)).evaluate(&Value::Null));
}
#[test]
fn lte_matches() {
let cond = SortCondition::Lte(json!(10));
assert!(cond.evaluate(&json!(5)));
assert!(cond.evaluate(&json!(10)));
assert!(!cond.evaluate(&json!(15)));
}
#[test]
fn lte_null_returns_false() {
assert!(!SortCondition::Lte(json!(10)).evaluate(&Value::Null));
}
#[test]
fn gt_matches() {
let cond = SortCondition::Gt(json!(10));
assert!(cond.evaluate(&json!(15)));
assert!(!cond.evaluate(&json!(10)));
assert!(!cond.evaluate(&json!(5)));
}
#[test]
fn gt_null_returns_false() {
assert!(!SortCondition::Gt(json!(10)).evaluate(&Value::Null));
}
#[test]
fn gte_matches() {
let cond = SortCondition::Gte(json!(10));
assert!(cond.evaluate(&json!(15)));
assert!(cond.evaluate(&json!(10)));
assert!(!cond.evaluate(&json!(5)));
}
#[test]
fn gte_null_returns_false() {
assert!(!SortCondition::Gte(json!(10)).evaluate(&Value::Null));
}
#[test]
fn between_inclusive_bounds() {
let cond = SortCondition::Between(json!(5), json!(15));
assert!(cond.evaluate(&json!(5)));
assert!(cond.evaluate(&json!(10)));
assert!(cond.evaluate(&json!(15)));
assert!(!cond.evaluate(&json!(4)));
assert!(!cond.evaluate(&json!(16)));
}
#[test]
fn between_null_returns_false() {
assert!(!SortCondition::Between(json!("a"), json!("z")).evaluate(&Value::Null));
}
#[test]
fn begins_with_matches() {
let cond = SortCondition::BeginsWith("user-".to_string());
assert!(cond.evaluate(&json!("user-abc")));
assert!(!cond.evaluate(&json!("admin-abc")));
}
#[test]
fn begins_with_null_returns_false() {
assert!(!SortCondition::BeginsWith("x".to_string()).evaluate(&Value::Null));
}
#[test]
fn begins_with_non_string_returns_false() {
assert!(!SortCondition::BeginsWith("x".to_string()).evaluate(&json!(42)));
}
#[test]
fn contains_matches() {
let cond = SortCondition::Contains("error".to_string());
assert!(cond.evaluate(&json!("server error")));
assert!(!cond.evaluate(&json!("all good")));
}
#[test]
fn contains_null_returns_false() {
assert!(!SortCondition::Contains("x".to_string()).evaluate(&Value::Null));
}
#[test]
fn contains_non_string_returns_false() {
assert!(!SortCondition::Contains("x".to_string()).evaluate(&json!(42)));
}
}