fraiseql_core/validation/
cross_field.rs1use std::cmp::Ordering;
25
26use serde_json::Value;
27
28use crate::error::{FraiseQLError, Result};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[non_exhaustive]
33pub enum ComparisonOperator {
34 LessThan,
36 LessEqual,
38 GreaterThan,
40 GreaterEqual,
42 Equal,
44 NotEqual,
46}
47
48impl ComparisonOperator {
49 #[allow(clippy::should_implement_trait)]
51 #[must_use]
54 pub fn from_str(s: &str) -> Option<Self> {
55 match s {
56 "<" | "lt" => Some(Self::LessThan),
57 "<=" | "lte" => Some(Self::LessEqual),
58 ">" | "gt" => Some(Self::GreaterThan),
59 ">=" | "gte" => Some(Self::GreaterEqual),
60 "==" | "eq" => Some(Self::Equal),
61 "!=" | "neq" => Some(Self::NotEqual),
62 _ => None,
63 }
64 }
65
66 #[must_use]
68 pub const fn symbol(&self) -> &'static str {
69 match self {
70 Self::LessThan => "<",
71 Self::LessEqual => "<=",
72 Self::GreaterThan => ">",
73 Self::GreaterEqual => ">=",
74 Self::Equal => "==",
75 Self::NotEqual => "!=",
76 }
77 }
78
79 #[must_use]
81 pub const fn name(&self) -> &'static str {
82 match self {
83 Self::LessThan => "less than",
84 Self::LessEqual => "less than or equal to",
85 Self::GreaterThan => "greater than",
86 Self::GreaterEqual => "greater than or equal to",
87 Self::Equal => "equal to",
88 Self::NotEqual => "not equal to",
89 }
90 }
91}
92
93pub fn validate_cross_field_comparison(
112 input: &Value,
113 left_field: &str,
114 operator: ComparisonOperator,
115 right_field: &str,
116 context_path: Option<&str>,
117) -> Result<()> {
118 let field_path = context_path.unwrap_or("input");
119
120 if let Value::Object(obj) = input {
121 let left_val = obj.get(left_field).ok_or_else(|| FraiseQLError::Validation {
122 message: format!("Field '{}' not found in input", left_field),
123 path: Some(field_path.to_string()),
124 })?;
125
126 let right_val = obj.get(right_field).ok_or_else(|| FraiseQLError::Validation {
127 message: format!("Field '{}' not found in input", right_field),
128 path: Some(field_path.to_string()),
129 })?;
130
131 if matches!(left_val, Value::Null) || matches!(right_val, Value::Null) {
133 return Ok(());
134 }
135
136 compare_values(left_val, right_val, left_field, operator, right_field, field_path)
137 } else {
138 Err(FraiseQLError::Validation {
139 message: "Input is not an object".to_string(),
140 path: Some(field_path.to_string()),
141 })
142 }
143}
144
145fn compare_values(
147 left: &Value,
148 right: &Value,
149 left_field: &str,
150 operator: ComparisonOperator,
151 right_field: &str,
152 context_path: &str,
153) -> Result<()> {
154 let ordering = match (left, right) {
155 (Value::Number(l), Value::Number(r)) => {
157 let l_val = l.as_f64().unwrap_or(0.0);
158 let r_val = r.as_f64().unwrap_or(0.0);
159 if l_val < r_val {
160 Ordering::Less
161 } else if l_val > r_val {
162 Ordering::Greater
163 } else {
164 Ordering::Equal
165 }
166 },
167 (Value::String(l), Value::String(r)) => l.cmp(r),
169 _ => {
171 return Err(FraiseQLError::Validation {
172 message: format!(
173 "Cannot compare '{}' ({}) with '{}' ({})",
174 left_field,
175 value_type_name(left),
176 right_field,
177 value_type_name(right)
178 ),
179 path: Some(context_path.to_string()),
180 });
181 },
182 };
183
184 let result = match operator {
185 ComparisonOperator::LessThan => matches!(ordering, Ordering::Less),
186 ComparisonOperator::LessEqual => !matches!(ordering, Ordering::Greater),
187 ComparisonOperator::GreaterThan => matches!(ordering, Ordering::Greater),
188 ComparisonOperator::GreaterEqual => !matches!(ordering, Ordering::Less),
189 ComparisonOperator::Equal => matches!(ordering, Ordering::Equal),
190 ComparisonOperator::NotEqual => !matches!(ordering, Ordering::Equal),
191 };
192
193 if !result {
194 return Err(FraiseQLError::Validation {
195 message: format!(
196 "'{}' ({}) must be {} '{}' ({})",
197 left_field,
198 value_to_string(left),
199 operator.name(),
200 right_field,
201 value_to_string(right)
202 ),
203 path: Some(context_path.to_string()),
204 });
205 }
206
207 Ok(())
208}
209
210const fn value_type_name(val: &Value) -> &'static str {
212 match val {
213 Value::Null => "null",
214 Value::Bool(_) => "boolean",
215 Value::Number(_) => "number",
216 Value::String(_) => "string",
217 Value::Array(_) => "array",
218 Value::Object(_) => "object",
219 }
220}
221
222fn value_to_string(val: &Value) -> String {
224 match val {
225 Value::String(s) => format!("\"{}\"", s),
226 Value::Number(n) => n.to_string(),
227 Value::Bool(b) => b.to_string(),
228 Value::Null => "null".to_string(),
229 _ => val.to_string(),
230 }
231}