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    /// Converts this value into an owned [`FilterValue<'static>`], cloning any
165    /// borrowed string data so the result no longer borrows from its source.
166    ///
167    /// A [`FilterValue<'a>`](FilterValue) may borrow its string data straight
168    /// out of the object being filtered (see [`Filterable::get`]). That keeps
169    /// evaluation allocation-free, but it also ties the value's lifetime to the
170    /// borrowed object. When you need to store or return a value beyond that
171    /// borrow — for instance to hand it back from a function or stash it in a
172    /// longer-lived collection — `into_owned` promotes it to `'static` by taking
173    /// ownership of the underlying data.
174    ///
175    /// This is the [`FilterValue`] analogue of [`Cow::into_owned`]: variants
176    /// that never borrow ([`Null`](FilterValue::Null),
177    /// [`Bool`](FilterValue::Bool), [`Number`](FilterValue::Number), and the
178    /// feature-gated variants) are returned unchanged, owned
179    /// [`String`](FilterValue::String) data is reused without reallocating, and
180    /// [`Tuple`](FilterValue::Tuple) elements are converted recursively.
181    ///
182    /// ```
183    /// use filt_rs::FilterValue;
184    ///
185    /// fn borrow() -> FilterValue<'static> {
186    ///     let owned = String::from("hello");
187    ///     // `value` borrows from `owned`, so it cannot outlive this scope...
188    ///     let value: FilterValue<'_> = owned.as_str().into();
189    ///     // ...until we take ownership of its data.
190    ///     value.into_owned()
191    /// }
192    ///
193    /// assert_eq!(borrow(), FilterValue::String("hello".into()));
194    /// ```
195    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    /// Determines whether this value is considered "truthy" by the filter language.
214    ///
215    /// Filters match an object when their expression evaluates to a truthy
216    /// value. [`FilterValue::Null`], `false`, `0`, empty strings, and empty
217    /// tuples are falsy; everything else is truthy.
218    ///
219    /// With the `chrono` feature enabled, datetimes are always truthy, while
220    /// durations are truthy if (and only if) they are non-zero.
221    ///
222    /// ```
223    /// use filt_rs::FilterValue;
224    ///
225    /// assert!(FilterValue::Bool(true).is_truthy());
226    /// assert!(FilterValue::String("hello".into()).is_truthy());
227    /// assert!(!FilterValue::Null.is_truthy());
228    /// assert!(!FilterValue::Number(0.0).is_truthy());
229    /// ```
230    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    /// Determines whether this value contains the provided value.
247    ///
248    /// For tuples, this checks whether any element is equal to `other`; for
249    /// strings, it performs a case-insensitive substring search. All other
250    /// combinations return `false`. This powers the `contains` and `in`
251    /// operators in the filter language.
252    ///
253    /// The string comparison case-folds both operands character-by-character
254    /// without allocating, using the same Unicode case-folding rules as the
255    /// rest of the filter language: all Greek sigma forms (`Σ`, `σ`, and the
256    /// final-position `ς`) are treated as equivalent regardless of where they
257    /// appear in a word, and multi-character folds such as `ß` → `ss`
258    /// participate fully.
259    ///
260    /// ```
261    /// use filt_rs::FilterValue;
262    ///
263    /// let haystack: FilterValue<'_> ="Hello World".into();
264    /// assert!(haystack.contains(&"world".into()));
265    ///
266    /// let tuple = FilterValue::Tuple(vec!["a".into(), "b".into()]);
267    /// assert!(tuple.contains(&"a".into()));
268    /// assert!(!tuple.contains(&"c".into()));
269    /// ```
270    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    /// Determines whether this value starts with the provided value.
291    ///
292    /// For strings, this performs a case-insensitive prefix test; for tuples,
293    /// it checks whether any element is equal to `other`. This powers the
294    /// `startswith` operator in the filter language.
295    ///
296    /// The string comparison case-folds both operands character-by-character
297    /// without allocating, using the same Unicode case-folding rules as the
298    /// rest of the filter language (see [`FilterValue::contains`]).
299    ///
300    /// ```
301    /// use filt_rs::FilterValue;
302    ///
303    /// let value: FilterValue<'_> ="Hello World".into();
304    /// assert!(value.startswith(&"hello".into()));
305    /// assert!(!value.startswith(&"world".into()));
306    /// ```
307    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    /// Determines whether this value ends with the provided value.
328    ///
329    /// For strings, this performs a case-insensitive suffix test; for tuples,
330    /// it checks whether any element is equal to `other`. This powers the
331    /// `endswith` operator in the filter language.
332    ///
333    /// The string comparison case-folds both operands character-by-character
334    /// without allocating, using the same Unicode case-folding rules as the
335    /// rest of the filter language (see [`FilterValue::contains`]).
336    ///
337    /// ```
338    /// use filt_rs::FilterValue;
339    ///
340    /// let value: FilterValue<'_> ="Hello World".into();
341    /// assert!(value.endswith(&"WORLD".into()));
342    /// assert!(!value.endswith(&"hello".into()));
343    /// ```
344    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    /// Determines whether this value is equal to the provided value, comparing
365    /// strings case-*sensitively*.
366    ///
367    /// This is the case-sensitive counterpart of the `==` operator (and the
368    /// [`PartialEq`] implementation): tuples compare their elements with
369    /// `eq_cs` recursively, and all other variants behave exactly as `==`
370    /// does. It underpins tuple membership for the `contains_cs` and `in_cs`
371    /// operators in the filter language.
372    ///
373    /// ```
374    /// use filt_rs::FilterValue;
375    ///
376    /// let value: FilterValue<'_> ="Hello".into();
377    /// assert!(value.eq_cs(&"Hello".into()));
378    /// assert!(!value.eq_cs(&"hello".into()));
379    /// ```
380    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    /// Determines whether this value contains the provided value, comparing
399    /// strings case-*sensitively*.
400    ///
401    /// This is the case-sensitive counterpart of [`FilterValue::contains`]:
402    /// tuples check whether any element is [`eq_cs`](FilterValue::eq_cs) to
403    /// `other`, strings perform an exact substring search, and all other
404    /// combinations return `false`. This powers the `contains_cs` and `in_cs`
405    /// operators in the filter language.
406    ///
407    /// ```
408    /// use filt_rs::FilterValue;
409    ///
410    /// let haystack: FilterValue<'_> ="Hello World".into();
411    /// assert!(haystack.contains_cs(&"World".into()));
412    /// assert!(!haystack.contains_cs(&"world".into()));
413    /// ```
414    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    /// Determines whether this value starts with the provided value, comparing
433    /// strings case-*sensitively*.
434    ///
435    /// This is the case-sensitive counterpart of [`FilterValue::startswith`],
436    /// powering the `startswith_cs` operator in the filter language.
437    ///
438    /// ```
439    /// use filt_rs::FilterValue;
440    ///
441    /// let value: FilterValue<'_> ="Hello World".into();
442    /// assert!(value.startswith_cs(&"Hello".into()));
443    /// assert!(!value.startswith_cs(&"hello".into()));
444    /// ```
445    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    /// Determines whether this value ends with the provided value, comparing
464    /// strings case-*sensitively*.
465    ///
466    /// This is the case-sensitive counterpart of [`FilterValue::endswith`],
467    /// powering the `endswith_cs` operator in the filter language.
468    ///
469    /// ```
470    /// use filt_rs::FilterValue;
471    ///
472    /// let value: FilterValue<'_> ="Hello World".into();
473    /// assert!(value.endswith_cs(&"World".into()));
474    /// assert!(!value.endswith_cs(&"WORLD".into()));
475    /// ```
476    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, // Return None for non-comparable types
557        }
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    /// Formats the value as it would appear within a filter expression.
663    ///
664    /// ```
665    /// use filt_rs::FilterValue;
666    ///
667    /// let value = FilterValue::Tuple(vec!["a".into(), 1.into(), FilterValue::Null]);
668    /// assert_eq!(value.to_string(), r#"["a", 1, null]"#);
669    /// ```
670    ///
671    /// Secret values (available with the `secrecy` feature) are always
672    /// formatted as `[REDACTED]`, never as their underlying string.
673    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/// Formats a duration in the same humanized compact form used by duration
708/// literals in the filter language (e.g. `1h30m`), with millisecond precision.
709#[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    /// Borrows the string slice rather than copying it, so a [`Filterable::get`]
777    /// implementation returning `self.field.as_str().into()` performs no
778    /// allocation. Use [`From<String>`] (or `.to_string().into()`) when you
779    /// need the value to own its data instead.
780    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    /// Wraps a [`secrecy::SecretString`] as a [`FilterValue::Secret`].
794    ///
795    /// ```
796    /// use filt_rs::FilterValue;
797    /// use secrecy::SecretString;
798    ///
799    /// let value: FilterValue<'_> =SecretString::from("hunter2").into();
800    /// assert_eq!(value, FilterValue::String("hunter2".into()));
801    /// assert_eq!(value.to_string(), "[REDACTED]");
802    /// ```
803    fn from(s: secrecy::SecretString) -> Self {
804        FilterValue::Secret(s)
805    }
806}
807
808/// Converts a UTC datetime into a [`FilterValue::DateTime`].
809///
810/// *Only available when the `chrono` crate feature is enabled.*
811///
812/// ```
813/// use filt_rs::FilterValue;
814///
815/// let timestamp = chrono::Utc::now();
816/// let value: FilterValue<'_> =timestamp.into();
817/// assert_eq!(value, FilterValue::DateTime(timestamp));
818/// ```
819#[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/// Converts a [`chrono::Duration`] into a [`FilterValue::Duration`].
827///
828/// *Only available when the `chrono` crate feature is enabled.*
829///
830/// ```
831/// use filt_rs::FilterValue;
832///
833/// let value: FilterValue<'_> =chrono::Duration::minutes(90).into();
834/// assert_eq!(value.to_string(), "1h30m");
835/// ```
836#[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/// Converts a [`std::time::SystemTime`] into a [`FilterValue::DateTime`],
844/// making it easy to expose timestamps from the standard library (such as
845/// file modification times) without converting them yourself.
846///
847/// *Only available when the `chrono` crate feature is enabled.*
848///
849/// ```
850/// use filt_rs::FilterValue;
851/// use std::time::SystemTime;
852///
853/// let value: FilterValue<'_> =SystemTime::UNIX_EPOCH.into();
854/// assert_eq!(value.to_string(), "1970-01-01T00:00:00Z");
855/// ```
856#[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    /// `into_owned` must promote a value to `'static` while preserving equality,
899    /// regardless of which variant (or nested variant) carried borrowed data.
900    #[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    /// The borrowed source can be dropped after `into_owned`, proving the result
915    /// owns its data rather than borrowing it.
916    #[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        // Equality folds case using the language's Unicode case-folding
964        // rules, including non-ASCII characters and multi-character folds.
965        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        // The `<` and `>` operators require every paired element to compare
982        // accordingly, while ordering between different-length tuples is
983        // driven by their lengths.
984        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)] // no case folding at all
1115    #[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    /// The case-insensitive string operations treat all Greek sigma forms
1174    /// (`Σ`, `σ`, and the word-final `ς`) as equivalent, regardless of where
1175    /// they appear within a word.
1176    ///
1177    /// This intentionally diverges from [`str::to_lowercase`]'s context
1178    /// sensitive final-sigma rule (which would, for example, consider
1179    /// `"ΛΟΓΟΣ"` *not* to end with `"Σ"` because the haystack lowercases to
1180    /// `"λογος"` while the needle lowercases to `"σ"`). Folding every sigma
1181    /// to `σ` mirrors Unicode simple case folding and gives
1182    /// position-independent results.
1183    #[rstest]
1184    #[case("ΛΟΓΟΣ", "Σ")] // upper-case needle vs word-final position
1185    #[case("ΛΟΓΟΣ", "ς")] // final-sigma needle
1186    #[case("ΛΟΓΟΣ", "σ")] // regular-sigma needle
1187    #[case("λογος", "Σ")] // word-final sigma in the haystack
1188    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    /// Characters whose case-folded form expands to multiple characters
1197    /// (such as `İ`, which folds to `i` followed by a combining dot above,
1198    /// or `ß`, which folds to `ss`) participate fully in the comparison.
1199    #[rstest]
1200    #[case("İstanbul", "i\u{307}stanbul", true)] // expanded folded form
1201    #[case("İstanbul", "\u{307}stanbul", true)] // matches mid-expansion, as str::contains would
1202    #[case("İstanbul", "istanbul", false)] // the combining mark is significant
1203    #[case("straße", "STRASSE", true)] // ß folds to ss
1204    #[case("groß", "ss", true)]
1205    #[case("gross", "ß", true)] // ...in the needle too
1206    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        /// For every comparison operation, a secret must behave exactly as the
1356        /// equivalent string would — whichever side of the operator it is on.
1357        #[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            // ...though tuples may still contain temporal values.
1580            let tuple = FilterValue::Tuple(vec![FilterValue::Duration(Duration::minutes(5))]);
1581            assert!(tuple.contains(&d));
1582        }
1583    }
1584}