1use std::cmp::Ordering;
23
24use serde_json::Value;
25
26use crate::error::{FraiseQLError, Result};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ComparisonOperator {
31 LessThan,
33 LessEqual,
35 GreaterThan,
37 GreaterEqual,
39 Equal,
41 NotEqual,
43}
44
45impl ComparisonOperator {
46 pub fn from_str(s: &str) -> Option<Self> {
48 match s {
49 "<" | "lt" => Some(Self::LessThan),
50 "<=" | "lte" => Some(Self::LessEqual),
51 ">" | "gt" => Some(Self::GreaterThan),
52 ">=" | "gte" => Some(Self::GreaterEqual),
53 "==" | "eq" => Some(Self::Equal),
54 "!=" | "neq" => Some(Self::NotEqual),
55 _ => None,
56 }
57 }
58
59 pub fn symbol(&self) -> &'static str {
61 match self {
62 Self::LessThan => "<",
63 Self::LessEqual => "<=",
64 Self::GreaterThan => ">",
65 Self::GreaterEqual => ">=",
66 Self::Equal => "==",
67 Self::NotEqual => "!=",
68 }
69 }
70
71 pub fn name(&self) -> &'static str {
73 match self {
74 Self::LessThan => "less than",
75 Self::LessEqual => "less than or equal to",
76 Self::GreaterThan => "greater than",
77 Self::GreaterEqual => "greater than or equal to",
78 Self::Equal => "equal to",
79 Self::NotEqual => "not equal to",
80 }
81 }
82}
83
84pub fn validate_cross_field_comparison(
103 input: &Value,
104 left_field: &str,
105 operator: ComparisonOperator,
106 right_field: &str,
107 context_path: Option<&str>,
108) -> Result<()> {
109 let field_path = context_path.unwrap_or("input");
110
111 if let Value::Object(obj) = input {
112 let left_val = obj.get(left_field).ok_or_else(|| FraiseQLError::Validation {
113 message: format!("Field '{}' not found in input", left_field),
114 path: Some(field_path.to_string()),
115 })?;
116
117 let right_val = obj.get(right_field).ok_or_else(|| FraiseQLError::Validation {
118 message: format!("Field '{}' not found in input", right_field),
119 path: Some(field_path.to_string()),
120 })?;
121
122 if matches!(left_val, Value::Null) || matches!(right_val, Value::Null) {
124 return Ok(());
125 }
126
127 compare_values(left_val, right_val, left_field, operator, right_field, field_path)
128 } else {
129 Err(FraiseQLError::Validation {
130 message: "Input is not an object".to_string(),
131 path: Some(field_path.to_string()),
132 })
133 }
134}
135
136fn compare_values(
138 left: &Value,
139 right: &Value,
140 left_field: &str,
141 operator: ComparisonOperator,
142 right_field: &str,
143 context_path: &str,
144) -> Result<()> {
145 let ordering = match (left, right) {
146 (Value::Number(l), Value::Number(r)) => {
148 let l_val = l.as_f64().unwrap_or(0.0);
149 let r_val = r.as_f64().unwrap_or(0.0);
150 if l_val < r_val {
151 Ordering::Less
152 } else if l_val > r_val {
153 Ordering::Greater
154 } else {
155 Ordering::Equal
156 }
157 },
158 (Value::String(l), Value::String(r)) => l.cmp(r),
160 _ => {
162 return Err(FraiseQLError::Validation {
163 message: format!(
164 "Cannot compare '{}' ({}) with '{}' ({})",
165 left_field,
166 value_type_name(left),
167 right_field,
168 value_type_name(right)
169 ),
170 path: Some(context_path.to_string()),
171 });
172 },
173 };
174
175 let result = match operator {
176 ComparisonOperator::LessThan => matches!(ordering, Ordering::Less),
177 ComparisonOperator::LessEqual => !matches!(ordering, Ordering::Greater),
178 ComparisonOperator::GreaterThan => matches!(ordering, Ordering::Greater),
179 ComparisonOperator::GreaterEqual => !matches!(ordering, Ordering::Less),
180 ComparisonOperator::Equal => matches!(ordering, Ordering::Equal),
181 ComparisonOperator::NotEqual => !matches!(ordering, Ordering::Equal),
182 };
183
184 if !result {
185 return Err(FraiseQLError::Validation {
186 message: format!(
187 "'{}' ({}) must be {} '{}' ({})",
188 left_field,
189 value_to_string(left),
190 operator.name(),
191 right_field,
192 value_to_string(right)
193 ),
194 path: Some(context_path.to_string()),
195 });
196 }
197
198 Ok(())
199}
200
201fn value_type_name(val: &Value) -> &'static str {
203 match val {
204 Value::Null => "null",
205 Value::Bool(_) => "boolean",
206 Value::Number(_) => "number",
207 Value::String(_) => "string",
208 Value::Array(_) => "array",
209 Value::Object(_) => "object",
210 }
211}
212
213fn value_to_string(val: &Value) -> String {
215 match val {
216 Value::String(s) => format!("\"{}\"", s),
217 Value::Number(n) => n.to_string(),
218 Value::Bool(b) => b.to_string(),
219 Value::Null => "null".to_string(),
220 _ => val.to_string(),
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use serde_json::json;
227
228 use super::*;
229
230 #[test]
231 fn test_operator_parsing() {
232 assert_eq!(ComparisonOperator::from_str("<"), Some(ComparisonOperator::LessThan));
233 assert_eq!(ComparisonOperator::from_str("lt"), Some(ComparisonOperator::LessThan));
234 assert_eq!(ComparisonOperator::from_str("<="), Some(ComparisonOperator::LessEqual));
235 assert_eq!(ComparisonOperator::from_str("lte"), Some(ComparisonOperator::LessEqual));
236 assert_eq!(ComparisonOperator::from_str(">"), Some(ComparisonOperator::GreaterThan));
237 assert_eq!(ComparisonOperator::from_str("gt"), Some(ComparisonOperator::GreaterThan));
238 assert_eq!(ComparisonOperator::from_str(">="), Some(ComparisonOperator::GreaterEqual));
239 assert_eq!(ComparisonOperator::from_str("gte"), Some(ComparisonOperator::GreaterEqual));
240 assert_eq!(ComparisonOperator::from_str("=="), Some(ComparisonOperator::Equal));
241 assert_eq!(ComparisonOperator::from_str("eq"), Some(ComparisonOperator::Equal));
242 assert_eq!(ComparisonOperator::from_str("!="), Some(ComparisonOperator::NotEqual));
243 assert_eq!(ComparisonOperator::from_str("neq"), Some(ComparisonOperator::NotEqual));
244 assert_eq!(ComparisonOperator::from_str("invalid"), None);
245 }
246
247 #[test]
248 fn test_numeric_less_than() {
249 let input = json!({
250 "start": 10,
251 "end": 20
252 });
253 let result = validate_cross_field_comparison(
254 &input,
255 "start",
256 ComparisonOperator::LessThan,
257 "end",
258 None,
259 );
260 assert!(result.is_ok());
261 }
262
263 #[test]
264 fn test_numeric_less_than_fails() {
265 let input = json!({
266 "start": 30,
267 "end": 20
268 });
269 let result = validate_cross_field_comparison(
270 &input,
271 "start",
272 ComparisonOperator::LessThan,
273 "end",
274 None,
275 );
276 assert!(result.is_err());
277 }
278
279 #[test]
280 fn test_numeric_equal() {
281 let input = json!({
282 "a": 42,
283 "b": 42
284 });
285 let result =
286 validate_cross_field_comparison(&input, "a", ComparisonOperator::Equal, "b", None);
287 assert!(result.is_ok());
288 }
289
290 #[test]
291 fn test_numeric_not_equal() {
292 let input = json!({
293 "a": 10,
294 "b": 20
295 });
296 let result =
297 validate_cross_field_comparison(&input, "a", ComparisonOperator::NotEqual, "b", None);
298 assert!(result.is_ok());
299 }
300
301 #[test]
302 fn test_numeric_greater_than_or_equal() {
303 let input = json!({
304 "min": 10,
305 "max": 10
306 });
307 let result = validate_cross_field_comparison(
308 &input,
309 "max",
310 ComparisonOperator::GreaterEqual,
311 "min",
312 None,
313 );
314 assert!(result.is_ok());
315 }
316
317 #[test]
318 fn test_string_comparison() {
319 let input = json!({
320 "start_name": "alice",
321 "end_name": "zoe"
322 });
323 let result = validate_cross_field_comparison(
324 &input,
325 "start_name",
326 ComparisonOperator::LessThan,
327 "end_name",
328 None,
329 );
330 assert!(result.is_ok());
331 }
332
333 #[test]
334 fn test_string_comparison_fails() {
335 let input = json!({
336 "start_name": "zoe",
337 "end_name": "alice"
338 });
339 let result = validate_cross_field_comparison(
340 &input,
341 "start_name",
342 ComparisonOperator::LessThan,
343 "end_name",
344 None,
345 );
346 assert!(result.is_err());
347 }
348
349 #[test]
350 fn test_date_string_comparison() {
351 let input = json!({
352 "start_date": "2024-01-01",
353 "end_date": "2024-12-31"
354 });
355 let result = validate_cross_field_comparison(
356 &input,
357 "start_date",
358 ComparisonOperator::LessThan,
359 "end_date",
360 None,
361 );
362 assert!(result.is_ok());
363 }
364
365 #[test]
366 fn test_float_comparison() {
367 let input = json!({
368 "price": 19.99,
369 "budget": 25.50
370 });
371 let result = validate_cross_field_comparison(
372 &input,
373 "price",
374 ComparisonOperator::LessThan,
375 "budget",
376 None,
377 );
378 assert!(result.is_ok());
379 }
380
381 #[test]
382 fn test_missing_left_field() {
383 let input = json!({
384 "end": 20
385 });
386 let result = validate_cross_field_comparison(
387 &input,
388 "start",
389 ComparisonOperator::LessThan,
390 "end",
391 None,
392 );
393 assert!(result.is_err());
394 }
395
396 #[test]
397 fn test_missing_right_field() {
398 let input = json!({
399 "start": 10
400 });
401 let result = validate_cross_field_comparison(
402 &input,
403 "start",
404 ComparisonOperator::LessThan,
405 "end",
406 None,
407 );
408 assert!(result.is_err());
409 }
410
411 #[test]
412 fn test_null_fields_skipped() {
413 let input = json!({
414 "start": null,
415 "end": 20
416 });
417 let result = validate_cross_field_comparison(
418 &input,
419 "start",
420 ComparisonOperator::LessThan,
421 "end",
422 None,
423 );
424 assert!(result.is_ok());
425 }
426
427 #[test]
428 fn test_both_null_fields_skipped() {
429 let input = json!({
430 "start": null,
431 "end": null
432 });
433 let result = validate_cross_field_comparison(
434 &input,
435 "start",
436 ComparisonOperator::LessThan,
437 "end",
438 None,
439 );
440 assert!(result.is_ok());
441 }
442
443 #[test]
444 fn test_type_mismatch_error() {
445 let input = json!({
446 "start": 10,
447 "end": "twenty"
448 });
449 let result = validate_cross_field_comparison(
450 &input,
451 "start",
452 ComparisonOperator::LessThan,
453 "end",
454 None,
455 );
456 assert!(result.is_err());
457 if let Err(FraiseQLError::Validation { message, .. }) = result {
458 assert!(message.contains("Cannot compare"));
459 }
460 }
461
462 #[test]
463 fn test_error_includes_context_path() {
464 let input = json!({
465 "start": 30,
466 "end": 20
467 });
468 let result = validate_cross_field_comparison(
469 &input,
470 "start",
471 ComparisonOperator::LessThan,
472 "end",
473 Some("dateRange"),
474 );
475 assert!(result.is_err());
476 if let Err(FraiseQLError::Validation { path, .. }) = result {
477 assert_eq!(path, Some("dateRange".to_string()));
478 }
479 }
480
481 #[test]
482 fn test_error_message_includes_values() {
483 let input = json!({
484 "price": 100,
485 "max_price": 50
486 });
487 let result = validate_cross_field_comparison(
488 &input,
489 "price",
490 ComparisonOperator::LessThan,
491 "max_price",
492 None,
493 );
494 assert!(result.is_err());
495 if let Err(FraiseQLError::Validation { message, .. }) = result {
496 assert!(message.contains("price"));
497 assert!(message.contains("max_price"));
498 assert!(message.contains("100"));
499 assert!(message.contains("50"));
500 }
501 }
502
503 #[test]
504 fn test_all_operators() {
505 let test_cases = vec![
506 (10, 20, ComparisonOperator::LessThan, true),
507 (10, 10, ComparisonOperator::LessEqual, true),
508 (20, 10, ComparisonOperator::GreaterThan, true),
509 (10, 10, ComparisonOperator::GreaterEqual, true),
510 (42, 42, ComparisonOperator::Equal, true),
511 (10, 20, ComparisonOperator::NotEqual, true),
512 (20, 10, ComparisonOperator::LessThan, false),
513 (10, 20, ComparisonOperator::GreaterThan, false),
514 ];
515
516 for (left, right, op, should_pass) in test_cases {
517 let input = json!({ "a": left, "b": right });
518 let result = validate_cross_field_comparison(&input, "a", op, "b", None);
519 assert_eq!(
520 result.is_ok(),
521 should_pass,
522 "Failed for {} {} {}",
523 left,
524 op.symbol(),
525 right
526 );
527 }
528 }
529
530 #[test]
531 fn test_non_object_input() {
532 let input = json!([1, 2, 3]);
533 let result =
534 validate_cross_field_comparison(&input, "a", ComparisonOperator::LessThan, "b", None);
535 assert!(result.is_err());
536 }
537
538 #[test]
539 fn test_empty_object() {
540 let input = json!({});
541 let result = validate_cross_field_comparison(
542 &input,
543 "start",
544 ComparisonOperator::LessThan,
545 "end",
546 None,
547 );
548 assert!(result.is_err());
549 }
550
551 #[test]
552 fn test_zero_comparison() {
553 let input = json!({
554 "a": 0,
555 "b": 0
556 });
557 let result =
558 validate_cross_field_comparison(&input, "a", ComparisonOperator::Equal, "b", None);
559 assert!(result.is_ok());
560 }
561
562 #[test]
563 fn test_negative_number_comparison() {
564 let input = json!({
565 "a": -10,
566 "b": 5
567 });
568 let result =
569 validate_cross_field_comparison(&input, "a", ComparisonOperator::LessThan, "b", None);
570 assert!(result.is_ok());
571 }
572
573 #[test]
574 fn test_empty_string_comparison() {
575 let input = json!({
576 "a": "",
577 "b": "text"
578 });
579 let result =
580 validate_cross_field_comparison(&input, "a", ComparisonOperator::LessThan, "b", None);
581 assert!(result.is_ok());
582 }
583}