1use std::fmt;
60use std::ops;
61use uuid::Uuid;
62
63#[derive(Debug, Clone, PartialEq)]
65pub enum SearchFilterError {
66 ListNotSupportedInOData,
68}
69
70impl fmt::Display for SearchFilterError {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 match self {
73 SearchFilterError::ListNotSupportedInOData => {
74 write!(
75 f,
76 "List values can only be used with search.in operations, not as direct OData literals"
77 )
78 }
79 }
80 }
81}
82
83impl std::error::Error for SearchFilterError {}
84
85#[derive(Debug, Clone, PartialEq)]
89pub enum SearchFilterValue {
90 Str(String),
91 Bool(bool),
92 Int(i64),
93 Float(f64),
94 List(Vec<String>),
95 Null,
96}
97
98impl SearchFilterValue {
99 pub fn null() -> Self {
101 SearchFilterValue::Null
102 }
103
104 pub fn to_odata(&self) -> Result<String, SearchFilterError> {
125 match self {
126 SearchFilterValue::Str(s) => Ok(format!("'{}'", s.replace('\'', "''"))),
127 SearchFilterValue::Bool(b) => Ok(b.to_string()),
128 SearchFilterValue::Int(i) => Ok(i.to_string()),
129 SearchFilterValue::Float(f) => Ok(f.to_string()),
130 SearchFilterValue::List(_) => Err(SearchFilterError::ListNotSupportedInOData),
131 SearchFilterValue::Null => Ok("null".to_string()),
132 }
133 }
134}
135
136impl From<Uuid> for SearchFilterValue {
137 fn from(id: Uuid) -> Self {
138 SearchFilterValue::Str(id.to_string())
139 }
140}
141
142impl From<String> for SearchFilterValue {
143 fn from(val: String) -> Self {
144 SearchFilterValue::Str(val)
145 }
146}
147
148impl From<&str> for SearchFilterValue {
149 fn from(val: &str) -> Self {
150 SearchFilterValue::Str(val.to_string())
151 }
152}
153
154impl From<bool> for SearchFilterValue {
155 fn from(val: bool) -> Self {
156 SearchFilterValue::Bool(val)
157 }
158}
159
160impl From<i64> for SearchFilterValue {
161 fn from(val: i64) -> Self {
162 SearchFilterValue::Int(val)
163 }
164}
165
166impl From<i32> for SearchFilterValue {
167 fn from(val: i32) -> Self {
168 SearchFilterValue::Int(val as i64)
169 }
170}
171
172impl From<f64> for SearchFilterValue {
173 fn from(val: f64) -> Self {
174 SearchFilterValue::Float(val)
175 }
176}
177
178impl From<f32> for SearchFilterValue {
179 fn from(val: f32) -> Self {
180 SearchFilterValue::Float(val as f64)
181 }
182}
183
184impl From<Vec<Uuid>> for SearchFilterValue {
185 fn from(ids: Vec<Uuid>) -> Self {
186 SearchFilterValue::List(ids.into_iter().map(|u| u.to_string()).collect())
187 }
188}
189
190#[derive(Debug, Clone, PartialEq)]
194pub enum SearchFilter {
195 Eq(String, SearchFilterValue),
196 Ne(String, SearchFilterValue),
197 Gt(String, SearchFilterValue),
198 Lt(String, SearchFilterValue),
199 Ge(String, SearchFilterValue),
200 Le(String, SearchFilterValue),
201 In(String, Vec<String>),
202 InWithDelimiter(String, Vec<String>, String),
204 And(Box<SearchFilter>, Box<SearchFilter>),
205 Or(Box<SearchFilter>, Box<SearchFilter>),
206 Not(Box<SearchFilter>),
207 Parentheses(Box<SearchFilter>),
209 Raw(String),
210 Field(String),
212 Any(String, Option<Box<SearchFilter>>),
214 All(String, Box<SearchFilter>),
216}
217
218impl SearchFilter {
219 pub fn eq<T: Into<SearchFilterValue>>(field: impl Into<String>, value: T) -> Self {
221 SearchFilter::Eq(field.into(), value.into())
222 }
223
224 pub fn ne<T: Into<SearchFilterValue>>(field: impl Into<String>, value: T) -> Self {
226 SearchFilter::Ne(field.into(), value.into())
227 }
228
229 pub fn gt<T: Into<SearchFilterValue>>(field: impl Into<String>, value: T) -> Self {
231 SearchFilter::Gt(field.into(), value.into())
232 }
233
234 pub fn lt<T: Into<SearchFilterValue>>(field: impl Into<String>, value: T) -> Self {
236 SearchFilter::Lt(field.into(), value.into())
237 }
238
239 pub fn ge<T: Into<SearchFilterValue>>(field: impl Into<String>, value: T) -> Self {
241 SearchFilter::Ge(field.into(), value.into())
242 }
243
244 pub fn le<T: Into<SearchFilterValue>>(field: impl Into<String>, value: T) -> Self {
246 SearchFilter::Le(field.into(), value.into())
247 }
248
249 pub fn search_in(field: impl Into<String>, values: Vec<String>) -> Self {
251 SearchFilter::In(field.into(), values)
252 }
253
254 pub fn search_in_with_delimiter(
256 field: impl Into<String>,
257 values: Vec<String>,
258 delimiter: &str,
259 ) -> Self {
260 SearchFilter::InWithDelimiter(field.into(), values, delimiter.to_string())
261 }
262
263 pub fn and(self, other: SearchFilter) -> Self {
265 SearchFilter::And(Box::new(self), Box::new(other))
266 }
267
268 pub fn or(self, other: SearchFilter) -> Self {
270 SearchFilter::Or(Box::new(self), Box::new(other))
271 }
272
273 #[allow(clippy::should_implement_trait)] pub fn not(self) -> Self {
276 SearchFilter::Not(Box::new(self))
277 }
278
279 pub fn parentheses(self) -> Self {
281 SearchFilter::Parentheses(Box::new(self))
282 }
283
284 pub fn raw(expr: impl Into<String>) -> Self {
286 SearchFilter::Raw(expr.into())
287 }
288
289 pub fn field(field_name: impl Into<String>) -> Self {
291 SearchFilter::Field(field_name.into())
292 }
293
294 pub fn any(collection_path: impl Into<String>) -> Self {
296 SearchFilter::Any(collection_path.into(), None)
297 }
298
299 pub fn any_with_filter(collection_path: impl Into<String>, filter: SearchFilter) -> Self {
301 SearchFilter::Any(collection_path.into(), Some(Box::new(filter)))
302 }
303
304 pub fn all(collection_path: impl Into<String>, filter: SearchFilter) -> Self {
306 SearchFilter::All(collection_path.into(), Box::new(filter))
307 }
308
309 pub fn to_odata(&self) -> Result<String, SearchFilterError> {
316 self.to_odata_internal()
317 }
318
319 fn format_operand(filter: &SearchFilter) -> Result<String, SearchFilterError> {
323 match filter {
324 SearchFilter::Parentheses(_)
326 | SearchFilter::Raw(_)
327 | SearchFilter::Field(_)
328 | SearchFilter::Eq(_, _)
329 | SearchFilter::Ne(_, _)
330 | SearchFilter::Gt(_, _)
331 | SearchFilter::Lt(_, _)
332 | SearchFilter::Ge(_, _)
333 | SearchFilter::Le(_, _)
334 | SearchFilter::In(_, _)
335 | SearchFilter::InWithDelimiter(_, _, _)
336 | SearchFilter::Any(_, _)
337 | SearchFilter::All(_, _) => filter.to_odata_internal(),
338 _ => Ok(format!("({})", filter.to_odata_internal()?)),
340 }
341 }
342
343 fn to_odata_internal(&self) -> Result<String, SearchFilterError> {
344 match self {
345 SearchFilter::Eq(f, v) => Ok(format!("{} eq {}", f, v.to_odata()?)),
347 SearchFilter::Ne(f, v) => Ok(format!("{} ne {}", f, v.to_odata()?)),
348 SearchFilter::Gt(f, v) => Ok(format!("{} gt {}", f, v.to_odata()?)),
349 SearchFilter::Lt(f, v) => Ok(format!("{} lt {}", f, v.to_odata()?)),
350 SearchFilter::Ge(f, v) => Ok(format!("{} ge {}", f, v.to_odata()?)),
351 SearchFilter::Le(f, v) => Ok(format!("{} le {}", f, v.to_odata()?)),
352 SearchFilter::In(f, vs) => Ok(format!("search.in({}, '{}', ',')", f, vs.join(","))),
353 SearchFilter::InWithDelimiter(f, vs, delimiter) => Ok(format!(
354 "search.in({}, '{}', '{}')",
355 f,
356 vs.join(delimiter),
357 delimiter
358 )),
359
360 SearchFilter::And(a, b) => {
362 let left = SearchFilter::format_operand(a.as_ref())?;
363 let right = SearchFilter::format_operand(b.as_ref())?;
364 Ok(format!("{} and {}", left, right))
365 }
366 SearchFilter::Or(a, b) => {
367 let left = SearchFilter::format_operand(a.as_ref())?;
368 let right = SearchFilter::format_operand(b.as_ref())?;
369 Ok(format!("{} or {}", left, right))
370 }
371
372 SearchFilter::Not(i) => Ok(format!("not ({})", i.to_odata_internal()?)),
374
375 SearchFilter::Parentheses(f) => Ok(format!("({})", f.to_odata_internal()?)),
377
378 SearchFilter::Raw(s) => Ok(s.clone()),
380 SearchFilter::Field(f) => Ok(f.clone()),
381 SearchFilter::Any(f, Some(filter)) => {
382 Ok(format!("{}/any({})", f, filter.to_odata_internal()?))
383 }
384 SearchFilter::Any(f, None) => Ok(format!("{}/any()", f)),
385 SearchFilter::All(f, filter) => {
386 Ok(format!("{}/all({})", f, filter.to_odata_internal()?))
387 }
388 }
389 }
390}
391
392impl fmt::Display for SearchFilter {
393 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
394 match self.to_odata() {
395 Ok(odata) => write!(f, "{}", odata),
396 Err(err) => write!(f, "Error: {}", err),
397 }
398 }
399}
400
401impl ops::Not for SearchFilter {
402 type Output = Self;
403
404 fn not(self) -> Self::Output {
405 SearchFilter::Not(Box::new(self))
406 }
407}
408
409impl ops::BitAnd for SearchFilter {
410 type Output = Self;
411
412 fn bitand(self, rhs: Self) -> Self::Output {
413 SearchFilter::Parentheses(Box::new(SearchFilter::And(Box::new(self), Box::new(rhs))))
414 }
415}
416
417impl ops::BitOr for SearchFilter {
418 type Output = Self;
419
420 fn bitor(self, rhs: Self) -> Self::Output {
421 SearchFilter::Parentheses(Box::new(SearchFilter::Or(Box::new(self), Box::new(rhs))))
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 #[test]
430 fn value_to_odata_literals() -> Result<(), SearchFilterError> {
431 assert_eq!(
432 SearchFilterValue::Str("o'neil".into()).to_odata()?,
433 "'o''neil'"
434 );
435 assert_eq!(SearchFilterValue::Int(123).to_odata()?, "123");
436 assert_eq!(SearchFilterValue::Bool(true).to_odata()?, "true");
437 assert_eq!(SearchFilterValue::Float(3.5).to_odata()?, "3.5");
438 assert_eq!(SearchFilterValue::Null.to_odata()?, "null");
439 Ok(())
440 }
441
442 #[test]
443 fn list_value_error() {
444 let list_value = SearchFilterValue::List(vec!["a".to_string(), "b".to_string()]);
445 let result = list_value.to_odata();
446 assert!(result.is_err());
447 assert_eq!(
448 result.unwrap_err(),
449 SearchFilterError::ListNotSupportedInOData
450 );
451 }
452
453 #[test]
454 fn uuid_into_value() -> Result<(), SearchFilterError> {
455 let id = Uuid::parse_str("11111111-2222-3333-4444-555555555555").unwrap();
456 assert_eq!(
457 SearchFilterValue::from(id).to_odata()?,
458 "'11111111-2222-3333-4444-555555555555'"
459 );
460 Ok(())
461 }
462
463 #[test]
464 fn simple_comparisons() {
465 let f = SearchFilter::eq("a", "x");
466 assert_eq!(f.to_string(), "a eq 'x'");
467 let f = SearchFilter::gt("n", 10);
468 assert_eq!(f.to_string(), "n gt 10");
469 }
470
471 #[test]
472 fn logical_combinations() -> Result<(), SearchFilterError> {
473 let a = SearchFilter::eq("x", 1);
474 let b = SearchFilter::ne("y", false);
475 let c = a.clone().and(b.clone());
476 assert_eq!(c.to_odata()?, "x eq 1 and y ne false");
477 let d = c.or(SearchFilter::raw(
478 "geo.distance(loc, geography'POINT(0 0)') le 5",
479 ));
480 assert_eq!(
481 d.to_string(),
482 "(x eq 1 and y ne false) or geo.distance(loc, geography'POINT(0 0)') le 5"
483 );
484 Ok(())
485 }
486
487 #[test]
488 fn search_in_multiple_values() -> Result<(), SearchFilterError> {
489 let vals = vec!["a".into(), "b".into(), "c".into()];
490 let f = SearchFilter::search_in("tags", vals);
491 assert_eq!(f.to_odata()?, "search.in(tags, 'a,b,c', ',')");
492 Ok(())
493 }
494
495 #[test]
496 fn not_operator() {
497 let f = SearchFilter::eq("status", "open").not();
498 assert_eq!(f.to_string(), "not (status eq 'open')");
499 }
500
501 #[test]
502 fn not_operator_trait() -> Result<(), SearchFilterError> {
503 let filter = SearchFilter::eq("status", "open");
505 let method_result = filter.clone().not();
506 let operator_result = !filter;
507
508 assert_eq!(method_result.to_odata()?, "not (status eq 'open')");
509 assert_eq!(operator_result.to_odata()?, "not (status eq 'open')");
510 assert_eq!(method_result.to_odata()?, operator_result.to_odata()?);
511 Ok(())
512 }
513
514 #[test]
515 fn bitand_operator_trait() -> Result<(), SearchFilterError> {
516 let left = SearchFilter::eq("status", "active");
518 let right = SearchFilter::gt("rating", 4);
519
520 let method_result = left.clone().and(right.clone());
521 let operator_result = left & right;
522
523 assert_eq!(
524 method_result.to_odata()?,
525 "status eq 'active' and rating gt 4"
526 );
527 assert_eq!(
528 operator_result.to_odata()?,
529 "(status eq 'active' and rating gt 4)"
530 );
531 Ok(())
533 }
534
535 #[test]
536 fn bitor_operator_trait() -> Result<(), SearchFilterError> {
537 let left = SearchFilter::eq("category", "luxury");
539 let right = SearchFilter::eq("parking", true);
540
541 let method_result = left.clone().or(right.clone());
542 let operator_result = left | right;
543
544 assert_eq!(
545 method_result.to_odata()?,
546 "category eq 'luxury' or parking eq true"
547 );
548 assert_eq!(
549 operator_result.to_odata()?,
550 "(category eq 'luxury' or parking eq true)"
551 );
552 Ok(())
554 }
555
556 #[test]
557 fn combined_operators() -> Result<(), SearchFilterError> {
558 let a = SearchFilter::eq("deleted", true);
560 let b = SearchFilter::eq("status", "active");
561 let c = SearchFilter::eq("featured", true);
562
563 let result = !a & b | c;
564 assert_eq!(
565 result.to_odata()?,
566 "(((not (deleted eq true)) and status eq 'active') or featured eq true)"
567 );
568 Ok(())
569 }
570
571 #[test]
572 fn operator_precedence_demonstration() -> Result<(), SearchFilterError> {
573 let a = SearchFilter::eq("a", 1);
576 let b = SearchFilter::eq("b", 2);
577 let c = SearchFilter::eq("c", 3);
578
579 let result = a | b & c;
580 assert_eq!(result.to_odata()?, "(a eq 1 or (b eq 2 and c eq 3))");
581
582 let a = SearchFilter::eq("a", 1);
584 let b = SearchFilter::eq("b", 2);
585 let c = SearchFilter::eq("c", 3);
586
587 let result = (a | b) & c;
588 assert_eq!(result.to_odata()?, "((a eq 1 or b eq 2) and c eq 3)");
589 Ok(())
590 }
591
592 #[test]
593 fn nested_mix() -> Result<(), SearchFilterError> {
594 let f = SearchFilter::eq("id", "abc")
595 .and(SearchFilter::search_in("cat", vec!["x".into(), "y".into()]))
596 .or(SearchFilter::raw("search.ismatchscoring('foo')"));
597 assert_eq!(
598 f.to_odata()?,
599 "(id eq 'abc' and search.in(cat, 'x,y', ',')) or search.ismatchscoring('foo')"
600 );
601 Ok(())
602 }
603
604 #[test]
605 fn null_comparisons() -> Result<(), SearchFilterError> {
606 let f = SearchFilter::eq("Description", SearchFilterValue::null());
607 assert_eq!(f.to_odata()?, "Description eq null");
608
609 let f = SearchFilter::ne("Title", SearchFilterValue::null());
610 assert_eq!(f.to_odata()?, "Title ne null");
611 Ok(())
612 }
613
614 #[test]
615 fn boolean_field_expressions() -> Result<(), SearchFilterError> {
616 let f = SearchFilter::field("IsEnabled");
617 assert_eq!(f.to_odata()?, "IsEnabled");
618
619 let f = SearchFilter::field("ParkingIncluded").and(SearchFilter::eq("Rating", 5));
620 assert_eq!(f.to_odata()?, "ParkingIncluded and Rating eq 5");
621 Ok(())
622 }
623
624 #[test]
625 fn collection_any_operator() -> Result<(), SearchFilterError> {
626 let f = SearchFilter::any("Rooms");
628 assert_eq!(f.to_odata()?, "Rooms/any()");
629
630 let f = SearchFilter::any_with_filter(
632 "Rooms",
633 SearchFilter::raw("room: room/BaseRate lt 200.0"),
634 );
635 assert_eq!(f.to_odata()?, "Rooms/any(room: room/BaseRate lt 200.0)");
636
637 let f = SearchFilter::any_with_filter(
639 "Rooms",
640 SearchFilter::raw("room: room/BaseRate lt 200.0"),
641 )
642 .and(SearchFilter::ge("Rating", 4));
643 assert_eq!(
644 f.to_odata()?,
645 "Rooms/any(room: room/BaseRate lt 200.0) and Rating ge 4"
646 );
647 Ok(())
648 }
649
650 #[test]
651 fn collection_all_operator() -> Result<(), SearchFilterError> {
652 let f = SearchFilter::all("Rooms", SearchFilter::raw("room: not room/SmokingAllowed"));
654 assert_eq!(f.to_odata()?, "Rooms/all(room: not room/SmokingAllowed)");
655
656 let f = SearchFilter::field("ParkingIncluded").and(SearchFilter::all(
658 "Rooms",
659 SearchFilter::raw("room: not room/SmokingAllowed"),
660 ));
661 assert_eq!(
662 f.to_odata()?,
663 "ParkingIncluded and Rooms/all(room: not room/SmokingAllowed)"
664 );
665 Ok(())
666 }
667
668 #[test]
669 fn operator_precedence_examples() -> Result<(), SearchFilterError> {
670 let f = SearchFilter::gt("Rating", 0)
673 .and(SearchFilter::lt("Rating", 3))
674 .or(SearchFilter::gt("Rating", 7).and(SearchFilter::lt("Rating", 10)));
675 assert_eq!(
676 f.to_odata()?,
677 "(Rating gt 0 and Rating lt 3) or (Rating gt 7 and Rating lt 10)"
678 );
679 Ok(())
680 }
681
682 #[test]
683 fn not_operator_precedence() -> Result<(), SearchFilterError> {
684 let f = SearchFilter::gt("Rating", 5).not();
686 assert_eq!(f.to_odata()?, "not (Rating gt 5)");
687
688 let f = SearchFilter::any("Rooms").not();
690 assert_eq!(f.to_odata()?, "not (Rooms/any())");
691 Ok(())
692 }
693
694 #[test]
695 fn complex_real_world_examples() -> Result<(), SearchFilterError> {
696 let f = SearchFilter::eq("Category", "Luxury")
699 .or(SearchFilter::eq("ParkingIncluded", true))
700 .and(SearchFilter::eq("Rating", 5));
701 assert_eq!(
702 f.to_odata()?,
703 "(Category eq 'Luxury' or ParkingIncluded eq true) and Rating eq 5"
704 );
705
706 let f = SearchFilter::ne("HotelName", "Sea View Motel").and(SearchFilter::ge(
708 "LastRenovationDate",
709 "2010-01-01T00:00:00Z",
710 ));
711 assert_eq!(
712 f.to_odata()?,
713 "HotelName ne 'Sea View Motel' and LastRenovationDate ge '2010-01-01T00:00:00Z'"
714 );
715 Ok(())
716 }
717
718 #[test]
719 fn search_in_with_different_delimiters() -> Result<(), SearchFilterError> {
720 let f = SearchFilter::search_in(
722 "HotelName",
723 vec!["Sea View motel".into(), "Budget hotel".into()],
724 );
725 assert_eq!(
726 f.to_odata()?,
727 "search.in(HotelName, 'Sea View motel,Budget hotel', ',')"
728 );
729
730 let f = SearchFilter::search_in_with_delimiter(
732 "HotelName",
733 vec!["Sea View motel".into(), "Budget hotel".into()],
734 "|",
735 );
736 assert_eq!(
737 f.to_odata()?,
738 "search.in(HotelName, 'Sea View motel|Budget hotel', '|')"
739 );
740
741 let vals = vec!["heated towel racks".into(), "hairdryer included".into()];
743 let f = SearchFilter::search_in("Tags", vals);
744 assert_eq!(
745 f.to_odata()?,
746 "search.in(Tags, 'heated towel racks,hairdryer included', ',')"
747 );
748 Ok(())
749 }
750
751 #[test]
752 fn documentation_examples_comprehensive() -> Result<(), SearchFilterError> {
753 let f = SearchFilter::any_with_filter(
755 "Rooms",
756 SearchFilter::raw("room: room/BaseRate lt 200.0"),
757 )
758 .and(SearchFilter::ge("Rating", 4));
759 assert_eq!(
760 f.to_odata()?,
761 "Rooms/any(room: room/BaseRate lt 200.0) and Rating ge 4"
762 );
763
764 let f = SearchFilter::ne("HotelName", "Sea View Motel").and(SearchFilter::ge(
766 "LastRenovationDate",
767 "2010-01-01T00:00:00Z",
768 ));
769 assert_eq!(
770 f.to_odata()?,
771 "HotelName ne 'Sea View Motel' and LastRenovationDate ge '2010-01-01T00:00:00Z'"
772 );
773
774 let f = SearchFilter::ge("LastRenovationDate", "2010-01-01T00:00:00-08:00");
776 assert_eq!(
777 f.to_odata()?,
778 "LastRenovationDate ge '2010-01-01T00:00:00-08:00'"
779 );
780
781 let f = SearchFilter::field("ParkingIncluded").and(SearchFilter::all(
783 "Rooms",
784 SearchFilter::raw("room: not room/SmokingAllowed"),
785 ));
786 assert_eq!(
787 f.to_odata()?,
788 "ParkingIncluded and Rooms/all(room: not room/SmokingAllowed)"
789 );
790
791 let f = SearchFilter::eq("ParkingIncluded", true).and(SearchFilter::all(
793 "Rooms",
794 SearchFilter::raw("room: room/SmokingAllowed eq false"),
795 ));
796 assert_eq!(
797 f.to_odata()?,
798 "ParkingIncluded eq true and Rooms/all(room: room/SmokingAllowed eq false)"
799 );
800
801 let f = SearchFilter::eq("Category", "Luxury")
805 .or(SearchFilter::eq("ParkingIncluded", true))
806 .and(SearchFilter::eq("Rating", 5));
807 assert_eq!(
808 f.to_odata()?,
809 "(Category eq 'Luxury' or ParkingIncluded eq true) and Rating eq 5"
810 );
811 Ok(())
812 }
813
814 #[test]
815 fn nested_collection_operations() -> Result<(), SearchFilterError> {
816 let f = SearchFilter::any_with_filter(
818 "Rooms",
819 SearchFilter::raw("room: room/Tags/any(tag: tag eq 'wifi')"),
820 );
821 assert_eq!(
822 f.to_odata()?,
823 "Rooms/any(room: room/Tags/any(tag: tag eq 'wifi'))"
824 );
825
826 let f = SearchFilter::any("Rooms");
828 assert_eq!(f.to_odata()?, "Rooms/any()");
829
830 let f = SearchFilter::any("Rooms").not();
832 assert_eq!(f.to_odata()?, "not (Rooms/any())");
833
834 let f = SearchFilter::any_with_filter(
836 "Rooms",
837 SearchFilter::raw("room: room/Tags/any(tag: search.in(tag, 'wifi, tub'))"),
838 );
839 assert_eq!(
840 f.to_odata()?,
841 "Rooms/any(room: room/Tags/any(tag: search.in(tag, 'wifi, tub')))"
842 );
843 Ok(())
844 }
845
846 #[test]
847 fn geospatial_examples() -> Result<(), SearchFilterError> {
848 let f = SearchFilter::raw(
850 "geo.distance(Location, geography'POINT(-122.131577 47.678581)') le 10",
851 );
852 assert_eq!(
853 f.to_odata()?,
854 "geo.distance(Location, geography'POINT(-122.131577 47.678581)') le 10"
855 );
856
857 let f = SearchFilter::raw(
859 "geo.intersects(Location, geography'POLYGON((-122.031577 47.578581, -122.031577 47.678581, -122.131577 47.678581, -122.031577 47.578581))')",
860 );
861 assert_eq!(
862 f.to_odata()?,
863 "geo.intersects(Location, geography'POLYGON((-122.031577 47.578581, -122.031577 47.678581, -122.131577 47.678581, -122.031577 47.578581))')"
864 );
865 Ok(())
866 }
867
868 #[test]
869 fn full_text_search_examples() -> Result<(), SearchFilterError> {
870 let f = SearchFilter::raw("search.ismatchscoring('waterfront')");
872 assert_eq!(f.to_odata()?, "search.ismatchscoring('waterfront')");
873
874 let f = SearchFilter::raw("search.ismatchscoring('hostel')")
876 .and(SearchFilter::ge("rating", 4))
877 .or(SearchFilter::raw("search.ismatchscoring('motel')")
878 .and(SearchFilter::eq("rating", 5)));
879 assert_eq!(
880 f.to_odata()?,
881 "(search.ismatchscoring('hostel') and rating ge 4) or (search.ismatchscoring('motel') and rating eq 5)"
882 );
883
884 let f = SearchFilter::raw("search.ismatch('luxury')").not();
886 assert_eq!(f.to_odata()?, "not (search.ismatch('luxury'))");
887
888 let f =
890 SearchFilter::raw("search.ismatchscoring('\"ocean view\"', 'Description,HotelName')")
891 .or(SearchFilter::eq("Rating", 5));
892 assert_eq!(
893 f.to_odata()?,
894 "search.ismatchscoring('\"ocean view\"', 'Description,HotelName') or Rating eq 5"
895 );
896
897 let f = SearchFilter::raw(
899 "search.ismatch('\"hotel airport\"~5', 'Description', 'full', 'any')",
900 )
901 .and(
902 SearchFilter::any_with_filter("Rooms", SearchFilter::raw("room: room/SmokingAllowed"))
903 .not(),
904 );
905 assert_eq!(
906 f.to_odata()?,
907 "search.ismatch('\"hotel airport\"~5', 'Description', 'full', 'any') and (not (Rooms/any(room: room/SmokingAllowed)))"
908 );
909
910 let f = SearchFilter::raw("search.ismatch('lux*', 'Description')");
912 assert_eq!(f.to_odata()?, "search.ismatch('lux*', 'Description')");
913 Ok(())
914 }
915
916 #[test]
917 fn operator_precedence_documentation_examples() -> Result<(), SearchFilterError> {
918 let f = SearchFilter::gt("Rating", 0)
921 .and(SearchFilter::lt("Rating", 3))
922 .or(SearchFilter::gt("Rating", 7).and(SearchFilter::lt("Rating", 10)));
923 assert_eq!(
924 f.to_odata()?,
925 "(Rating gt 0 and Rating lt 3) or (Rating gt 7 and Rating lt 10)"
926 );
927
928 let f = SearchFilter::eq("A", 1).or(SearchFilter::eq("B", 2).and(SearchFilter::eq("C", 3)));
930 assert_eq!(f.to_odata()?, "A eq 1 or (B eq 2 and C eq 3)");
931 Ok(())
932 }
933
934 #[test]
935 fn explicit_parentheses_for_precedence_override() -> Result<(), SearchFilterError> {
936 let f = SearchFilter::eq("Category", "Luxury")
939 .or(SearchFilter::eq("ParkingIncluded", true))
940 .parentheses()
941 .and(SearchFilter::eq("Rating", 5));
942 assert_eq!(
943 f.to_odata()?,
944 "(Category eq 'Luxury' or ParkingIncluded eq true) and Rating eq 5"
945 );
946
947 let f_no_parens = SearchFilter::eq("Category", "Luxury")
949 .or(SearchFilter::eq("ParkingIncluded", true))
950 .and(SearchFilter::eq("Rating", 5));
951 assert_eq!(
952 f_no_parens.to_odata()?,
953 "(Category eq 'Luxury' or ParkingIncluded eq true) and Rating eq 5"
954 );
955 Ok(())
956 }
957
958 #[test]
959 fn odata_specification_compliance() -> Result<(), SearchFilterError> {
960 let filter = SearchFilter::ne("HotelName", "Sea View Motel").and(SearchFilter::ge(
965 "LastRenovationDate",
966 "2010-01-01T00:00:00Z",
967 ));
968 assert_eq!(
969 filter.to_odata()?,
970 "HotelName ne 'Sea View Motel' and LastRenovationDate ge '2010-01-01T00:00:00Z'"
971 );
972
973 let filter = SearchFilter::eq("Category", "Luxury")
977 .or(SearchFilter::eq("ParkingIncluded", true))
978 .and(SearchFilter::eq("Rating", 5));
979 assert_eq!(
980 filter.to_odata()?,
981 "(Category eq 'Luxury' or ParkingIncluded eq true) and Rating eq 5"
982 );
983
984 let filter = SearchFilter::field("ParkingIncluded").and(SearchFilter::all(
987 "Rooms",
988 SearchFilter::raw("room: not room/SmokingAllowed"),
989 ));
990 assert_eq!(
991 filter.to_odata()?,
992 "ParkingIncluded and Rooms/all(room: not room/SmokingAllowed)"
993 );
994
995 let filter = SearchFilter::any("Rooms").not();
998 assert_eq!(filter.to_odata()?, "not (Rooms/any())");
999
1000 let filter = SearchFilter::eq("Description", SearchFilterValue::null());
1003 assert_eq!(filter.to_odata()?, "Description eq null");
1004
1005 let filter = SearchFilter::search_in(
1008 "HotelName",
1009 vec!["Sea View motel".into(), "Budget hotel".into()],
1010 );
1011 assert_eq!(
1012 filter.to_odata()?,
1013 "search.in(HotelName, 'Sea View motel,Budget hotel', ',')"
1014 );
1015
1016 let filter = SearchFilter::gt("Rating", 0)
1020 .and(SearchFilter::lt("Rating", 3))
1021 .or(SearchFilter::gt("Rating", 7).and(SearchFilter::lt("Rating", 10)));
1022 assert_eq!(
1023 filter.to_odata()?,
1024 "(Rating gt 0 and Rating lt 3) or (Rating gt 7 and Rating lt 10)"
1025 );
1026 Ok(())
1027 }
1028}