use std::cmp::Ordering;
use serde_json::Value;
use crate::error::{FraiseQLError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ComparisonOperator {
LessThan,
LessEqual,
GreaterThan,
GreaterEqual,
Equal,
NotEqual,
}
impl ComparisonOperator {
#[allow(clippy::should_implement_trait)]
#[must_use]
pub fn from_str(s: &str) -> Option<Self> {
match s {
"<" | "lt" => Some(Self::LessThan),
"<=" | "lte" => Some(Self::LessEqual),
">" | "gt" => Some(Self::GreaterThan),
">=" | "gte" => Some(Self::GreaterEqual),
"==" | "eq" => Some(Self::Equal),
"!=" | "neq" => Some(Self::NotEqual),
_ => None,
}
}
#[must_use]
pub const fn symbol(&self) -> &'static str {
match self {
Self::LessThan => "<",
Self::LessEqual => "<=",
Self::GreaterThan => ">",
Self::GreaterEqual => ">=",
Self::Equal => "==",
Self::NotEqual => "!=",
}
}
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::LessThan => "less than",
Self::LessEqual => "less than or equal to",
Self::GreaterThan => "greater than",
Self::GreaterEqual => "greater than or equal to",
Self::Equal => "equal to",
Self::NotEqual => "not equal to",
}
}
}
pub fn validate_cross_field_comparison(
input: &Value,
left_field: &str,
operator: ComparisonOperator,
right_field: &str,
context_path: Option<&str>,
) -> Result<()> {
let field_path = context_path.unwrap_or("input");
if let Value::Object(obj) = input {
let left_val = obj.get(left_field).ok_or_else(|| FraiseQLError::Validation {
message: format!("Field '{}' not found in input", left_field),
path: Some(field_path.to_string()),
})?;
let right_val = obj.get(right_field).ok_or_else(|| FraiseQLError::Validation {
message: format!("Field '{}' not found in input", right_field),
path: Some(field_path.to_string()),
})?;
if matches!(left_val, Value::Null) || matches!(right_val, Value::Null) {
return Ok(());
}
compare_values(left_val, right_val, left_field, operator, right_field, field_path)
} else {
Err(FraiseQLError::Validation {
message: "Input is not an object".to_string(),
path: Some(field_path.to_string()),
})
}
}
fn compare_values(
left: &Value,
right: &Value,
left_field: &str,
operator: ComparisonOperator,
right_field: &str,
context_path: &str,
) -> Result<()> {
let ordering = match (left, right) {
(Value::Number(l), Value::Number(r)) => {
let l_val = l.as_f64().unwrap_or(0.0);
let r_val = r.as_f64().unwrap_or(0.0);
if l_val < r_val {
Ordering::Less
} else if l_val > r_val {
Ordering::Greater
} else {
Ordering::Equal
}
},
(Value::String(l), Value::String(r)) => l.cmp(r),
_ => {
return Err(FraiseQLError::Validation {
message: format!(
"Cannot compare '{}' ({}) with '{}' ({})",
left_field,
value_type_name(left),
right_field,
value_type_name(right)
),
path: Some(context_path.to_string()),
});
},
};
let result = match operator {
ComparisonOperator::LessThan => matches!(ordering, Ordering::Less),
ComparisonOperator::LessEqual => !matches!(ordering, Ordering::Greater),
ComparisonOperator::GreaterThan => matches!(ordering, Ordering::Greater),
ComparisonOperator::GreaterEqual => !matches!(ordering, Ordering::Less),
ComparisonOperator::Equal => matches!(ordering, Ordering::Equal),
ComparisonOperator::NotEqual => !matches!(ordering, Ordering::Equal),
};
if !result {
return Err(FraiseQLError::Validation {
message: format!(
"'{}' ({}) must be {} '{}' ({})",
left_field,
value_to_string(left),
operator.name(),
right_field,
value_to_string(right)
),
path: Some(context_path.to_string()),
});
}
Ok(())
}
const fn value_type_name(val: &Value) -> &'static str {
match val {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
fn value_to_string(val: &Value) -> String {
match val {
Value::String(s) => format!("\"{}\"", s),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => "null".to_string(),
_ => val.to_string(),
}
}