Skip to main content

filt_rs/
value.rs

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
12/// A trait for types which can be filtered by the filter system.
13///
14/// Types which implement this trait can be filtered through the use
15/// of filter DSL expressions. A filter expression might look something
16/// like the following:
17///
18/// ```text
19/// repo.public && !repo.fork && repo.name in ["git-tool", "grey"]
20/// ```
21///
22/// In this case, the [`Filter`](crate::Filter) would call [`Filterable::get`]
23/// with the property keys it intends to retrieve, in this case: `repo.public`,
24/// `repo.fork`, and `repo.name`. The [`Filterable`] implementation would
25/// then return the appropriate [`FilterValue`] for each key.
26///
27/// ```
28/// use filt_rs::{FilterValue, Filterable};
29///
30/// struct Repo {
31///     name: String,
32///     public: bool,
33///     fork: bool,
34/// }
35///
36/// impl Filterable for Repo {
37///     fn get(&self, key: &str) -> FilterValue<'_> {
38///         match key {
39///             "repo.name" => self.name.as_str().into(),
40///             "repo.public" => self.public.into(),
41///             "repo.fork" => self.fork.into(),
42///             _ => FilterValue::Null,
43///         }
44///     }
45/// }
46/// ```
47pub trait Filterable {
48    /// Retrieve the value of a property key.
49    ///
50    /// This method should return the value of the property key as it
51    /// pertains to the filterable object. If the key is not present,
52    /// the method should return a [`FilterValue::Null`] value.
53    ///
54    /// The returned value is bound to the lifetime of `&self`, so
55    /// implementations may borrow directly from the object being filtered
56    /// (for example by returning `FilterValue::String(Cow::Borrowed(..))`,
57    /// which is what `From<&str>` produces) to avoid copying its data.
58    fn get(&self, key: &str) -> FilterValue<'_>;
59}
60
61/// A value which may appear within a filter expression, either as a literal
62/// or as the result of resolving a property on a [`Filterable`] object.
63///
64/// `FilterValue` implements [`From`] for most primitive Rust types (booleans,
65/// numbers, strings, [`Option`]s, and vectors of values), making it easy to
66/// construct from your own data within a [`Filterable::get`] implementation.
67///
68/// ```
69/// use filt_rs::FilterValue;
70///
71/// let value: FilterValue<'_> = 42.into();
72/// assert_eq!(value, FilterValue::Number(42.0));
73///
74/// let value: FilterValue<'_> = Some("hello").into();
75/// assert_eq!(value, FilterValue::String("hello".into()));
76///
77/// let value: FilterValue<'_> = None::<bool>.into();
78/// assert_eq!(value, FilterValue::Null);
79/// ```
80///
81/// Note that string equality comparisons between `FilterValue`s are
82/// case-insensitive, mirroring the behaviour of the filter language itself.
83/// Case is folded character-by-character using the language's Unicode
84/// case-folding rules (with all Greek sigma forms treated as equivalent),
85/// so multi-character folds such as `ß` → `ss` compare equal too.
86///
87/// ```
88/// use filt_rs::FilterValue;
89///
90/// let a: FilterValue<'_> = "Hello".into();
91/// let b: FilterValue<'_> = "hello".into();
92/// assert_eq!(a, b);
93///
94/// let a: FilterValue<'_> = "STRASSE".into();
95/// let b: FilterValue<'_> = "straße".into();
96/// assert_eq!(a, b);
97/// ```
98#[derive(Clone, Default)]
99pub enum FilterValue<'a> {
100    /// The absence of a value, also returned for unknown property keys.
101    #[default]
102    Null,
103    /// A boolean value (`true` or `false`).
104    Bool(bool),
105    /// A numeric value; all numbers are represented as 64-bit floats.
106    Number(f64),
107    /// A string value, compared case-insensitively by the filter language.
108    ///
109    /// The string may borrow from the object being filtered (when produced
110    /// by a [`Filterable::get`] implementation) or own its data, courtesy of
111    /// the [`Cow`].
112    String(Cow<'a, str>),
113    /// An ordered list of values, written as `[a, b, c]` in filter expressions.
114    Tuple(Vec<FilterValue<'a>>),
115    /// A secret string value which behaves exactly like a [`FilterValue::String`]
116    /// in comparisons, but is always redacted as `[REDACTED]` when formatted.
117    #[cfg(feature = "secrecy")]
118    Secret(secrecy::SecretString),
119    /// A point in time (in UTC), produced by functions like `now()` or by a
120    /// [`Filterable::get`] implementation. Displayed in RFC 3339 format.
121    ///
122    /// *Only available when the `chrono` crate feature is enabled.*
123    #[cfg(feature = "chrono")]
124    DateTime(chrono::DateTime<chrono::Utc>),
125    /// A span of time, written as duration literals like `5m` or `1h30m` in
126    /// filter expressions. Displayed in the same compact form (e.g. `1h30m`).
127    ///
128    /// *Only available when the `chrono` crate feature is enabled.*
129    #[cfg(feature = "chrono")]
130    Duration(chrono::Duration),
131}
132
133impl<'a> FilterValue<'a> {
134    /// Creates a secret string value backed by a [`secrecy::SecretString`].
135    ///
136    /// Secret values behave exactly like a [`FilterValue::String`] in every
137    /// comparison operation (equality, ordering, `contains`, `in`,
138    /// `startswith`, `endswith`, and truthiness), but are always redacted as
139    /// `[REDACTED]` when formatted with [`Display`] or [`Debug`], making it
140    /// impossible to leak the underlying secret through logging.
141    ///
142    /// Note that, like every comparison in this crate, secret comparisons are
143    /// not constant-time and should not be relied upon to defend against
144    /// timing attacks.
145    ///
146    /// ```
147    /// use filt_rs::FilterValue;
148    ///
149    /// let password = FilterValue::secret("hunter2");
150    ///
151    /// // Secrets compare exactly like strings (case-insensitively for equality)...
152    /// assert_eq!(password, FilterValue::String("HUNTER2".into()));
153    /// assert!(password.contains(&"unter".into()));
154    ///
155    /// // ...but they are always redacted when formatted.
156    /// assert_eq!(password.to_string(), "[REDACTED]");
157    /// assert_eq!(format!("{password:?}"), "[REDACTED]");
158    /// ```
159    #[cfg(feature = "secrecy")]
160    pub fn secret(value: impl Into<String>) -> Self {
161        FilterValue::Secret(secrecy::SecretString::from(value.into()))
162    }
163
164    /// Determines whether this value is considered "truthy" by the filter language.
165    ///
166    /// Filters match an object when their expression evaluates to a truthy
167    /// value. [`FilterValue::Null`], `false`, `0`, empty strings, and empty
168    /// tuples are falsy; everything else is truthy.
169    ///
170    /// With the `chrono` feature enabled, datetimes are always truthy, while
171    /// durations are truthy if (and only if) they are non-zero.
172    ///
173    /// ```
174    /// use filt_rs::FilterValue;
175    ///
176    /// assert!(FilterValue::Bool(true).is_truthy());
177    /// assert!(FilterValue::String("hello".into()).is_truthy());
178    /// assert!(!FilterValue::Null.is_truthy());
179    /// assert!(!FilterValue::Number(0.0).is_truthy());
180    /// ```
181    pub fn is_truthy(&self) -> bool {
182        match self {
183            FilterValue::Null => false,
184            FilterValue::Bool(b) => *b,
185            FilterValue::Number(n) => *n != 0.0,
186            FilterValue::String(s) => !s.is_empty(),
187            FilterValue::Tuple(v) => !v.is_empty(),
188            #[cfg(feature = "secrecy")]
189            FilterValue::Secret(s) => !s.expose_secret().is_empty(),
190            #[cfg(feature = "chrono")]
191            FilterValue::DateTime(..) => true,
192            #[cfg(feature = "chrono")]
193            FilterValue::Duration(d) => !d.is_zero(),
194        }
195    }
196
197    /// Determines whether this value contains the provided value.
198    ///
199    /// For tuples, this checks whether any element is equal to `other`; for
200    /// strings, it performs a case-insensitive substring search. All other
201    /// combinations return `false`. This powers the `contains` and `in`
202    /// operators in the filter language.
203    ///
204    /// The string comparison case-folds both operands character-by-character
205    /// without allocating, using the same Unicode case-folding rules as the
206    /// rest of the filter language: all Greek sigma forms (`Σ`, `σ`, and the
207    /// final-position `ς`) are treated as equivalent regardless of where they
208    /// appear in a word, and multi-character folds such as `ß` → `ss`
209    /// participate fully.
210    ///
211    /// ```
212    /// use filt_rs::FilterValue;
213    ///
214    /// let haystack: FilterValue<'_> ="Hello World".into();
215    /// assert!(haystack.contains(&"world".into()));
216    ///
217    /// let tuple = FilterValue::Tuple(vec!["a".into(), "b".into()]);
218    /// assert!(tuple.contains(&"a".into()));
219    /// assert!(!tuple.contains(&"c".into()));
220    /// ```
221    pub fn contains(&self, other: &FilterValue<'a>) -> bool {
222        match (self, other) {
223            (FilterValue::Tuple(a), b) => a.iter().any(|ai| ai == b),
224            (FilterValue::String(a), FilterValue::String(b)) => caseless_contains(a, b),
225            #[cfg(feature = "secrecy")]
226            (FilterValue::Secret(a), FilterValue::Secret(b)) => {
227                caseless_contains(a.expose_secret(), b.expose_secret())
228            }
229            #[cfg(feature = "secrecy")]
230            (FilterValue::Secret(a), FilterValue::String(b)) => {
231                caseless_contains(a.expose_secret(), b)
232            }
233            #[cfg(feature = "secrecy")]
234            (FilterValue::String(a), FilterValue::Secret(b)) => {
235                caseless_contains(a, b.expose_secret())
236            }
237            _ => false,
238        }
239    }
240
241    /// Determines whether this value starts with the provided value.
242    ///
243    /// For strings, this performs a case-insensitive prefix test; for tuples,
244    /// it checks whether any element is equal to `other`. This powers the
245    /// `startswith` operator in the filter language.
246    ///
247    /// The string comparison case-folds both operands character-by-character
248    /// without allocating, using the same Unicode case-folding rules as the
249    /// rest of the filter language (see [`FilterValue::contains`]).
250    ///
251    /// ```
252    /// use filt_rs::FilterValue;
253    ///
254    /// let value: FilterValue<'_> ="Hello World".into();
255    /// assert!(value.startswith(&"hello".into()));
256    /// assert!(!value.startswith(&"world".into()));
257    /// ```
258    pub fn startswith(&self, other: &FilterValue<'a>) -> bool {
259        match (self, other) {
260            (FilterValue::Tuple(a), b) => a.iter().any(|ai| ai == b),
261            (FilterValue::String(a), FilterValue::String(b)) => caseless_starts_with(a, b),
262            #[cfg(feature = "secrecy")]
263            (FilterValue::Secret(a), FilterValue::Secret(b)) => {
264                caseless_starts_with(a.expose_secret(), b.expose_secret())
265            }
266            #[cfg(feature = "secrecy")]
267            (FilterValue::Secret(a), FilterValue::String(b)) => {
268                caseless_starts_with(a.expose_secret(), b)
269            }
270            #[cfg(feature = "secrecy")]
271            (FilterValue::String(a), FilterValue::Secret(b)) => {
272                caseless_starts_with(a, b.expose_secret())
273            }
274            _ => false,
275        }
276    }
277
278    /// Determines whether this value ends with the provided value.
279    ///
280    /// For strings, this performs a case-insensitive suffix test; for tuples,
281    /// it checks whether any element is equal to `other`. This powers the
282    /// `endswith` operator in the filter language.
283    ///
284    /// The string comparison case-folds both operands character-by-character
285    /// without allocating, using the same Unicode case-folding rules as the
286    /// rest of the filter language (see [`FilterValue::contains`]).
287    ///
288    /// ```
289    /// use filt_rs::FilterValue;
290    ///
291    /// let value: FilterValue<'_> ="Hello World".into();
292    /// assert!(value.endswith(&"WORLD".into()));
293    /// assert!(!value.endswith(&"hello".into()));
294    /// ```
295    pub fn endswith(&self, other: &FilterValue<'a>) -> bool {
296        match (self, other) {
297            (FilterValue::Tuple(a), b) => a.iter().any(|ai| ai == b),
298            (FilterValue::String(a), FilterValue::String(b)) => caseless_ends_with(a, b),
299            #[cfg(feature = "secrecy")]
300            (FilterValue::Secret(a), FilterValue::Secret(b)) => {
301                caseless_ends_with(a.expose_secret(), b.expose_secret())
302            }
303            #[cfg(feature = "secrecy")]
304            (FilterValue::Secret(a), FilterValue::String(b)) => {
305                caseless_ends_with(a.expose_secret(), b)
306            }
307            #[cfg(feature = "secrecy")]
308            (FilterValue::String(a), FilterValue::Secret(b)) => {
309                caseless_ends_with(a, b.expose_secret())
310            }
311            _ => false,
312        }
313    }
314
315    /// Determines whether this value is equal to the provided value, comparing
316    /// strings case-*sensitively*.
317    ///
318    /// This is the case-sensitive counterpart of the `==` operator (and the
319    /// [`PartialEq`] implementation): tuples compare their elements with
320    /// `eq_cs` recursively, and all other variants behave exactly as `==`
321    /// does. It underpins tuple membership for the `contains_cs` and `in_cs`
322    /// operators in the filter language.
323    ///
324    /// ```
325    /// use filt_rs::FilterValue;
326    ///
327    /// let value: FilterValue<'_> ="Hello".into();
328    /// assert!(value.eq_cs(&"Hello".into()));
329    /// assert!(!value.eq_cs(&"hello".into()));
330    /// ```
331    pub fn eq_cs(&self, other: &FilterValue<'a>) -> bool {
332        match (self, other) {
333            (FilterValue::String(a), FilterValue::String(b)) => a == b,
334            #[cfg(feature = "secrecy")]
335            (FilterValue::Secret(a), FilterValue::Secret(b)) => {
336                a.expose_secret() == b.expose_secret()
337            }
338            #[cfg(feature = "secrecy")]
339            (FilterValue::Secret(a), FilterValue::String(b)) => a.expose_secret() == b,
340            #[cfg(feature = "secrecy")]
341            (FilterValue::String(a), FilterValue::Secret(b)) => a == b.expose_secret(),
342            (FilterValue::Tuple(a), FilterValue::Tuple(b)) => {
343                a.len() == b.len() && a.iter().zip(b.iter()).all(|(a, b)| a.eq_cs(b))
344            }
345            _ => self == other,
346        }
347    }
348
349    /// Determines whether this value contains the provided value, comparing
350    /// strings case-*sensitively*.
351    ///
352    /// This is the case-sensitive counterpart of [`FilterValue::contains`]:
353    /// tuples check whether any element is [`eq_cs`](FilterValue::eq_cs) to
354    /// `other`, strings perform an exact substring search, and all other
355    /// combinations return `false`. This powers the `contains_cs` and `in_cs`
356    /// operators in the filter language.
357    ///
358    /// ```
359    /// use filt_rs::FilterValue;
360    ///
361    /// let haystack: FilterValue<'_> ="Hello World".into();
362    /// assert!(haystack.contains_cs(&"World".into()));
363    /// assert!(!haystack.contains_cs(&"world".into()));
364    /// ```
365    pub fn contains_cs(&self, other: &FilterValue<'a>) -> bool {
366        match (self, other) {
367            (FilterValue::Tuple(a), b) => a.iter().any(|ai| ai.eq_cs(b)),
368            (FilterValue::String(a), FilterValue::String(b)) => a.contains(b.as_ref()),
369            #[cfg(feature = "secrecy")]
370            (FilterValue::Secret(a), FilterValue::Secret(b)) => {
371                a.expose_secret().contains(b.expose_secret())
372            }
373            #[cfg(feature = "secrecy")]
374            (FilterValue::Secret(a), FilterValue::String(b)) => {
375                a.expose_secret().contains(b.as_ref())
376            }
377            #[cfg(feature = "secrecy")]
378            (FilterValue::String(a), FilterValue::Secret(b)) => a.contains(b.expose_secret()),
379            _ => false,
380        }
381    }
382
383    /// Determines whether this value starts with the provided value, comparing
384    /// strings case-*sensitively*.
385    ///
386    /// This is the case-sensitive counterpart of [`FilterValue::startswith`],
387    /// powering the `startswith_cs` operator in the filter language.
388    ///
389    /// ```
390    /// use filt_rs::FilterValue;
391    ///
392    /// let value: FilterValue<'_> ="Hello World".into();
393    /// assert!(value.startswith_cs(&"Hello".into()));
394    /// assert!(!value.startswith_cs(&"hello".into()));
395    /// ```
396    pub fn startswith_cs(&self, other: &FilterValue<'a>) -> bool {
397        match (self, other) {
398            (FilterValue::Tuple(a), b) => a.iter().any(|ai| ai.eq_cs(b)),
399            #[cfg(feature = "secrecy")]
400            (FilterValue::Secret(a), FilterValue::Secret(b)) => {
401                a.expose_secret().starts_with(b.expose_secret())
402            }
403            #[cfg(feature = "secrecy")]
404            (FilterValue::Secret(a), FilterValue::String(b)) => {
405                a.expose_secret().starts_with(b.as_ref())
406            }
407            #[cfg(feature = "secrecy")]
408            (FilterValue::String(a), FilterValue::Secret(b)) => a.starts_with(b.expose_secret()),
409            (FilterValue::String(a), FilterValue::String(b)) => a.starts_with(b.as_ref()),
410            _ => false,
411        }
412    }
413
414    /// Determines whether this value ends with the provided value, comparing
415    /// strings case-*sensitively*.
416    ///
417    /// This is the case-sensitive counterpart of [`FilterValue::endswith`],
418    /// powering the `endswith_cs` operator in the filter language.
419    ///
420    /// ```
421    /// use filt_rs::FilterValue;
422    ///
423    /// let value: FilterValue<'_> ="Hello World".into();
424    /// assert!(value.endswith_cs(&"World".into()));
425    /// assert!(!value.endswith_cs(&"WORLD".into()));
426    /// ```
427    pub fn endswith_cs(&self, other: &FilterValue<'a>) -> bool {
428        match (self, other) {
429            (FilterValue::Tuple(a), b) => a.iter().any(|ai| ai.eq_cs(b)),
430            #[cfg(feature = "secrecy")]
431            (FilterValue::Secret(a), FilterValue::Secret(b)) => {
432                a.expose_secret().ends_with(b.expose_secret())
433            }
434            #[cfg(feature = "secrecy")]
435            (FilterValue::Secret(a), FilterValue::String(b)) => {
436                a.expose_secret().ends_with(b.as_ref())
437            }
438            #[cfg(feature = "secrecy")]
439            (FilterValue::String(a), FilterValue::Secret(b)) => a.ends_with(b.expose_secret()),
440            (FilterValue::String(a), FilterValue::String(b)) => a.ends_with(b.as_ref()),
441            _ => false,
442        }
443    }
444}
445
446impl<'a> PartialEq for FilterValue<'a> {
447    fn eq(&self, other: &Self) -> bool {
448        match (self, other) {
449            (FilterValue::Null, FilterValue::Null) => true,
450            (FilterValue::Bool(a), FilterValue::Bool(b)) => a == b,
451            (FilterValue::Number(a), FilterValue::Number(b)) => a == b,
452            (FilterValue::String(a), FilterValue::String(b)) => caseless_eq(a, b),
453            (FilterValue::Tuple(a), FilterValue::Tuple(b)) => {
454                a.len() == b.len() && a.iter().zip(b.iter()).all(|(a, b)| a == b)
455            }
456            #[cfg(feature = "secrecy")]
457            (FilterValue::Secret(a), FilterValue::Secret(b)) => {
458                caseless_eq(a.expose_secret(), b.expose_secret())
459            }
460            #[cfg(feature = "secrecy")]
461            (FilterValue::Secret(a), FilterValue::String(b)) => caseless_eq(a.expose_secret(), b),
462            #[cfg(feature = "secrecy")]
463            (FilterValue::String(a), FilterValue::Secret(b)) => caseless_eq(a, b.expose_secret()),
464            #[cfg(feature = "chrono")]
465            (FilterValue::DateTime(a), FilterValue::DateTime(b)) => a == b,
466            #[cfg(feature = "chrono")]
467            (FilterValue::Duration(a), FilterValue::Duration(b)) => a == b,
468            _ => false,
469        }
470    }
471}
472
473impl<'a> PartialOrd for FilterValue<'a> {
474    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
475        match (self, other) {
476            (FilterValue::Null, FilterValue::Null) => Some(Ordering::Equal),
477            (FilterValue::Bool(a), FilterValue::Bool(b)) => a.partial_cmp(b),
478            (FilterValue::Number(a), FilterValue::Number(b)) => a.partial_cmp(b),
479            (FilterValue::String(a), FilterValue::String(b)) => a.partial_cmp(b),
480            (FilterValue::Tuple(a), FilterValue::Tuple(b)) => {
481                if a.len() != b.len() {
482                    a.len().partial_cmp(&b.len())
483                } else {
484                    a.iter()
485                        .zip(b.iter())
486                        .map(|(x, y)| x.partial_cmp(y))
487                        .find(|&cmp| cmp != Some(Ordering::Equal))
488                        .unwrap_or(Some(Ordering::Equal))
489                }
490            }
491            #[cfg(feature = "secrecy")]
492            (FilterValue::Secret(a), FilterValue::Secret(b)) => {
493                a.expose_secret().partial_cmp(b.expose_secret())
494            }
495            #[cfg(feature = "secrecy")]
496            (FilterValue::Secret(a), FilterValue::String(b)) => {
497                a.expose_secret().partial_cmp(b.as_ref())
498            }
499            #[cfg(feature = "secrecy")]
500            (FilterValue::String(a), FilterValue::Secret(b)) => {
501                a.as_ref().partial_cmp(b.expose_secret())
502            }
503            #[cfg(feature = "chrono")]
504            (FilterValue::DateTime(a), FilterValue::DateTime(b)) => a.partial_cmp(b),
505            #[cfg(feature = "chrono")]
506            (FilterValue::Duration(a), FilterValue::Duration(b)) => a.partial_cmp(b),
507            _ => None, // Return None for non-comparable types
508        }
509    }
510
511    fn lt(&self, other: &Self) -> bool {
512        match (self, other) {
513            (FilterValue::Null, FilterValue::Null) => true,
514            (FilterValue::Bool(a), FilterValue::Bool(b)) => a < b,
515            (FilterValue::Number(a), FilterValue::Number(b)) => a < b,
516            (FilterValue::String(a), FilterValue::String(b)) => a < b,
517            (FilterValue::Tuple(a), FilterValue::Tuple(b)) => {
518                a.len() <= b.len() && a.iter().zip(b.iter()).all(|(a, b)| a < b)
519            }
520            #[cfg(feature = "secrecy")]
521            (FilterValue::Secret(a), FilterValue::Secret(b)) => {
522                a.expose_secret() < b.expose_secret()
523            }
524            #[cfg(feature = "secrecy")]
525            (FilterValue::Secret(a), FilterValue::String(b)) => a.expose_secret() < b.as_ref(),
526            #[cfg(feature = "secrecy")]
527            (FilterValue::String(a), FilterValue::Secret(b)) => a.as_ref() < b.expose_secret(),
528            #[cfg(feature = "chrono")]
529            (FilterValue::DateTime(a), FilterValue::DateTime(b)) => a < b,
530            #[cfg(feature = "chrono")]
531            (FilterValue::Duration(a), FilterValue::Duration(b)) => a < b,
532            _ => false,
533        }
534    }
535
536    fn le(&self, other: &Self) -> bool {
537        match (self, other) {
538            (FilterValue::Null, FilterValue::Null) => true,
539            (FilterValue::Bool(a), FilterValue::Bool(b)) => a <= b,
540            (FilterValue::Number(a), FilterValue::Number(b)) => a <= b,
541            (FilterValue::String(a), FilterValue::String(b)) => a <= b,
542            (FilterValue::Tuple(a), FilterValue::Tuple(b)) => {
543                a.len() <= b.len() && a.iter().zip(b.iter()).all(|(a, b)| a <= b)
544            }
545            #[cfg(feature = "secrecy")]
546            (FilterValue::Secret(a), FilterValue::Secret(b)) => {
547                a.expose_secret() <= b.expose_secret()
548            }
549            #[cfg(feature = "secrecy")]
550            (FilterValue::Secret(a), FilterValue::String(b)) => a.expose_secret() <= b.as_ref(),
551            #[cfg(feature = "secrecy")]
552            (FilterValue::String(a), FilterValue::Secret(b)) => a.as_ref() <= b.expose_secret(),
553            #[cfg(feature = "chrono")]
554            (FilterValue::DateTime(a), FilterValue::DateTime(b)) => a <= b,
555            #[cfg(feature = "chrono")]
556            (FilterValue::Duration(a), FilterValue::Duration(b)) => a <= b,
557            _ => false,
558        }
559    }
560
561    fn gt(&self, other: &Self) -> bool {
562        match (self, other) {
563            (FilterValue::Null, FilterValue::Null) => true,
564            (FilterValue::Bool(a), FilterValue::Bool(b)) => a > b,
565            (FilterValue::Number(a), FilterValue::Number(b)) => a > b,
566            (FilterValue::String(a), FilterValue::String(b)) => a > b,
567            (FilterValue::Tuple(a), FilterValue::Tuple(b)) => {
568                a.len() >= b.len() && a.iter().zip(b.iter()).all(|(a, b)| a > b)
569            }
570            #[cfg(feature = "secrecy")]
571            (FilterValue::Secret(a), FilterValue::Secret(b)) => {
572                a.expose_secret() > b.expose_secret()
573            }
574            #[cfg(feature = "secrecy")]
575            (FilterValue::Secret(a), FilterValue::String(b)) => a.expose_secret() > b.as_ref(),
576            #[cfg(feature = "secrecy")]
577            (FilterValue::String(a), FilterValue::Secret(b)) => a.as_ref() > b.expose_secret(),
578            #[cfg(feature = "chrono")]
579            (FilterValue::DateTime(a), FilterValue::DateTime(b)) => a > b,
580            #[cfg(feature = "chrono")]
581            (FilterValue::Duration(a), FilterValue::Duration(b)) => a > b,
582            _ => false,
583        }
584    }
585
586    fn ge(&self, other: &Self) -> bool {
587        match (self, other) {
588            (FilterValue::Null, FilterValue::Null) => true,
589            (FilterValue::Bool(a), FilterValue::Bool(b)) => a >= b,
590            (FilterValue::Number(a), FilterValue::Number(b)) => a >= b,
591            (FilterValue::String(a), FilterValue::String(b)) => a >= b,
592            (FilterValue::Tuple(a), FilterValue::Tuple(b)) => {
593                a.len() >= b.len() && a.iter().zip(b.iter()).all(|(a, b)| a >= b)
594            }
595            #[cfg(feature = "secrecy")]
596            (FilterValue::Secret(a), FilterValue::Secret(b)) => {
597                a.expose_secret() >= b.expose_secret()
598            }
599            #[cfg(feature = "secrecy")]
600            (FilterValue::Secret(a), FilterValue::String(b)) => a.expose_secret() >= b.as_ref(),
601            #[cfg(feature = "secrecy")]
602            (FilterValue::String(a), FilterValue::Secret(b)) => a.as_ref() >= b.expose_secret(),
603            #[cfg(feature = "chrono")]
604            (FilterValue::DateTime(a), FilterValue::DateTime(b)) => a >= b,
605            #[cfg(feature = "chrono")]
606            (FilterValue::Duration(a), FilterValue::Duration(b)) => a >= b,
607            _ => false,
608        }
609    }
610}
611
612impl<'a> Display for FilterValue<'a> {
613    /// Formats the value as it would appear within a filter expression.
614    ///
615    /// ```
616    /// use filt_rs::FilterValue;
617    ///
618    /// let value = FilterValue::Tuple(vec!["a".into(), 1.into(), FilterValue::Null]);
619    /// assert_eq!(value.to_string(), r#"["a", 1, null]"#);
620    /// ```
621    ///
622    /// Secret values (available with the `secrecy` feature) are always
623    /// formatted as `[REDACTED]`, never as their underlying string.
624    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
625        match self {
626            FilterValue::Null => write!(f, "null"),
627            FilterValue::Bool(b) => write!(f, "{}", b),
628            FilterValue::Number(n) => write!(f, "{}", n),
629            FilterValue::String(s) => {
630                write!(f, "\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
631            }
632            FilterValue::Tuple(v) => {
633                write!(f, "[")?;
634                for (i, value) in v.iter().enumerate() {
635                    if i > 0 {
636                        write!(f, ", ")?;
637                    }
638                    write!(f, "{}", value)?;
639                }
640                write!(f, "]")
641            }
642            #[cfg(feature = "secrecy")]
643            FilterValue::Secret(_) => write!(f, "[REDACTED]"),
644            #[cfg(feature = "chrono")]
645            FilterValue::DateTime(dt) => {
646                write!(
647                    f,
648                    "{}",
649                    dt.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true)
650                )
651            }
652            #[cfg(feature = "chrono")]
653            FilterValue::Duration(d) => format_duration(f, d),
654        }
655    }
656}
657
658/// Formats a duration in the same humanized compact form used by duration
659/// literals in the filter language (e.g. `1h30m`), with millisecond precision.
660#[cfg(feature = "chrono")]
661fn format_duration(
662    f: &mut std::fmt::Formatter<'_>,
663    duration: &chrono::Duration,
664) -> std::fmt::Result {
665    let mut remaining_ms = duration.num_milliseconds();
666    if remaining_ms == 0 {
667        return write!(f, "0s");
668    }
669
670    if remaining_ms < 0 {
671        write!(f, "-")?;
672        remaining_ms = remaining_ms.checked_neg().unwrap_or(i64::MAX);
673    }
674
675    for (unit, size_ms) in [
676        ("w", 604_800_000),
677        ("d", 86_400_000),
678        ("h", 3_600_000),
679        ("m", 60_000),
680        ("s", 1_000),
681        ("ms", 1),
682    ] {
683        let count = remaining_ms / size_ms;
684        if count > 0 {
685            write!(f, "{count}{unit}")?;
686            remaining_ms %= size_ms;
687        }
688    }
689
690    Ok(())
691}
692
693impl<'a> Debug for FilterValue<'a> {
694    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
695        write!(f, "{}", self)
696    }
697}
698
699impl<'a> From<bool> for FilterValue<'a> {
700    fn from(b: bool) -> Self {
701        FilterValue::Bool(b)
702    }
703}
704
705macro_rules! number {
706    ($t:ty) => {
707        impl<'a> From<$t> for FilterValue<'a> {
708            fn from(n: $t) -> Self {
709                FilterValue::Number(n as f64)
710            }
711        }
712    };
713}
714
715number!(i8);
716number!(u8);
717number!(i16);
718number!(u16);
719number!(f32);
720number!(i32);
721number!(u32);
722number!(f64);
723number!(i64);
724number!(u64);
725
726impl<'a> From<&'a str> for FilterValue<'a> {
727    /// Borrows the string slice rather than copying it, so a [`Filterable::get`]
728    /// implementation returning `self.field.as_str().into()` performs no
729    /// allocation. Use [`From<String>`] (or `.to_string().into()`) when you
730    /// need the value to own its data instead.
731    fn from(s: &'a str) -> Self {
732        FilterValue::String(Cow::Borrowed(s))
733    }
734}
735
736impl<'a> From<String> for FilterValue<'a> {
737    fn from(s: String) -> Self {
738        FilterValue::String(Cow::Owned(s))
739    }
740}
741
742#[cfg(feature = "secrecy")]
743impl<'a> From<secrecy::SecretString> for FilterValue<'a> {
744    /// Wraps a [`secrecy::SecretString`] as a [`FilterValue::Secret`].
745    ///
746    /// ```
747    /// use filt_rs::FilterValue;
748    /// use secrecy::SecretString;
749    ///
750    /// let value: FilterValue<'_> =SecretString::from("hunter2").into();
751    /// assert_eq!(value, FilterValue::String("hunter2".into()));
752    /// assert_eq!(value.to_string(), "[REDACTED]");
753    /// ```
754    fn from(s: secrecy::SecretString) -> Self {
755        FilterValue::Secret(s)
756    }
757}
758
759/// Converts a UTC datetime into a [`FilterValue::DateTime`].
760///
761/// *Only available when the `chrono` crate feature is enabled.*
762///
763/// ```
764/// use filt_rs::FilterValue;
765///
766/// let timestamp = chrono::Utc::now();
767/// let value: FilterValue<'_> =timestamp.into();
768/// assert_eq!(value, FilterValue::DateTime(timestamp));
769/// ```
770#[cfg(feature = "chrono")]
771impl<'a> From<chrono::DateTime<chrono::Utc>> for FilterValue<'a> {
772    fn from(dt: chrono::DateTime<chrono::Utc>) -> Self {
773        FilterValue::DateTime(dt)
774    }
775}
776
777/// Converts a [`chrono::Duration`] into a [`FilterValue::Duration`].
778///
779/// *Only available when the `chrono` crate feature is enabled.*
780///
781/// ```
782/// use filt_rs::FilterValue;
783///
784/// let value: FilterValue<'_> =chrono::Duration::minutes(90).into();
785/// assert_eq!(value.to_string(), "1h30m");
786/// ```
787#[cfg(feature = "chrono")]
788impl<'a> From<chrono::Duration> for FilterValue<'a> {
789    fn from(d: chrono::Duration) -> Self {
790        FilterValue::Duration(d)
791    }
792}
793
794/// Converts a [`std::time::SystemTime`] into a [`FilterValue::DateTime`],
795/// making it easy to expose timestamps from the standard library (such as
796/// file modification times) without converting them yourself.
797///
798/// *Only available when the `chrono` crate feature is enabled.*
799///
800/// ```
801/// use filt_rs::FilterValue;
802/// use std::time::SystemTime;
803///
804/// let value: FilterValue<'_> =SystemTime::UNIX_EPOCH.into();
805/// assert_eq!(value.to_string(), "1970-01-01T00:00:00Z");
806/// ```
807#[cfg(feature = "chrono")]
808impl<'a> From<std::time::SystemTime> for FilterValue<'a> {
809    fn from(t: std::time::SystemTime) -> Self {
810        FilterValue::DateTime(t.into())
811    }
812}
813
814impl<'a, T> From<Option<T>> for FilterValue<'a>
815where
816    T: Into<FilterValue<'a>>,
817{
818    fn from(o: Option<T>) -> Self {
819        o.map_or(FilterValue::Null, Into::into)
820    }
821}
822
823impl<'a> From<Vec<FilterValue<'a>>> for FilterValue<'a> {
824    fn from(v: Vec<FilterValue<'a>>) -> Self {
825        FilterValue::Tuple(v)
826    }
827}
828
829#[cfg(test)]
830mod tests {
831    use rstest::rstest;
832
833    use super::*;
834
835    #[rstest]
836    #[case(FilterValue::Null, false)]
837    #[case(FilterValue::Bool(false), false)]
838    #[case(FilterValue::Bool(true), true)]
839    #[case(FilterValue::Number(0.0), false)]
840    #[case(FilterValue::Number(1.0), true)]
841    #[case(FilterValue::String("".into()), false)]
842    #[case(FilterValue::String("hello".into()), true)]
843    #[case(FilterValue::Tuple(vec![]), false)]
844    #[case(FilterValue::Tuple(vec![FilterValue::Bool(true)]), true)]
845    fn test_truthy(#[case] value: FilterValue<'_>, #[case] truthy: bool) {
846        assert_eq!(value.is_truthy(), truthy);
847    }
848
849    #[test]
850    fn test_bool_comparison() {
851        assert!(FilterValue::Bool(false) < FilterValue::Bool(true));
852        assert!(FilterValue::Bool(true) > FilterValue::Bool(false));
853        assert_eq!(FilterValue::Bool(true), FilterValue::Bool(true));
854        assert_eq!(FilterValue::Bool(false), FilterValue::Bool(false));
855    }
856
857    #[test]
858    fn test_number_comparison() {
859        assert!(FilterValue::Number(1.0) < FilterValue::Number(2.0));
860        assert!(FilterValue::Number(2.0) > FilterValue::Number(1.0));
861        assert_eq!(FilterValue::Number(2.0), FilterValue::Number(2.0));
862    }
863
864    #[test]
865    fn test_string_comparison() {
866        assert!(FilterValue::String("abc".into()) < FilterValue::String("xyz".into()));
867        assert!(FilterValue::String("xyz".into()) > FilterValue::String("abc".into()));
868        assert_eq!(
869            FilterValue::String("abc".into()),
870            FilterValue::String("abc".into())
871        );
872    }
873
874    #[test]
875    fn test_string_equality_is_case_insensitive() {
876        assert_eq!(
877            FilterValue::String("Hello World".into()),
878            FilterValue::String("hello world".into())
879        );
880        assert_ne!(
881            FilterValue::String("Hello World".into()),
882            FilterValue::String("goodbye world".into())
883        );
884
885        // Equality folds case using the language's Unicode case-folding
886        // rules, including non-ASCII characters and multi-character folds.
887        assert_eq!(
888            FilterValue::String("JÜRGEN".into()),
889            FilterValue::String("jürgen".into())
890        );
891        assert_eq!(
892            FilterValue::String("ΛΟΓΟΣ".into()),
893            FilterValue::String("λογος".into())
894        );
895        assert_eq!(
896            FilterValue::String("straße".into()),
897            FilterValue::String("STRASSE".into())
898        );
899    }
900
901    #[test]
902    fn test_tuple_comparison() {
903        // The `<` and `>` operators require every paired element to compare
904        // accordingly, while ordering between different-length tuples is
905        // driven by their lengths.
906        assert!(
907            FilterValue::Tuple(vec![1.into(), 2.into()])
908                < FilterValue::Tuple(vec![3.into(), 4.into()])
909        );
910        assert!(
911            FilterValue::Tuple(vec![3.into(), 4.into()])
912                > FilterValue::Tuple(vec![1.into(), 2.into()])
913        );
914
915        let short = FilterValue::Tuple(vec![1.into()]);
916        let long = FilterValue::Tuple(vec![1.into(), 2.into()]);
917        assert_eq!(short.partial_cmp(&long), Some(Ordering::Less));
918        assert_eq!(long.partial_cmp(&short), Some(Ordering::Greater));
919
920        assert_eq!(
921            FilterValue::Tuple(vec![1.into(), 2.into()]),
922            FilterValue::Tuple(vec![1.into(), 2.into()])
923        );
924        assert_ne!(
925            FilterValue::Tuple(vec![1.into(), 2.into()]),
926            FilterValue::Tuple(vec![2.into(), 1.into()])
927        );
928    }
929
930    #[rstest]
931    #[case(FilterValue::Null, FilterValue::Bool(true))]
932    #[case(FilterValue::Bool(true), FilterValue::Number(1.0))]
933    #[case(FilterValue::Number(1.0), FilterValue::String("1".into()))]
934    #[case(FilterValue::String("a".into()), FilterValue::Tuple(vec!["a".into()]))]
935    fn test_mismatched_types_are_not_equal_or_ordered(
936        #[case] left: FilterValue<'_>,
937        #[case] right: FilterValue<'_>,
938    ) {
939        assert_ne!(left, right);
940        assert_eq!(left.partial_cmp(&right), None);
941        assert!(!left.lt(&right));
942        assert!(!left.le(&right));
943        assert!(!left.gt(&right));
944        assert!(!left.ge(&right));
945    }
946
947    #[rstest]
948    #[case(true.into(), FilterValue::Bool(true))]
949    #[case(42i8.into(), FilterValue::Number(42.0))]
950    #[case(42u8.into(), FilterValue::Number(42.0))]
951    #[case(42i16.into(), FilterValue::Number(42.0))]
952    #[case(42u16.into(), FilterValue::Number(42.0))]
953    #[case(42i32.into(), FilterValue::Number(42.0))]
954    #[case(42u32.into(), FilterValue::Number(42.0))]
955    #[case(42i64.into(), FilterValue::Number(42.0))]
956    #[case(42u64.into(), FilterValue::Number(42.0))]
957    #[case(4.2f32.into(), FilterValue::Number(4.2f32 as f64))]
958    #[case(4.2f64.into(), FilterValue::Number(4.2))]
959    #[case("hello".into(), FilterValue::String("hello".into()))]
960    #[case(String::from("hello").into(), FilterValue::String("hello".into()))]
961    #[case(Some(1).into(), FilterValue::Number(1.0))]
962    #[case(None::<i32>.into(), FilterValue::Null)]
963    #[case(vec![FilterValue::Null].into(), FilterValue::Tuple(vec![FilterValue::Null]))]
964    fn test_conversions(#[case] converted: FilterValue<'_>, #[case] expected: FilterValue<'_>) {
965        assert_eq!(converted, expected);
966    }
967
968    #[rstest]
969    #[case(FilterValue::Null, "null")]
970    #[case(FilterValue::Bool(true), "true")]
971    #[case(FilterValue::Bool(false), "false")]
972    #[case(FilterValue::Number(1.5), "1.5")]
973    #[case(FilterValue::String("hello".into()), "\"hello\"")]
974    #[case(FilterValue::String("say \"hi\"".into()), "\"say \\\"hi\\\"\"")]
975    #[case(FilterValue::String("back\\slash".into()), "\"back\\\\slash\"")]
976    #[case(FilterValue::Tuple(vec![]), "[]")]
977    #[case(FilterValue::Tuple(vec![1.into(), "a".into()]), "[1, \"a\"]")]
978    fn test_display(#[case] value: FilterValue<'_>, #[case] expected: &str) {
979        assert_eq!(value.to_string(), expected);
980        assert_eq!(format!("{value:?}"), expected);
981    }
982
983    #[rstest]
984    #[case("Hello World".into(), "world".into(), true)]
985    #[case("Hello World".into(), "WORLD".into(), true)]
986    #[case("Hello World".into(), "mars".into(), false)]
987    #[case(FilterValue::Tuple(vec!["a".into(), "b".into()]), "A".into(), true)]
988    #[case(FilterValue::Tuple(vec!["a".into(), "b".into()]), "c".into(), false)]
989    #[case(FilterValue::Tuple(vec![]), FilterValue::Null, false)]
990    #[case(FilterValue::Null, FilterValue::Null, false)]
991    #[case(FilterValue::Number(12.0), FilterValue::Number(2.0), false)]
992    fn test_contains(
993        #[case] value: FilterValue<'_>,
994        #[case] other: FilterValue<'_>,
995        #[case] expected: bool,
996    ) {
997        assert_eq!(value.contains(&other), expected);
998    }
999
1000    #[rstest]
1001    #[case("Hello World".into(), "hello".into(), true)]
1002    #[case("Hello World".into(), "world".into(), false)]
1003    #[case(FilterValue::Tuple(vec!["a".into()]), "a".into(), true)]
1004    #[case(FilterValue::Null, "a".into(), false)]
1005    #[case("Hello".into(), FilterValue::Null, false)]
1006    fn test_startswith(
1007        #[case] value: FilterValue<'_>,
1008        #[case] other: FilterValue<'_>,
1009        #[case] expected: bool,
1010    ) {
1011        assert_eq!(value.startswith(&other), expected);
1012    }
1013
1014    #[rstest]
1015    #[case("Hello World".into(), "world".into(), true)]
1016    #[case("Hello World".into(), "hello".into(), false)]
1017    #[case(FilterValue::Tuple(vec!["a".into()]), "a".into(), true)]
1018    #[case(FilterValue::Null, "a".into(), false)]
1019    #[case("Hello".into(), FilterValue::Null, false)]
1020    fn test_endswith(
1021        #[case] value: FilterValue<'_>,
1022        #[case] other: FilterValue<'_>,
1023        #[case] expected: bool,
1024    ) {
1025        assert_eq!(value.endswith(&other), expected);
1026    }
1027
1028    #[test]
1029    fn test_default_is_null() {
1030        assert_eq!(FilterValue::default(), FilterValue::Null);
1031    }
1032
1033    #[rstest]
1034    #[case("Hello".into(), "Hello".into(), true)]
1035    #[case("Hello".into(), "hello".into(), false)]
1036    #[case("straße".into(), "STRASSE".into(), false)] // no case folding at all
1037    #[case(FilterValue::Null, FilterValue::Null, true)]
1038    #[case(FilterValue::Bool(true), FilterValue::Bool(true), true)]
1039    #[case(FilterValue::Number(1.0), FilterValue::Number(1.0), true)]
1040    #[case(FilterValue::Tuple(vec!["A".into()]), FilterValue::Tuple(vec!["A".into()]), true)]
1041    #[case(FilterValue::Tuple(vec!["A".into()]), FilterValue::Tuple(vec!["a".into()]), false)]
1042    #[case("1".into(), FilterValue::Number(1.0), false)]
1043    fn test_eq_cs(
1044        #[case] left: FilterValue<'_>,
1045        #[case] right: FilterValue<'_>,
1046        #[case] expected: bool,
1047    ) {
1048        assert_eq!(left.eq_cs(&right), expected);
1049        assert_eq!(right.eq_cs(&left), expected);
1050    }
1051
1052    #[rstest]
1053    #[case("Hello World".into(), "World".into(), true)]
1054    #[case("Hello World".into(), "world".into(), false)]
1055    #[case(FilterValue::Tuple(vec!["a".into(), "B".into()]), "B".into(), true)]
1056    #[case(FilterValue::Tuple(vec!["a".into(), "B".into()]), "b".into(), false)]
1057    #[case(FilterValue::Null, FilterValue::Null, false)]
1058    #[case(FilterValue::Number(12.0), FilterValue::Number(2.0), false)]
1059    fn test_contains_cs(
1060        #[case] value: FilterValue<'_>,
1061        #[case] other: FilterValue<'_>,
1062        #[case] expected: bool,
1063    ) {
1064        assert_eq!(value.contains_cs(&other), expected);
1065    }
1066
1067    #[rstest]
1068    #[case("Hello World".into(), "Hello".into(), true)]
1069    #[case("Hello World".into(), "hello".into(), false)]
1070    #[case(FilterValue::Tuple(vec!["A".into()]), "A".into(), true)]
1071    #[case(FilterValue::Tuple(vec!["A".into()]), "a".into(), false)]
1072    #[case(FilterValue::Null, "a".into(), false)]
1073    fn test_startswith_cs(
1074        #[case] value: FilterValue<'_>,
1075        #[case] other: FilterValue<'_>,
1076        #[case] expected: bool,
1077    ) {
1078        assert_eq!(value.startswith_cs(&other), expected);
1079    }
1080
1081    #[rstest]
1082    #[case("Hello World".into(), "World".into(), true)]
1083    #[case("Hello World".into(), "WORLD".into(), false)]
1084    #[case(FilterValue::Tuple(vec!["A".into()]), "A".into(), true)]
1085    #[case(FilterValue::Tuple(vec!["A".into()]), "a".into(), false)]
1086    #[case(FilterValue::Null, "a".into(), false)]
1087    fn test_endswith_cs(
1088        #[case] value: FilterValue<'_>,
1089        #[case] other: FilterValue<'_>,
1090        #[case] expected: bool,
1091    ) {
1092        assert_eq!(value.endswith_cs(&other), expected);
1093    }
1094
1095    /// The case-insensitive string operations treat all Greek sigma forms
1096    /// (`Σ`, `σ`, and the word-final `ς`) as equivalent, regardless of where
1097    /// they appear within a word.
1098    ///
1099    /// This intentionally diverges from [`str::to_lowercase`]'s context
1100    /// sensitive final-sigma rule (which would, for example, consider
1101    /// `"ΛΟΓΟΣ"` *not* to end with `"Σ"` because the haystack lowercases to
1102    /// `"λογος"` while the needle lowercases to `"σ"`). Folding every sigma
1103    /// to `σ` mirrors Unicode simple case folding and gives
1104    /// position-independent results.
1105    #[rstest]
1106    #[case("ΛΟΓΟΣ", "Σ")] // upper-case needle vs word-final position
1107    #[case("ΛΟΓΟΣ", "ς")] // final-sigma needle
1108    #[case("ΛΟΓΟΣ", "σ")] // regular-sigma needle
1109    #[case("λογος", "Σ")] // word-final sigma in the haystack
1110    fn test_greek_sigma_forms_are_equivalent(#[case] haystack: &str, #[case] needle: &str) {
1111        let haystack: FilterValue<'_> = haystack.into();
1112        let needle: FilterValue<'_> = needle.into();
1113
1114        assert!(haystack.endswith(&needle));
1115        assert!(haystack.contains(&needle));
1116    }
1117
1118    /// Characters whose case-folded form expands to multiple characters
1119    /// (such as `İ`, which folds to `i` followed by a combining dot above,
1120    /// or `ß`, which folds to `ss`) participate fully in the comparison.
1121    #[rstest]
1122    #[case("İstanbul", "i\u{307}stanbul", true)] // expanded folded form
1123    #[case("İstanbul", "\u{307}stanbul", true)] // matches mid-expansion, as str::contains would
1124    #[case("İstanbul", "istanbul", false)] // the combining mark is significant
1125    #[case("straße", "STRASSE", true)] // ß folds to ss
1126    #[case("groß", "ss", true)]
1127    #[case("gross", "ß", true)] // ...in the needle too
1128    fn test_multi_char_lowercase_expansions(
1129        #[case] haystack: &str,
1130        #[case] needle: &str,
1131        #[case] expected: bool,
1132    ) {
1133        let haystack: FilterValue<'_> = haystack.into();
1134        let needle: FilterValue<'_> = needle.into();
1135
1136        assert_eq!(haystack.contains(&needle), expected);
1137    }
1138
1139    #[cfg(feature = "secrecy")]
1140    mod secrecy_tests {
1141        use super::*;
1142
1143        #[rstest]
1144        #[case(FilterValue::secret(""), false)]
1145        #[case(FilterValue::secret("hunter2"), true)]
1146        fn test_secret_truthy(#[case] value: FilterValue<'_>, #[case] truthy: bool) {
1147            assert_eq!(value.is_truthy(), truthy);
1148        }
1149
1150        #[rstest]
1151        #[case(FilterValue::secret("hunter2"), FilterValue::secret("hunter2"), true)]
1152        #[case(FilterValue::secret("hunter2"), FilterValue::secret("HUNTER2"), true)]
1153        #[case(
1154            FilterValue::secret("hunter2"),
1155            FilterValue::secret("swordfish"),
1156            false
1157        )]
1158        #[case(FilterValue::secret("hunter2"), "hunter2".into(), true)]
1159        #[case(FilterValue::secret("hunter2"), "HUNTER2".into(), true)]
1160        #[case("HUNTER2".into(), FilterValue::secret("hunter2"), true)]
1161        #[case("swordfish".into(), FilterValue::secret("hunter2"), false)]
1162        fn test_secret_equality(
1163            #[case] left: FilterValue<'_>,
1164            #[case] right: FilterValue<'_>,
1165            #[case] equal: bool,
1166        ) {
1167            assert_eq!(left == right, equal);
1168            assert_eq!(left != right, !equal);
1169        }
1170
1171        #[rstest]
1172        #[case(FilterValue::secret("abc"), FilterValue::secret("xyz"))]
1173        #[case(FilterValue::secret("abc"), "xyz".into())]
1174        #[case("abc".into(), FilterValue::secret("xyz"))]
1175        fn test_secret_ordering(#[case] smaller: FilterValue<'_>, #[case] larger: FilterValue<'_>) {
1176            assert_eq!(smaller.partial_cmp(&larger), Some(Ordering::Less));
1177            assert_eq!(larger.partial_cmp(&smaller), Some(Ordering::Greater));
1178            assert!(smaller < larger);
1179            assert!(smaller <= larger);
1180            assert!(larger > smaller);
1181            assert!(larger >= smaller);
1182            assert!(!smaller.gt(&larger));
1183            assert!(!smaller.ge(&larger));
1184            assert!(!larger.lt(&smaller));
1185            assert!(!larger.le(&smaller));
1186        }
1187
1188        #[rstest]
1189        #[case(FilterValue::secret("Hello World"), "world".into(), true)]
1190        #[case(FilterValue::secret("Hello World"), "mars".into(), false)]
1191        #[case("Hello World".into(), FilterValue::secret("WORLD"), true)]
1192        #[case("Hello World".into(), FilterValue::secret("mars"), false)]
1193        #[case(FilterValue::secret("Hello World"), FilterValue::secret("WORLD"), true)]
1194        #[case(FilterValue::Tuple(vec![FilterValue::secret("a"), "b".into()]), "A".into(), true)]
1195        #[case(FilterValue::Tuple(vec!["a".into(), "b".into()]), FilterValue::secret("B"), true)]
1196        #[case(FilterValue::Tuple(vec!["a".into(), "b".into()]), FilterValue::secret("c"), false)]
1197        fn test_secret_contains(
1198            #[case] value: FilterValue<'_>,
1199            #[case] other: FilterValue<'_>,
1200            #[case] expected: bool,
1201        ) {
1202            assert_eq!(value.contains(&other), expected);
1203        }
1204
1205        #[rstest]
1206        #[case(FilterValue::secret("Hello World"), "hello".into(), true)]
1207        #[case(FilterValue::secret("Hello World"), "world".into(), false)]
1208        #[case("Hello World".into(), FilterValue::secret("HELLO"), true)]
1209        #[case("Hello World".into(), FilterValue::secret("world"), false)]
1210        #[case(FilterValue::secret("Hello World"), FilterValue::secret("HELLO"), true)]
1211        fn test_secret_startswith(
1212            #[case] value: FilterValue<'_>,
1213            #[case] other: FilterValue<'_>,
1214            #[case] expected: bool,
1215        ) {
1216            assert_eq!(value.startswith(&other), expected);
1217        }
1218
1219        #[rstest]
1220        #[case(FilterValue::secret("Hello World"), "WORLD".into(), true)]
1221        #[case(FilterValue::secret("Hello World"), "hello".into(), false)]
1222        #[case("Hello World".into(), FilterValue::secret("world"), true)]
1223        #[case("Hello World".into(), FilterValue::secret("hello"), false)]
1224        #[case(FilterValue::secret("Hello World"), FilterValue::secret("world"), true)]
1225        fn test_secret_endswith(
1226            #[case] value: FilterValue<'_>,
1227            #[case] other: FilterValue<'_>,
1228            #[case] expected: bool,
1229        ) {
1230            assert_eq!(value.endswith(&other), expected);
1231        }
1232
1233        #[rstest]
1234        #[case(FilterValue::Null)]
1235        #[case(FilterValue::Bool(true))]
1236        #[case(FilterValue::Number(1.0))]
1237        #[case(FilterValue::Tuple(vec!["hunter2".into()]))]
1238        fn test_secrets_are_not_equal_or_ordered_against_other_types(
1239            #[case] other: FilterValue<'_>,
1240        ) {
1241            let secret = FilterValue::secret("hunter2");
1242            assert_ne!(secret, other);
1243            assert_ne!(other, secret);
1244            assert_eq!(secret.partial_cmp(&other), None);
1245            assert_eq!(other.partial_cmp(&secret), None);
1246            assert!(!secret.lt(&other));
1247            assert!(!secret.le(&other));
1248            assert!(!secret.gt(&other));
1249            assert!(!secret.ge(&other));
1250        }
1251
1252        #[rstest]
1253        #[case(FilterValue::secret("hunter2"), "[REDACTED]")]
1254        #[case(FilterValue::secret(""), "[REDACTED]")]
1255        #[case(
1256            FilterValue::Tuple(vec!["a".into(), FilterValue::secret("hunter2"), 1.into()]),
1257            "[\"a\", [REDACTED], 1]"
1258        )]
1259        fn test_secret_display_is_redacted(#[case] value: FilterValue<'_>, #[case] expected: &str) {
1260            assert_eq!(value.to_string(), expected);
1261            assert_eq!(format!("{value:?}"), expected);
1262            assert!(!value.to_string().contains("hunter2"));
1263            assert!(!format!("{value:?}").contains("hunter2"));
1264        }
1265
1266        #[test]
1267        fn test_secret_conversions() {
1268            let secret: FilterValue<'_> = secrecy::SecretString::from("hunter2").into();
1269            assert_eq!(secret, FilterValue::secret("hunter2"));
1270            assert!(matches!(secret, FilterValue::Secret(_)));
1271            assert!(matches!(
1272                FilterValue::secret(String::from("hunter2")),
1273                FilterValue::Secret(_)
1274            ));
1275        }
1276
1277        /// For every comparison operation, a secret must behave exactly as the
1278        /// equivalent string would — whichever side of the operator it is on.
1279        #[rstest]
1280        #[case("hunter2", "hunter2")]
1281        #[case("hunter2", "HUNTER2")]
1282        #[case("hunter2", "swordfish")]
1283        #[case("abc", "abd")]
1284        #[case("abd", "abc")]
1285        #[case("Hello World", "WORLD")]
1286        #[case("Hello World", "hello")]
1287        #[case("", "")]
1288        #[case("", "a")]
1289        #[case("ÜBER", "über")]
1290        fn test_secrets_behave_exactly_like_strings(
1291            #[case] secret: &'static str,
1292            #[case] other: &'static str,
1293        ) {
1294            let as_secret = FilterValue::secret(secret);
1295            let as_string = FilterValue::String(secret.into());
1296            let other = FilterValue::String(other.into());
1297
1298            assert_eq!(
1299                as_secret == other,
1300                as_string == other,
1301                "{secret} == {other}"
1302            );
1303            assert_eq!(
1304                other == as_secret,
1305                other == as_string,
1306                "{other} == {secret}"
1307            );
1308            assert_eq!(
1309                as_secret.partial_cmp(&other),
1310                as_string.partial_cmp(&other),
1311                "{secret} cmp {other}"
1312            );
1313            assert_eq!(
1314                other.partial_cmp(&as_secret),
1315                other.partial_cmp(&as_string),
1316                "{other} cmp {secret}"
1317            );
1318            assert_eq!(as_secret < other, as_string < other, "{secret} < {other}");
1319            assert_eq!(other < as_secret, other < as_string, "{other} < {secret}");
1320            assert_eq!(
1321                as_secret <= other,
1322                as_string <= other,
1323                "{secret} <= {other}"
1324            );
1325            assert_eq!(
1326                other <= as_secret,
1327                other <= as_string,
1328                "{other} <= {secret}"
1329            );
1330            assert_eq!(as_secret > other, as_string > other, "{secret} > {other}");
1331            assert_eq!(other > as_secret, other > as_string, "{other} > {secret}");
1332            assert_eq!(
1333                as_secret >= other,
1334                as_string >= other,
1335                "{secret} >= {other}"
1336            );
1337            assert_eq!(
1338                other >= as_secret,
1339                other >= as_string,
1340                "{other} >= {secret}"
1341            );
1342            assert_eq!(
1343                as_secret.contains(&other),
1344                as_string.contains(&other),
1345                "{secret} contains {other}"
1346            );
1347            assert_eq!(
1348                other.contains(&as_secret),
1349                other.contains(&as_string),
1350                "{other} contains {secret}"
1351            );
1352            assert_eq!(
1353                as_secret.startswith(&other),
1354                as_string.startswith(&other),
1355                "{secret} starts with {other}"
1356            );
1357            assert_eq!(
1358                other.startswith(&as_secret),
1359                other.startswith(&as_string),
1360                "{other} starts with {secret}"
1361            );
1362            assert_eq!(
1363                as_secret.endswith(&other),
1364                as_string.endswith(&other),
1365                "{secret} ends with {other}"
1366            );
1367            assert_eq!(
1368                other.endswith(&as_secret),
1369                other.endswith(&as_string),
1370                "{other} ends with {secret}"
1371            );
1372            assert_eq!(
1373                as_secret.is_truthy(),
1374                as_string.is_truthy(),
1375                "{secret} is_truthy"
1376            );
1377        }
1378    }
1379
1380    #[cfg(feature = "chrono")]
1381    mod chrono_tests {
1382        use super::*;
1383        use chrono::{Duration, TimeZone, Utc};
1384
1385        fn datetime() -> chrono::DateTime<Utc> {
1386            Utc.with_ymd_and_hms(2026, 6, 12, 13, 30, 45).unwrap()
1387        }
1388
1389        #[test]
1390        fn test_truthiness() {
1391            assert!(FilterValue::DateTime(datetime()).is_truthy());
1392            assert!(FilterValue::Duration(Duration::seconds(1)).is_truthy());
1393            assert!(FilterValue::Duration(Duration::seconds(-1)).is_truthy());
1394            assert!(!FilterValue::Duration(Duration::zero()).is_truthy());
1395        }
1396
1397        #[test]
1398        fn test_datetime_comparison() {
1399            let earlier = FilterValue::DateTime(datetime());
1400            let later = FilterValue::DateTime(datetime() + Duration::minutes(5));
1401
1402            assert!(earlier < later);
1403            assert!(later > earlier);
1404            assert!(earlier <= later);
1405            assert!(later >= earlier);
1406            assert_eq!(earlier, FilterValue::DateTime(datetime()));
1407            assert_ne!(earlier, later);
1408            assert_eq!(earlier.partial_cmp(&later), Some(Ordering::Less));
1409        }
1410
1411        #[test]
1412        fn test_duration_comparison() {
1413            let shorter = FilterValue::Duration(Duration::minutes(5));
1414            let longer = FilterValue::Duration(Duration::hours(1));
1415
1416            assert!(shorter < longer);
1417            assert!(longer > shorter);
1418            assert!(shorter <= longer);
1419            assert!(longer >= shorter);
1420            assert_eq!(shorter, FilterValue::Duration(Duration::seconds(300)));
1421            assert_ne!(shorter, longer);
1422            assert_eq!(shorter.partial_cmp(&longer), Some(Ordering::Less));
1423        }
1424
1425        #[rstest]
1426        #[case(
1427            FilterValue::DateTime(datetime()),
1428            FilterValue::Duration(Duration::minutes(5))
1429        )]
1430        #[case(FilterValue::DateTime(datetime()), FilterValue::Number(1.0))]
1431        #[case(
1432            FilterValue::Duration(Duration::minutes(5)),
1433            FilterValue::Number(300.0)
1434        )]
1435        #[case(FilterValue::Duration(Duration::minutes(5)), FilterValue::String("5m".into()))]
1436        #[case(FilterValue::DateTime(datetime()), FilterValue::Null)]
1437        fn test_mismatched_types_are_not_equal_or_ordered(
1438            #[case] left: FilterValue<'_>,
1439            #[case] right: FilterValue<'_>,
1440        ) {
1441            assert_ne!(left, right);
1442            assert_eq!(left.partial_cmp(&right), None);
1443            assert!(!left.lt(&right));
1444            assert!(!left.le(&right));
1445            assert!(!left.gt(&right));
1446            assert!(!left.ge(&right));
1447        }
1448
1449        #[rstest]
1450        #[case(FilterValue::Duration(Duration::zero()), "0s")]
1451        #[case(FilterValue::Duration(Duration::milliseconds(500)), "500ms")]
1452        #[case(FilterValue::Duration(Duration::seconds(90)), "1m30s")]
1453        #[case(FilterValue::Duration(Duration::minutes(90)), "1h30m")]
1454        #[case(FilterValue::Duration(Duration::hours(26)), "1d2h")]
1455        #[case(FilterValue::Duration(Duration::days(15)), "2w1d")]
1456        #[case(
1457            FilterValue::Duration(Duration::milliseconds(90_061_001)),
1458            "1d1h1m1s1ms"
1459        )]
1460        #[case(FilterValue::Duration(Duration::minutes(-90)), "-1h30m")]
1461        fn test_duration_display(#[case] value: FilterValue<'_>, #[case] expected: &str) {
1462            assert_eq!(value.to_string(), expected);
1463        }
1464
1465        #[test]
1466        fn test_datetime_display_is_rfc3339() {
1467            assert_eq!(
1468                FilterValue::DateTime(datetime()).to_string(),
1469                "2026-06-12T13:30:45Z"
1470            );
1471        }
1472
1473        #[test]
1474        fn test_conversions() {
1475            assert_eq!(
1476                FilterValue::from(datetime()),
1477                FilterValue::DateTime(datetime())
1478            );
1479            assert_eq!(
1480                FilterValue::from(Duration::minutes(5)),
1481                FilterValue::Duration(Duration::minutes(5))
1482            );
1483            assert_eq!(
1484                FilterValue::from(std::time::SystemTime::UNIX_EPOCH),
1485                FilterValue::DateTime(Utc.timestamp_opt(0, 0).unwrap())
1486            );
1487        }
1488
1489        #[test]
1490        fn test_contains_and_friends_are_false_for_temporal_values() {
1491            let dt = FilterValue::DateTime(datetime());
1492            let d = FilterValue::Duration(Duration::minutes(5));
1493
1494            assert!(!dt.contains(&d));
1495            assert!(!dt.startswith(&d));
1496            assert!(!dt.endswith(&d));
1497            assert!(!d.contains(&dt));
1498            assert!(!d.startswith(&dt));
1499            assert!(!d.endswith(&dt));
1500
1501            // ...though tuples may still contain temporal values.
1502            let tuple = FilterValue::Tuple(vec![FilterValue::Duration(Duration::minutes(5))]);
1503            assert!(tuple.contains(&d));
1504        }
1505    }
1506}