1use std::borrow::Cow;
2use std::cmp::Ordering;
3use std::fmt::{Debug, Display};
4
5use crate::case_sensitivity::{
6 caseless_contains, caseless_ends_with, caseless_eq, caseless_starts_with,
7};
8
9#[cfg(feature = "secrecy")]
10use secrecy::ExposeSecret;
11
12pub trait Filterable {
48 fn get(&self, key: &str) -> FilterValue<'_>;
59}
60
61#[derive(Clone, Default)]
99pub enum FilterValue<'a> {
100 #[default]
102 Null,
103 Bool(bool),
105 Number(f64),
107 String(Cow<'a, str>),
113 Tuple(Vec<FilterValue<'a>>),
115 #[cfg(feature = "secrecy")]
118 Secret(secrecy::SecretString),
119 #[cfg(feature = "chrono")]
124 DateTime(chrono::DateTime<chrono::Utc>),
125 #[cfg(feature = "chrono")]
130 Duration(chrono::Duration),
131}
132
133impl<'a> FilterValue<'a> {
134 #[cfg(feature = "secrecy")]
160 pub fn secret(value: impl Into<String>) -> Self {
161 FilterValue::Secret(secrecy::SecretString::from(value.into()))
162 }
163
164 pub fn into_owned(self) -> FilterValue<'static> {
196 match self {
197 FilterValue::Null => FilterValue::Null,
198 FilterValue::Bool(b) => FilterValue::Bool(b),
199 FilterValue::Number(n) => FilterValue::Number(n),
200 FilterValue::String(s) => FilterValue::String(Cow::Owned(s.into_owned())),
201 FilterValue::Tuple(v) => {
202 FilterValue::Tuple(v.into_iter().map(FilterValue::into_owned).collect())
203 }
204 #[cfg(feature = "secrecy")]
205 FilterValue::Secret(s) => FilterValue::Secret(s),
206 #[cfg(feature = "chrono")]
207 FilterValue::DateTime(d) => FilterValue::DateTime(d),
208 #[cfg(feature = "chrono")]
209 FilterValue::Duration(d) => FilterValue::Duration(d),
210 }
211 }
212
213 pub fn is_truthy(&self) -> bool {
231 match self {
232 FilterValue::Null => false,
233 FilterValue::Bool(b) => *b,
234 FilterValue::Number(n) => *n != 0.0,
235 FilterValue::String(s) => !s.is_empty(),
236 FilterValue::Tuple(v) => !v.is_empty(),
237 #[cfg(feature = "secrecy")]
238 FilterValue::Secret(s) => !s.expose_secret().is_empty(),
239 #[cfg(feature = "chrono")]
240 FilterValue::DateTime(..) => true,
241 #[cfg(feature = "chrono")]
242 FilterValue::Duration(d) => !d.is_zero(),
243 }
244 }
245
246 pub fn contains(&self, other: &FilterValue<'a>) -> bool {
271 match (self, other) {
272 (FilterValue::Tuple(a), b) => a.iter().any(|ai| ai == b),
273 (FilterValue::String(a), FilterValue::String(b)) => caseless_contains(a, b),
274 #[cfg(feature = "secrecy")]
275 (FilterValue::Secret(a), FilterValue::Secret(b)) => {
276 caseless_contains(a.expose_secret(), b.expose_secret())
277 }
278 #[cfg(feature = "secrecy")]
279 (FilterValue::Secret(a), FilterValue::String(b)) => {
280 caseless_contains(a.expose_secret(), b)
281 }
282 #[cfg(feature = "secrecy")]
283 (FilterValue::String(a), FilterValue::Secret(b)) => {
284 caseless_contains(a, b.expose_secret())
285 }
286 _ => false,
287 }
288 }
289
290 pub fn startswith(&self, other: &FilterValue<'a>) -> bool {
308 match (self, other) {
309 (FilterValue::Tuple(a), b) => a.iter().any(|ai| ai == b),
310 (FilterValue::String(a), FilterValue::String(b)) => caseless_starts_with(a, b),
311 #[cfg(feature = "secrecy")]
312 (FilterValue::Secret(a), FilterValue::Secret(b)) => {
313 caseless_starts_with(a.expose_secret(), b.expose_secret())
314 }
315 #[cfg(feature = "secrecy")]
316 (FilterValue::Secret(a), FilterValue::String(b)) => {
317 caseless_starts_with(a.expose_secret(), b)
318 }
319 #[cfg(feature = "secrecy")]
320 (FilterValue::String(a), FilterValue::Secret(b)) => {
321 caseless_starts_with(a, b.expose_secret())
322 }
323 _ => false,
324 }
325 }
326
327 pub fn endswith(&self, other: &FilterValue<'a>) -> bool {
345 match (self, other) {
346 (FilterValue::Tuple(a), b) => a.iter().any(|ai| ai == b),
347 (FilterValue::String(a), FilterValue::String(b)) => caseless_ends_with(a, b),
348 #[cfg(feature = "secrecy")]
349 (FilterValue::Secret(a), FilterValue::Secret(b)) => {
350 caseless_ends_with(a.expose_secret(), b.expose_secret())
351 }
352 #[cfg(feature = "secrecy")]
353 (FilterValue::Secret(a), FilterValue::String(b)) => {
354 caseless_ends_with(a.expose_secret(), b)
355 }
356 #[cfg(feature = "secrecy")]
357 (FilterValue::String(a), FilterValue::Secret(b)) => {
358 caseless_ends_with(a, b.expose_secret())
359 }
360 _ => false,
361 }
362 }
363
364 pub fn eq_cs(&self, other: &FilterValue<'a>) -> bool {
381 match (self, other) {
382 (FilterValue::String(a), FilterValue::String(b)) => a == b,
383 #[cfg(feature = "secrecy")]
384 (FilterValue::Secret(a), FilterValue::Secret(b)) => {
385 a.expose_secret() == b.expose_secret()
386 }
387 #[cfg(feature = "secrecy")]
388 (FilterValue::Secret(a), FilterValue::String(b)) => a.expose_secret() == b,
389 #[cfg(feature = "secrecy")]
390 (FilterValue::String(a), FilterValue::Secret(b)) => a == b.expose_secret(),
391 (FilterValue::Tuple(a), FilterValue::Tuple(b)) => {
392 a.len() == b.len() && a.iter().zip(b.iter()).all(|(a, b)| a.eq_cs(b))
393 }
394 _ => self == other,
395 }
396 }
397
398 pub fn contains_cs(&self, other: &FilterValue<'a>) -> bool {
415 match (self, other) {
416 (FilterValue::Tuple(a), b) => a.iter().any(|ai| ai.eq_cs(b)),
417 (FilterValue::String(a), FilterValue::String(b)) => a.contains(b.as_ref()),
418 #[cfg(feature = "secrecy")]
419 (FilterValue::Secret(a), FilterValue::Secret(b)) => {
420 a.expose_secret().contains(b.expose_secret())
421 }
422 #[cfg(feature = "secrecy")]
423 (FilterValue::Secret(a), FilterValue::String(b)) => {
424 a.expose_secret().contains(b.as_ref())
425 }
426 #[cfg(feature = "secrecy")]
427 (FilterValue::String(a), FilterValue::Secret(b)) => a.contains(b.expose_secret()),
428 _ => false,
429 }
430 }
431
432 pub fn startswith_cs(&self, other: &FilterValue<'a>) -> bool {
446 match (self, other) {
447 (FilterValue::Tuple(a), b) => a.iter().any(|ai| ai.eq_cs(b)),
448 #[cfg(feature = "secrecy")]
449 (FilterValue::Secret(a), FilterValue::Secret(b)) => {
450 a.expose_secret().starts_with(b.expose_secret())
451 }
452 #[cfg(feature = "secrecy")]
453 (FilterValue::Secret(a), FilterValue::String(b)) => {
454 a.expose_secret().starts_with(b.as_ref())
455 }
456 #[cfg(feature = "secrecy")]
457 (FilterValue::String(a), FilterValue::Secret(b)) => a.starts_with(b.expose_secret()),
458 (FilterValue::String(a), FilterValue::String(b)) => a.starts_with(b.as_ref()),
459 _ => false,
460 }
461 }
462
463 pub fn endswith_cs(&self, other: &FilterValue<'a>) -> bool {
477 match (self, other) {
478 (FilterValue::Tuple(a), b) => a.iter().any(|ai| ai.eq_cs(b)),
479 #[cfg(feature = "secrecy")]
480 (FilterValue::Secret(a), FilterValue::Secret(b)) => {
481 a.expose_secret().ends_with(b.expose_secret())
482 }
483 #[cfg(feature = "secrecy")]
484 (FilterValue::Secret(a), FilterValue::String(b)) => {
485 a.expose_secret().ends_with(b.as_ref())
486 }
487 #[cfg(feature = "secrecy")]
488 (FilterValue::String(a), FilterValue::Secret(b)) => a.ends_with(b.expose_secret()),
489 (FilterValue::String(a), FilterValue::String(b)) => a.ends_with(b.as_ref()),
490 _ => false,
491 }
492 }
493}
494
495impl<'a> PartialEq for FilterValue<'a> {
496 fn eq(&self, other: &Self) -> bool {
497 match (self, other) {
498 (FilterValue::Null, FilterValue::Null) => true,
499 (FilterValue::Bool(a), FilterValue::Bool(b)) => a == b,
500 (FilterValue::Number(a), FilterValue::Number(b)) => a == b,
501 (FilterValue::String(a), FilterValue::String(b)) => caseless_eq(a, b),
502 (FilterValue::Tuple(a), FilterValue::Tuple(b)) => {
503 a.len() == b.len() && a.iter().zip(b.iter()).all(|(a, b)| a == b)
504 }
505 #[cfg(feature = "secrecy")]
506 (FilterValue::Secret(a), FilterValue::Secret(b)) => {
507 caseless_eq(a.expose_secret(), b.expose_secret())
508 }
509 #[cfg(feature = "secrecy")]
510 (FilterValue::Secret(a), FilterValue::String(b)) => caseless_eq(a.expose_secret(), b),
511 #[cfg(feature = "secrecy")]
512 (FilterValue::String(a), FilterValue::Secret(b)) => caseless_eq(a, b.expose_secret()),
513 #[cfg(feature = "chrono")]
514 (FilterValue::DateTime(a), FilterValue::DateTime(b)) => a == b,
515 #[cfg(feature = "chrono")]
516 (FilterValue::Duration(a), FilterValue::Duration(b)) => a == b,
517 _ => false,
518 }
519 }
520}
521
522impl<'a> PartialOrd for FilterValue<'a> {
523 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
524 match (self, other) {
525 (FilterValue::Null, FilterValue::Null) => Some(Ordering::Equal),
526 (FilterValue::Bool(a), FilterValue::Bool(b)) => a.partial_cmp(b),
527 (FilterValue::Number(a), FilterValue::Number(b)) => a.partial_cmp(b),
528 (FilterValue::String(a), FilterValue::String(b)) => a.partial_cmp(b),
529 (FilterValue::Tuple(a), FilterValue::Tuple(b)) => {
530 if a.len() != b.len() {
531 a.len().partial_cmp(&b.len())
532 } else {
533 a.iter()
534 .zip(b.iter())
535 .map(|(x, y)| x.partial_cmp(y))
536 .find(|&cmp| cmp != Some(Ordering::Equal))
537 .unwrap_or(Some(Ordering::Equal))
538 }
539 }
540 #[cfg(feature = "secrecy")]
541 (FilterValue::Secret(a), FilterValue::Secret(b)) => {
542 a.expose_secret().partial_cmp(b.expose_secret())
543 }
544 #[cfg(feature = "secrecy")]
545 (FilterValue::Secret(a), FilterValue::String(b)) => {
546 a.expose_secret().partial_cmp(b.as_ref())
547 }
548 #[cfg(feature = "secrecy")]
549 (FilterValue::String(a), FilterValue::Secret(b)) => {
550 a.as_ref().partial_cmp(b.expose_secret())
551 }
552 #[cfg(feature = "chrono")]
553 (FilterValue::DateTime(a), FilterValue::DateTime(b)) => a.partial_cmp(b),
554 #[cfg(feature = "chrono")]
555 (FilterValue::Duration(a), FilterValue::Duration(b)) => a.partial_cmp(b),
556 _ => None, }
558 }
559
560 fn lt(&self, other: &Self) -> bool {
561 match (self, other) {
562 (FilterValue::Null, FilterValue::Null) => true,
563 (FilterValue::Bool(a), FilterValue::Bool(b)) => a < b,
564 (FilterValue::Number(a), FilterValue::Number(b)) => a < b,
565 (FilterValue::String(a), FilterValue::String(b)) => a < b,
566 (FilterValue::Tuple(a), FilterValue::Tuple(b)) => {
567 a.len() <= b.len() && a.iter().zip(b.iter()).all(|(a, b)| a < b)
568 }
569 #[cfg(feature = "secrecy")]
570 (FilterValue::Secret(a), FilterValue::Secret(b)) => {
571 a.expose_secret() < b.expose_secret()
572 }
573 #[cfg(feature = "secrecy")]
574 (FilterValue::Secret(a), FilterValue::String(b)) => a.expose_secret() < b.as_ref(),
575 #[cfg(feature = "secrecy")]
576 (FilterValue::String(a), FilterValue::Secret(b)) => a.as_ref() < b.expose_secret(),
577 #[cfg(feature = "chrono")]
578 (FilterValue::DateTime(a), FilterValue::DateTime(b)) => a < b,
579 #[cfg(feature = "chrono")]
580 (FilterValue::Duration(a), FilterValue::Duration(b)) => a < b,
581 _ => false,
582 }
583 }
584
585 fn le(&self, other: &Self) -> bool {
586 match (self, other) {
587 (FilterValue::Null, FilterValue::Null) => true,
588 (FilterValue::Bool(a), FilterValue::Bool(b)) => a <= b,
589 (FilterValue::Number(a), FilterValue::Number(b)) => a <= b,
590 (FilterValue::String(a), FilterValue::String(b)) => a <= b,
591 (FilterValue::Tuple(a), FilterValue::Tuple(b)) => {
592 a.len() <= b.len() && a.iter().zip(b.iter()).all(|(a, b)| a <= b)
593 }
594 #[cfg(feature = "secrecy")]
595 (FilterValue::Secret(a), FilterValue::Secret(b)) => {
596 a.expose_secret() <= b.expose_secret()
597 }
598 #[cfg(feature = "secrecy")]
599 (FilterValue::Secret(a), FilterValue::String(b)) => a.expose_secret() <= b.as_ref(),
600 #[cfg(feature = "secrecy")]
601 (FilterValue::String(a), FilterValue::Secret(b)) => a.as_ref() <= b.expose_secret(),
602 #[cfg(feature = "chrono")]
603 (FilterValue::DateTime(a), FilterValue::DateTime(b)) => a <= b,
604 #[cfg(feature = "chrono")]
605 (FilterValue::Duration(a), FilterValue::Duration(b)) => a <= b,
606 _ => false,
607 }
608 }
609
610 fn gt(&self, other: &Self) -> bool {
611 match (self, other) {
612 (FilterValue::Null, FilterValue::Null) => true,
613 (FilterValue::Bool(a), FilterValue::Bool(b)) => a > b,
614 (FilterValue::Number(a), FilterValue::Number(b)) => a > b,
615 (FilterValue::String(a), FilterValue::String(b)) => a > b,
616 (FilterValue::Tuple(a), FilterValue::Tuple(b)) => {
617 a.len() >= b.len() && a.iter().zip(b.iter()).all(|(a, b)| a > b)
618 }
619 #[cfg(feature = "secrecy")]
620 (FilterValue::Secret(a), FilterValue::Secret(b)) => {
621 a.expose_secret() > b.expose_secret()
622 }
623 #[cfg(feature = "secrecy")]
624 (FilterValue::Secret(a), FilterValue::String(b)) => a.expose_secret() > b.as_ref(),
625 #[cfg(feature = "secrecy")]
626 (FilterValue::String(a), FilterValue::Secret(b)) => a.as_ref() > b.expose_secret(),
627 #[cfg(feature = "chrono")]
628 (FilterValue::DateTime(a), FilterValue::DateTime(b)) => a > b,
629 #[cfg(feature = "chrono")]
630 (FilterValue::Duration(a), FilterValue::Duration(b)) => a > b,
631 _ => false,
632 }
633 }
634
635 fn ge(&self, other: &Self) -> bool {
636 match (self, other) {
637 (FilterValue::Null, FilterValue::Null) => true,
638 (FilterValue::Bool(a), FilterValue::Bool(b)) => a >= b,
639 (FilterValue::Number(a), FilterValue::Number(b)) => a >= b,
640 (FilterValue::String(a), FilterValue::String(b)) => a >= b,
641 (FilterValue::Tuple(a), FilterValue::Tuple(b)) => {
642 a.len() >= b.len() && a.iter().zip(b.iter()).all(|(a, b)| a >= b)
643 }
644 #[cfg(feature = "secrecy")]
645 (FilterValue::Secret(a), FilterValue::Secret(b)) => {
646 a.expose_secret() >= b.expose_secret()
647 }
648 #[cfg(feature = "secrecy")]
649 (FilterValue::Secret(a), FilterValue::String(b)) => a.expose_secret() >= b.as_ref(),
650 #[cfg(feature = "secrecy")]
651 (FilterValue::String(a), FilterValue::Secret(b)) => a.as_ref() >= b.expose_secret(),
652 #[cfg(feature = "chrono")]
653 (FilterValue::DateTime(a), FilterValue::DateTime(b)) => a >= b,
654 #[cfg(feature = "chrono")]
655 (FilterValue::Duration(a), FilterValue::Duration(b)) => a >= b,
656 _ => false,
657 }
658 }
659}
660
661impl<'a> Display for FilterValue<'a> {
662 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
674 match self {
675 FilterValue::Null => write!(f, "null"),
676 FilterValue::Bool(b) => write!(f, "{}", b),
677 FilterValue::Number(n) => write!(f, "{}", n),
678 FilterValue::String(s) => {
679 write!(f, "\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
680 }
681 FilterValue::Tuple(v) => {
682 write!(f, "[")?;
683 for (i, value) in v.iter().enumerate() {
684 if i > 0 {
685 write!(f, ", ")?;
686 }
687 write!(f, "{}", value)?;
688 }
689 write!(f, "]")
690 }
691 #[cfg(feature = "secrecy")]
692 FilterValue::Secret(_) => write!(f, "[REDACTED]"),
693 #[cfg(feature = "chrono")]
694 FilterValue::DateTime(dt) => {
695 write!(
696 f,
697 "{}",
698 dt.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true)
699 )
700 }
701 #[cfg(feature = "chrono")]
702 FilterValue::Duration(d) => format_duration(f, d),
703 }
704 }
705}
706
707#[cfg(feature = "chrono")]
710fn format_duration(
711 f: &mut std::fmt::Formatter<'_>,
712 duration: &chrono::Duration,
713) -> std::fmt::Result {
714 let mut remaining_ms = duration.num_milliseconds();
715 if remaining_ms == 0 {
716 return write!(f, "0s");
717 }
718
719 if remaining_ms < 0 {
720 write!(f, "-")?;
721 remaining_ms = remaining_ms.checked_neg().unwrap_or(i64::MAX);
722 }
723
724 for (unit, size_ms) in [
725 ("w", 604_800_000),
726 ("d", 86_400_000),
727 ("h", 3_600_000),
728 ("m", 60_000),
729 ("s", 1_000),
730 ("ms", 1),
731 ] {
732 let count = remaining_ms / size_ms;
733 if count > 0 {
734 write!(f, "{count}{unit}")?;
735 remaining_ms %= size_ms;
736 }
737 }
738
739 Ok(())
740}
741
742impl<'a> Debug for FilterValue<'a> {
743 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
744 write!(f, "{}", self)
745 }
746}
747
748impl<'a> From<bool> for FilterValue<'a> {
749 fn from(b: bool) -> Self {
750 FilterValue::Bool(b)
751 }
752}
753
754macro_rules! number {
755 ($t:ty) => {
756 impl<'a> From<$t> for FilterValue<'a> {
757 fn from(n: $t) -> Self {
758 FilterValue::Number(n as f64)
759 }
760 }
761 };
762}
763
764number!(i8);
765number!(u8);
766number!(i16);
767number!(u16);
768number!(f32);
769number!(i32);
770number!(u32);
771number!(f64);
772number!(i64);
773number!(u64);
774
775impl<'a> From<&'a str> for FilterValue<'a> {
776 fn from(s: &'a str) -> Self {
781 FilterValue::String(Cow::Borrowed(s))
782 }
783}
784
785impl<'a> From<String> for FilterValue<'a> {
786 fn from(s: String) -> Self {
787 FilterValue::String(Cow::Owned(s))
788 }
789}
790
791#[cfg(feature = "secrecy")]
792impl<'a> From<secrecy::SecretString> for FilterValue<'a> {
793 fn from(s: secrecy::SecretString) -> Self {
804 FilterValue::Secret(s)
805 }
806}
807
808#[cfg(feature = "chrono")]
820impl<'a> From<chrono::DateTime<chrono::Utc>> for FilterValue<'a> {
821 fn from(dt: chrono::DateTime<chrono::Utc>) -> Self {
822 FilterValue::DateTime(dt)
823 }
824}
825
826#[cfg(feature = "chrono")]
837impl<'a> From<chrono::Duration> for FilterValue<'a> {
838 fn from(d: chrono::Duration) -> Self {
839 FilterValue::Duration(d)
840 }
841}
842
843#[cfg(feature = "chrono")]
857impl<'a> From<std::time::SystemTime> for FilterValue<'a> {
858 fn from(t: std::time::SystemTime) -> Self {
859 FilterValue::DateTime(t.into())
860 }
861}
862
863impl<'a, T> From<Option<T>> for FilterValue<'a>
864where
865 T: Into<FilterValue<'a>>,
866{
867 fn from(o: Option<T>) -> Self {
868 o.map_or(FilterValue::Null, Into::into)
869 }
870}
871
872impl<'a> From<Vec<FilterValue<'a>>> for FilterValue<'a> {
873 fn from(v: Vec<FilterValue<'a>>) -> Self {
874 FilterValue::Tuple(v)
875 }
876}
877
878#[cfg(test)]
879mod tests {
880 use rstest::rstest;
881
882 use super::*;
883
884 #[rstest]
885 #[case(FilterValue::Null, false)]
886 #[case(FilterValue::Bool(false), false)]
887 #[case(FilterValue::Bool(true), true)]
888 #[case(FilterValue::Number(0.0), false)]
889 #[case(FilterValue::Number(1.0), true)]
890 #[case(FilterValue::String("".into()), false)]
891 #[case(FilterValue::String("hello".into()), true)]
892 #[case(FilterValue::Tuple(vec![]), false)]
893 #[case(FilterValue::Tuple(vec![FilterValue::Bool(true)]), true)]
894 fn test_truthy(#[case] value: FilterValue<'_>, #[case] truthy: bool) {
895 assert_eq!(value.is_truthy(), truthy);
896 }
897
898 #[rstest]
901 #[case(FilterValue::Null)]
902 #[case(FilterValue::Bool(true))]
903 #[case(FilterValue::Number(42.0))]
904 #[case(FilterValue::String("hello".into()))]
905 #[case(FilterValue::Tuple(vec![
906 FilterValue::String("a".into()),
907 FilterValue::Tuple(vec![FilterValue::String("b".into())]),
908 ]))]
909 fn test_into_owned(#[case] value: FilterValue<'_>) {
910 let owned: FilterValue<'static> = value.clone().into_owned();
911 assert_eq!(owned, value);
912 }
913
914 #[test]
917 fn test_into_owned_outlives_source() {
918 let owned = {
919 let source = String::from("borrowed");
920 let value: FilterValue<'_> = source.as_str().into();
921 value.into_owned()
922 };
923
924 assert_eq!(owned, FilterValue::String("borrowed".into()));
925 }
926
927 #[test]
928 fn test_bool_comparison() {
929 assert!(FilterValue::Bool(false) < FilterValue::Bool(true));
930 assert!(FilterValue::Bool(true) > FilterValue::Bool(false));
931 assert_eq!(FilterValue::Bool(true), FilterValue::Bool(true));
932 assert_eq!(FilterValue::Bool(false), FilterValue::Bool(false));
933 }
934
935 #[test]
936 fn test_number_comparison() {
937 assert!(FilterValue::Number(1.0) < FilterValue::Number(2.0));
938 assert!(FilterValue::Number(2.0) > FilterValue::Number(1.0));
939 assert_eq!(FilterValue::Number(2.0), FilterValue::Number(2.0));
940 }
941
942 #[test]
943 fn test_string_comparison() {
944 assert!(FilterValue::String("abc".into()) < FilterValue::String("xyz".into()));
945 assert!(FilterValue::String("xyz".into()) > FilterValue::String("abc".into()));
946 assert_eq!(
947 FilterValue::String("abc".into()),
948 FilterValue::String("abc".into())
949 );
950 }
951
952 #[test]
953 fn test_string_equality_is_case_insensitive() {
954 assert_eq!(
955 FilterValue::String("Hello World".into()),
956 FilterValue::String("hello world".into())
957 );
958 assert_ne!(
959 FilterValue::String("Hello World".into()),
960 FilterValue::String("goodbye world".into())
961 );
962
963 assert_eq!(
966 FilterValue::String("JÜRGEN".into()),
967 FilterValue::String("jürgen".into())
968 );
969 assert_eq!(
970 FilterValue::String("ΛΟΓΟΣ".into()),
971 FilterValue::String("λογος".into())
972 );
973 assert_eq!(
974 FilterValue::String("straße".into()),
975 FilterValue::String("STRASSE".into())
976 );
977 }
978
979 #[test]
980 fn test_tuple_comparison() {
981 assert!(
985 FilterValue::Tuple(vec![1.into(), 2.into()])
986 < FilterValue::Tuple(vec![3.into(), 4.into()])
987 );
988 assert!(
989 FilterValue::Tuple(vec![3.into(), 4.into()])
990 > FilterValue::Tuple(vec![1.into(), 2.into()])
991 );
992
993 let short = FilterValue::Tuple(vec![1.into()]);
994 let long = FilterValue::Tuple(vec![1.into(), 2.into()]);
995 assert_eq!(short.partial_cmp(&long), Some(Ordering::Less));
996 assert_eq!(long.partial_cmp(&short), Some(Ordering::Greater));
997
998 assert_eq!(
999 FilterValue::Tuple(vec![1.into(), 2.into()]),
1000 FilterValue::Tuple(vec![1.into(), 2.into()])
1001 );
1002 assert_ne!(
1003 FilterValue::Tuple(vec![1.into(), 2.into()]),
1004 FilterValue::Tuple(vec![2.into(), 1.into()])
1005 );
1006 }
1007
1008 #[rstest]
1009 #[case(FilterValue::Null, FilterValue::Bool(true))]
1010 #[case(FilterValue::Bool(true), FilterValue::Number(1.0))]
1011 #[case(FilterValue::Number(1.0), FilterValue::String("1".into()))]
1012 #[case(FilterValue::String("a".into()), FilterValue::Tuple(vec!["a".into()]))]
1013 fn test_mismatched_types_are_not_equal_or_ordered(
1014 #[case] left: FilterValue<'_>,
1015 #[case] right: FilterValue<'_>,
1016 ) {
1017 assert_ne!(left, right);
1018 assert_eq!(left.partial_cmp(&right), None);
1019 assert!(!left.lt(&right));
1020 assert!(!left.le(&right));
1021 assert!(!left.gt(&right));
1022 assert!(!left.ge(&right));
1023 }
1024
1025 #[rstest]
1026 #[case(true.into(), FilterValue::Bool(true))]
1027 #[case(42i8.into(), FilterValue::Number(42.0))]
1028 #[case(42u8.into(), FilterValue::Number(42.0))]
1029 #[case(42i16.into(), FilterValue::Number(42.0))]
1030 #[case(42u16.into(), FilterValue::Number(42.0))]
1031 #[case(42i32.into(), FilterValue::Number(42.0))]
1032 #[case(42u32.into(), FilterValue::Number(42.0))]
1033 #[case(42i64.into(), FilterValue::Number(42.0))]
1034 #[case(42u64.into(), FilterValue::Number(42.0))]
1035 #[case(4.2f32.into(), FilterValue::Number(4.2f32 as f64))]
1036 #[case(4.2f64.into(), FilterValue::Number(4.2))]
1037 #[case("hello".into(), FilterValue::String("hello".into()))]
1038 #[case(String::from("hello").into(), FilterValue::String("hello".into()))]
1039 #[case(Some(1).into(), FilterValue::Number(1.0))]
1040 #[case(None::<i32>.into(), FilterValue::Null)]
1041 #[case(vec![FilterValue::Null].into(), FilterValue::Tuple(vec![FilterValue::Null]))]
1042 fn test_conversions(#[case] converted: FilterValue<'_>, #[case] expected: FilterValue<'_>) {
1043 assert_eq!(converted, expected);
1044 }
1045
1046 #[rstest]
1047 #[case(FilterValue::Null, "null")]
1048 #[case(FilterValue::Bool(true), "true")]
1049 #[case(FilterValue::Bool(false), "false")]
1050 #[case(FilterValue::Number(1.5), "1.5")]
1051 #[case(FilterValue::String("hello".into()), "\"hello\"")]
1052 #[case(FilterValue::String("say \"hi\"".into()), "\"say \\\"hi\\\"\"")]
1053 #[case(FilterValue::String("back\\slash".into()), "\"back\\\\slash\"")]
1054 #[case(FilterValue::Tuple(vec![]), "[]")]
1055 #[case(FilterValue::Tuple(vec![1.into(), "a".into()]), "[1, \"a\"]")]
1056 fn test_display(#[case] value: FilterValue<'_>, #[case] expected: &str) {
1057 assert_eq!(value.to_string(), expected);
1058 assert_eq!(format!("{value:?}"), expected);
1059 }
1060
1061 #[rstest]
1062 #[case("Hello World".into(), "world".into(), true)]
1063 #[case("Hello World".into(), "WORLD".into(), true)]
1064 #[case("Hello World".into(), "mars".into(), false)]
1065 #[case(FilterValue::Tuple(vec!["a".into(), "b".into()]), "A".into(), true)]
1066 #[case(FilterValue::Tuple(vec!["a".into(), "b".into()]), "c".into(), false)]
1067 #[case(FilterValue::Tuple(vec![]), FilterValue::Null, false)]
1068 #[case(FilterValue::Null, FilterValue::Null, false)]
1069 #[case(FilterValue::Number(12.0), FilterValue::Number(2.0), false)]
1070 fn test_contains(
1071 #[case] value: FilterValue<'_>,
1072 #[case] other: FilterValue<'_>,
1073 #[case] expected: bool,
1074 ) {
1075 assert_eq!(value.contains(&other), expected);
1076 }
1077
1078 #[rstest]
1079 #[case("Hello World".into(), "hello".into(), true)]
1080 #[case("Hello World".into(), "world".into(), false)]
1081 #[case(FilterValue::Tuple(vec!["a".into()]), "a".into(), true)]
1082 #[case(FilterValue::Null, "a".into(), false)]
1083 #[case("Hello".into(), FilterValue::Null, false)]
1084 fn test_startswith(
1085 #[case] value: FilterValue<'_>,
1086 #[case] other: FilterValue<'_>,
1087 #[case] expected: bool,
1088 ) {
1089 assert_eq!(value.startswith(&other), expected);
1090 }
1091
1092 #[rstest]
1093 #[case("Hello World".into(), "world".into(), true)]
1094 #[case("Hello World".into(), "hello".into(), false)]
1095 #[case(FilterValue::Tuple(vec!["a".into()]), "a".into(), true)]
1096 #[case(FilterValue::Null, "a".into(), false)]
1097 #[case("Hello".into(), FilterValue::Null, false)]
1098 fn test_endswith(
1099 #[case] value: FilterValue<'_>,
1100 #[case] other: FilterValue<'_>,
1101 #[case] expected: bool,
1102 ) {
1103 assert_eq!(value.endswith(&other), expected);
1104 }
1105
1106 #[test]
1107 fn test_default_is_null() {
1108 assert_eq!(FilterValue::default(), FilterValue::Null);
1109 }
1110
1111 #[rstest]
1112 #[case("Hello".into(), "Hello".into(), true)]
1113 #[case("Hello".into(), "hello".into(), false)]
1114 #[case("straße".into(), "STRASSE".into(), false)] #[case(FilterValue::Null, FilterValue::Null, true)]
1116 #[case(FilterValue::Bool(true), FilterValue::Bool(true), true)]
1117 #[case(FilterValue::Number(1.0), FilterValue::Number(1.0), true)]
1118 #[case(FilterValue::Tuple(vec!["A".into()]), FilterValue::Tuple(vec!["A".into()]), true)]
1119 #[case(FilterValue::Tuple(vec!["A".into()]), FilterValue::Tuple(vec!["a".into()]), false)]
1120 #[case("1".into(), FilterValue::Number(1.0), false)]
1121 fn test_eq_cs(
1122 #[case] left: FilterValue<'_>,
1123 #[case] right: FilterValue<'_>,
1124 #[case] expected: bool,
1125 ) {
1126 assert_eq!(left.eq_cs(&right), expected);
1127 assert_eq!(right.eq_cs(&left), expected);
1128 }
1129
1130 #[rstest]
1131 #[case("Hello World".into(), "World".into(), true)]
1132 #[case("Hello World".into(), "world".into(), false)]
1133 #[case(FilterValue::Tuple(vec!["a".into(), "B".into()]), "B".into(), true)]
1134 #[case(FilterValue::Tuple(vec!["a".into(), "B".into()]), "b".into(), false)]
1135 #[case(FilterValue::Null, FilterValue::Null, false)]
1136 #[case(FilterValue::Number(12.0), FilterValue::Number(2.0), false)]
1137 fn test_contains_cs(
1138 #[case] value: FilterValue<'_>,
1139 #[case] other: FilterValue<'_>,
1140 #[case] expected: bool,
1141 ) {
1142 assert_eq!(value.contains_cs(&other), expected);
1143 }
1144
1145 #[rstest]
1146 #[case("Hello World".into(), "Hello".into(), true)]
1147 #[case("Hello World".into(), "hello".into(), false)]
1148 #[case(FilterValue::Tuple(vec!["A".into()]), "A".into(), true)]
1149 #[case(FilterValue::Tuple(vec!["A".into()]), "a".into(), false)]
1150 #[case(FilterValue::Null, "a".into(), false)]
1151 fn test_startswith_cs(
1152 #[case] value: FilterValue<'_>,
1153 #[case] other: FilterValue<'_>,
1154 #[case] expected: bool,
1155 ) {
1156 assert_eq!(value.startswith_cs(&other), expected);
1157 }
1158
1159 #[rstest]
1160 #[case("Hello World".into(), "World".into(), true)]
1161 #[case("Hello World".into(), "WORLD".into(), false)]
1162 #[case(FilterValue::Tuple(vec!["A".into()]), "A".into(), true)]
1163 #[case(FilterValue::Tuple(vec!["A".into()]), "a".into(), false)]
1164 #[case(FilterValue::Null, "a".into(), false)]
1165 fn test_endswith_cs(
1166 #[case] value: FilterValue<'_>,
1167 #[case] other: FilterValue<'_>,
1168 #[case] expected: bool,
1169 ) {
1170 assert_eq!(value.endswith_cs(&other), expected);
1171 }
1172
1173 #[rstest]
1184 #[case("ΛΟΓΟΣ", "Σ")] #[case("ΛΟΓΟΣ", "ς")] #[case("ΛΟΓΟΣ", "σ")] #[case("λογος", "Σ")] fn test_greek_sigma_forms_are_equivalent(#[case] haystack: &str, #[case] needle: &str) {
1189 let haystack: FilterValue<'_> = haystack.into();
1190 let needle: FilterValue<'_> = needle.into();
1191
1192 assert!(haystack.endswith(&needle));
1193 assert!(haystack.contains(&needle));
1194 }
1195
1196 #[rstest]
1200 #[case("İstanbul", "i\u{307}stanbul", true)] #[case("İstanbul", "\u{307}stanbul", true)] #[case("İstanbul", "istanbul", false)] #[case("straße", "STRASSE", true)] #[case("groß", "ss", true)]
1205 #[case("gross", "ß", true)] fn test_multi_char_lowercase_expansions(
1207 #[case] haystack: &str,
1208 #[case] needle: &str,
1209 #[case] expected: bool,
1210 ) {
1211 let haystack: FilterValue<'_> = haystack.into();
1212 let needle: FilterValue<'_> = needle.into();
1213
1214 assert_eq!(haystack.contains(&needle), expected);
1215 }
1216
1217 #[cfg(feature = "secrecy")]
1218 mod secrecy_tests {
1219 use super::*;
1220
1221 #[rstest]
1222 #[case(FilterValue::secret(""), false)]
1223 #[case(FilterValue::secret("hunter2"), true)]
1224 fn test_secret_truthy(#[case] value: FilterValue<'_>, #[case] truthy: bool) {
1225 assert_eq!(value.is_truthy(), truthy);
1226 }
1227
1228 #[rstest]
1229 #[case(FilterValue::secret("hunter2"), FilterValue::secret("hunter2"), true)]
1230 #[case(FilterValue::secret("hunter2"), FilterValue::secret("HUNTER2"), true)]
1231 #[case(
1232 FilterValue::secret("hunter2"),
1233 FilterValue::secret("swordfish"),
1234 false
1235 )]
1236 #[case(FilterValue::secret("hunter2"), "hunter2".into(), true)]
1237 #[case(FilterValue::secret("hunter2"), "HUNTER2".into(), true)]
1238 #[case("HUNTER2".into(), FilterValue::secret("hunter2"), true)]
1239 #[case("swordfish".into(), FilterValue::secret("hunter2"), false)]
1240 fn test_secret_equality(
1241 #[case] left: FilterValue<'_>,
1242 #[case] right: FilterValue<'_>,
1243 #[case] equal: bool,
1244 ) {
1245 assert_eq!(left == right, equal);
1246 assert_eq!(left != right, !equal);
1247 }
1248
1249 #[rstest]
1250 #[case(FilterValue::secret("abc"), FilterValue::secret("xyz"))]
1251 #[case(FilterValue::secret("abc"), "xyz".into())]
1252 #[case("abc".into(), FilterValue::secret("xyz"))]
1253 fn test_secret_ordering(#[case] smaller: FilterValue<'_>, #[case] larger: FilterValue<'_>) {
1254 assert_eq!(smaller.partial_cmp(&larger), Some(Ordering::Less));
1255 assert_eq!(larger.partial_cmp(&smaller), Some(Ordering::Greater));
1256 assert!(smaller < larger);
1257 assert!(smaller <= larger);
1258 assert!(larger > smaller);
1259 assert!(larger >= smaller);
1260 assert!(!smaller.gt(&larger));
1261 assert!(!smaller.ge(&larger));
1262 assert!(!larger.lt(&smaller));
1263 assert!(!larger.le(&smaller));
1264 }
1265
1266 #[rstest]
1267 #[case(FilterValue::secret("Hello World"), "world".into(), true)]
1268 #[case(FilterValue::secret("Hello World"), "mars".into(), false)]
1269 #[case("Hello World".into(), FilterValue::secret("WORLD"), true)]
1270 #[case("Hello World".into(), FilterValue::secret("mars"), false)]
1271 #[case(FilterValue::secret("Hello World"), FilterValue::secret("WORLD"), true)]
1272 #[case(FilterValue::Tuple(vec![FilterValue::secret("a"), "b".into()]), "A".into(), true)]
1273 #[case(FilterValue::Tuple(vec!["a".into(), "b".into()]), FilterValue::secret("B"), true)]
1274 #[case(FilterValue::Tuple(vec!["a".into(), "b".into()]), FilterValue::secret("c"), false)]
1275 fn test_secret_contains(
1276 #[case] value: FilterValue<'_>,
1277 #[case] other: FilterValue<'_>,
1278 #[case] expected: bool,
1279 ) {
1280 assert_eq!(value.contains(&other), expected);
1281 }
1282
1283 #[rstest]
1284 #[case(FilterValue::secret("Hello World"), "hello".into(), true)]
1285 #[case(FilterValue::secret("Hello World"), "world".into(), false)]
1286 #[case("Hello World".into(), FilterValue::secret("HELLO"), true)]
1287 #[case("Hello World".into(), FilterValue::secret("world"), false)]
1288 #[case(FilterValue::secret("Hello World"), FilterValue::secret("HELLO"), true)]
1289 fn test_secret_startswith(
1290 #[case] value: FilterValue<'_>,
1291 #[case] other: FilterValue<'_>,
1292 #[case] expected: bool,
1293 ) {
1294 assert_eq!(value.startswith(&other), expected);
1295 }
1296
1297 #[rstest]
1298 #[case(FilterValue::secret("Hello World"), "WORLD".into(), true)]
1299 #[case(FilterValue::secret("Hello World"), "hello".into(), false)]
1300 #[case("Hello World".into(), FilterValue::secret("world"), true)]
1301 #[case("Hello World".into(), FilterValue::secret("hello"), false)]
1302 #[case(FilterValue::secret("Hello World"), FilterValue::secret("world"), true)]
1303 fn test_secret_endswith(
1304 #[case] value: FilterValue<'_>,
1305 #[case] other: FilterValue<'_>,
1306 #[case] expected: bool,
1307 ) {
1308 assert_eq!(value.endswith(&other), expected);
1309 }
1310
1311 #[rstest]
1312 #[case(FilterValue::Null)]
1313 #[case(FilterValue::Bool(true))]
1314 #[case(FilterValue::Number(1.0))]
1315 #[case(FilterValue::Tuple(vec!["hunter2".into()]))]
1316 fn test_secrets_are_not_equal_or_ordered_against_other_types(
1317 #[case] other: FilterValue<'_>,
1318 ) {
1319 let secret = FilterValue::secret("hunter2");
1320 assert_ne!(secret, other);
1321 assert_ne!(other, secret);
1322 assert_eq!(secret.partial_cmp(&other), None);
1323 assert_eq!(other.partial_cmp(&secret), None);
1324 assert!(!secret.lt(&other));
1325 assert!(!secret.le(&other));
1326 assert!(!secret.gt(&other));
1327 assert!(!secret.ge(&other));
1328 }
1329
1330 #[rstest]
1331 #[case(FilterValue::secret("hunter2"), "[REDACTED]")]
1332 #[case(FilterValue::secret(""), "[REDACTED]")]
1333 #[case(
1334 FilterValue::Tuple(vec!["a".into(), FilterValue::secret("hunter2"), 1.into()]),
1335 "[\"a\", [REDACTED], 1]"
1336 )]
1337 fn test_secret_display_is_redacted(#[case] value: FilterValue<'_>, #[case] expected: &str) {
1338 assert_eq!(value.to_string(), expected);
1339 assert_eq!(format!("{value:?}"), expected);
1340 assert!(!value.to_string().contains("hunter2"));
1341 assert!(!format!("{value:?}").contains("hunter2"));
1342 }
1343
1344 #[test]
1345 fn test_secret_conversions() {
1346 let secret: FilterValue<'_> = secrecy::SecretString::from("hunter2").into();
1347 assert_eq!(secret, FilterValue::secret("hunter2"));
1348 assert!(matches!(secret, FilterValue::Secret(_)));
1349 assert!(matches!(
1350 FilterValue::secret(String::from("hunter2")),
1351 FilterValue::Secret(_)
1352 ));
1353 }
1354
1355 #[rstest]
1358 #[case("hunter2", "hunter2")]
1359 #[case("hunter2", "HUNTER2")]
1360 #[case("hunter2", "swordfish")]
1361 #[case("abc", "abd")]
1362 #[case("abd", "abc")]
1363 #[case("Hello World", "WORLD")]
1364 #[case("Hello World", "hello")]
1365 #[case("", "")]
1366 #[case("", "a")]
1367 #[case("ÜBER", "über")]
1368 fn test_secrets_behave_exactly_like_strings(
1369 #[case] secret: &'static str,
1370 #[case] other: &'static str,
1371 ) {
1372 let as_secret = FilterValue::secret(secret);
1373 let as_string = FilterValue::String(secret.into());
1374 let other = FilterValue::String(other.into());
1375
1376 assert_eq!(
1377 as_secret == other,
1378 as_string == other,
1379 "{secret} == {other}"
1380 );
1381 assert_eq!(
1382 other == as_secret,
1383 other == as_string,
1384 "{other} == {secret}"
1385 );
1386 assert_eq!(
1387 as_secret.partial_cmp(&other),
1388 as_string.partial_cmp(&other),
1389 "{secret} cmp {other}"
1390 );
1391 assert_eq!(
1392 other.partial_cmp(&as_secret),
1393 other.partial_cmp(&as_string),
1394 "{other} cmp {secret}"
1395 );
1396 assert_eq!(as_secret < other, as_string < other, "{secret} < {other}");
1397 assert_eq!(other < as_secret, other < as_string, "{other} < {secret}");
1398 assert_eq!(
1399 as_secret <= other,
1400 as_string <= other,
1401 "{secret} <= {other}"
1402 );
1403 assert_eq!(
1404 other <= as_secret,
1405 other <= as_string,
1406 "{other} <= {secret}"
1407 );
1408 assert_eq!(as_secret > other, as_string > other, "{secret} > {other}");
1409 assert_eq!(other > as_secret, other > as_string, "{other} > {secret}");
1410 assert_eq!(
1411 as_secret >= other,
1412 as_string >= other,
1413 "{secret} >= {other}"
1414 );
1415 assert_eq!(
1416 other >= as_secret,
1417 other >= as_string,
1418 "{other} >= {secret}"
1419 );
1420 assert_eq!(
1421 as_secret.contains(&other),
1422 as_string.contains(&other),
1423 "{secret} contains {other}"
1424 );
1425 assert_eq!(
1426 other.contains(&as_secret),
1427 other.contains(&as_string),
1428 "{other} contains {secret}"
1429 );
1430 assert_eq!(
1431 as_secret.startswith(&other),
1432 as_string.startswith(&other),
1433 "{secret} starts with {other}"
1434 );
1435 assert_eq!(
1436 other.startswith(&as_secret),
1437 other.startswith(&as_string),
1438 "{other} starts with {secret}"
1439 );
1440 assert_eq!(
1441 as_secret.endswith(&other),
1442 as_string.endswith(&other),
1443 "{secret} ends with {other}"
1444 );
1445 assert_eq!(
1446 other.endswith(&as_secret),
1447 other.endswith(&as_string),
1448 "{other} ends with {secret}"
1449 );
1450 assert_eq!(
1451 as_secret.is_truthy(),
1452 as_string.is_truthy(),
1453 "{secret} is_truthy"
1454 );
1455 }
1456 }
1457
1458 #[cfg(feature = "chrono")]
1459 mod chrono_tests {
1460 use super::*;
1461 use chrono::{Duration, TimeZone, Utc};
1462
1463 fn datetime() -> chrono::DateTime<Utc> {
1464 Utc.with_ymd_and_hms(2026, 6, 12, 13, 30, 45).unwrap()
1465 }
1466
1467 #[test]
1468 fn test_truthiness() {
1469 assert!(FilterValue::DateTime(datetime()).is_truthy());
1470 assert!(FilterValue::Duration(Duration::seconds(1)).is_truthy());
1471 assert!(FilterValue::Duration(Duration::seconds(-1)).is_truthy());
1472 assert!(!FilterValue::Duration(Duration::zero()).is_truthy());
1473 }
1474
1475 #[test]
1476 fn test_datetime_comparison() {
1477 let earlier = FilterValue::DateTime(datetime());
1478 let later = FilterValue::DateTime(datetime() + Duration::minutes(5));
1479
1480 assert!(earlier < later);
1481 assert!(later > earlier);
1482 assert!(earlier <= later);
1483 assert!(later >= earlier);
1484 assert_eq!(earlier, FilterValue::DateTime(datetime()));
1485 assert_ne!(earlier, later);
1486 assert_eq!(earlier.partial_cmp(&later), Some(Ordering::Less));
1487 }
1488
1489 #[test]
1490 fn test_duration_comparison() {
1491 let shorter = FilterValue::Duration(Duration::minutes(5));
1492 let longer = FilterValue::Duration(Duration::hours(1));
1493
1494 assert!(shorter < longer);
1495 assert!(longer > shorter);
1496 assert!(shorter <= longer);
1497 assert!(longer >= shorter);
1498 assert_eq!(shorter, FilterValue::Duration(Duration::seconds(300)));
1499 assert_ne!(shorter, longer);
1500 assert_eq!(shorter.partial_cmp(&longer), Some(Ordering::Less));
1501 }
1502
1503 #[rstest]
1504 #[case(
1505 FilterValue::DateTime(datetime()),
1506 FilterValue::Duration(Duration::minutes(5))
1507 )]
1508 #[case(FilterValue::DateTime(datetime()), FilterValue::Number(1.0))]
1509 #[case(
1510 FilterValue::Duration(Duration::minutes(5)),
1511 FilterValue::Number(300.0)
1512 )]
1513 #[case(FilterValue::Duration(Duration::minutes(5)), FilterValue::String("5m".into()))]
1514 #[case(FilterValue::DateTime(datetime()), FilterValue::Null)]
1515 fn test_mismatched_types_are_not_equal_or_ordered(
1516 #[case] left: FilterValue<'_>,
1517 #[case] right: FilterValue<'_>,
1518 ) {
1519 assert_ne!(left, right);
1520 assert_eq!(left.partial_cmp(&right), None);
1521 assert!(!left.lt(&right));
1522 assert!(!left.le(&right));
1523 assert!(!left.gt(&right));
1524 assert!(!left.ge(&right));
1525 }
1526
1527 #[rstest]
1528 #[case(FilterValue::Duration(Duration::zero()), "0s")]
1529 #[case(FilterValue::Duration(Duration::milliseconds(500)), "500ms")]
1530 #[case(FilterValue::Duration(Duration::seconds(90)), "1m30s")]
1531 #[case(FilterValue::Duration(Duration::minutes(90)), "1h30m")]
1532 #[case(FilterValue::Duration(Duration::hours(26)), "1d2h")]
1533 #[case(FilterValue::Duration(Duration::days(15)), "2w1d")]
1534 #[case(
1535 FilterValue::Duration(Duration::milliseconds(90_061_001)),
1536 "1d1h1m1s1ms"
1537 )]
1538 #[case(FilterValue::Duration(Duration::minutes(-90)), "-1h30m")]
1539 fn test_duration_display(#[case] value: FilterValue<'_>, #[case] expected: &str) {
1540 assert_eq!(value.to_string(), expected);
1541 }
1542
1543 #[test]
1544 fn test_datetime_display_is_rfc3339() {
1545 assert_eq!(
1546 FilterValue::DateTime(datetime()).to_string(),
1547 "2026-06-12T13:30:45Z"
1548 );
1549 }
1550
1551 #[test]
1552 fn test_conversions() {
1553 assert_eq!(
1554 FilterValue::from(datetime()),
1555 FilterValue::DateTime(datetime())
1556 );
1557 assert_eq!(
1558 FilterValue::from(Duration::minutes(5)),
1559 FilterValue::Duration(Duration::minutes(5))
1560 );
1561 assert_eq!(
1562 FilterValue::from(std::time::SystemTime::UNIX_EPOCH),
1563 FilterValue::DateTime(Utc.timestamp_opt(0, 0).unwrap())
1564 );
1565 }
1566
1567 #[test]
1568 fn test_contains_and_friends_are_false_for_temporal_values() {
1569 let dt = FilterValue::DateTime(datetime());
1570 let d = FilterValue::Duration(Duration::minutes(5));
1571
1572 assert!(!dt.contains(&d));
1573 assert!(!dt.startswith(&d));
1574 assert!(!dt.endswith(&d));
1575 assert!(!d.contains(&dt));
1576 assert!(!d.startswith(&dt));
1577 assert!(!d.endswith(&dt));
1578
1579 let tuple = FilterValue::Tuple(vec![FilterValue::Duration(Duration::minutes(5))]);
1581 assert!(tuple.contains(&d));
1582 }
1583 }
1584}