relay_protocol/
condition.rs

1//! Types to specify conditions on data.
2//!
3//! The root type is [`RuleCondition`].
4
5use relay_pattern::{CaseInsensitive, TypedPatterns};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::{Getter, Val};
10
11/// Options for [`EqCondition`].
12#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
13#[serde(rename_all = "camelCase")]
14pub struct EqCondOptions {
15    /// If `true`, string values are compared in case-insensitive mode.
16    ///
17    /// This has no effect on numeric or boolean comparisons.
18    #[serde(default)]
19    pub ignore_case: bool,
20}
21
22/// A condition that compares values for equality.
23///
24/// This operator supports:
25///  - boolean
26///  - strings, optionally ignoring ASCII-case
27///  - UUIDs
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct EqCondition {
31    /// Path of the field that should match the value.
32    pub name: String,
33
34    /// The value to check against.
35    ///
36    /// When comparing with a string field, this value can be an array. The condition matches if any
37    /// of the provided values matches the field.
38    pub value: Value,
39
40    /// Configuration options for the condition.
41    #[serde(default, skip_serializing_if = "is_default")]
42    pub options: EqCondOptions,
43}
44
45impl EqCondition {
46    /// Creates a new condition that checks for equality.
47    ///
48    /// By default, this condition will perform a case-sensitive check. To ignore ASCII case, use
49    /// [`EqCondition::ignore_case`].
50    ///
51    /// The main way to create this conditions is [`RuleCondition::eq`].
52    pub fn new(field: impl Into<String>, value: impl Into<Value>) -> Self {
53        Self {
54            name: field.into(),
55            value: value.into(),
56            options: EqCondOptions { ignore_case: false },
57        }
58    }
59
60    /// Enables case-insensitive comparisions for this rule.
61    ///
62    /// To create such a condition directly, use [`RuleCondition::eq_ignore_case`].
63    pub fn ignore_case(mut self) -> Self {
64        self.options.ignore_case = true;
65        self
66    }
67
68    fn cmp(&self, left: &str, right: &str) -> bool {
69        if self.options.ignore_case {
70            unicase::eq(left, right)
71        } else {
72            left == right
73        }
74    }
75
76    fn matches<T>(&self, instance: &T) -> bool
77    where
78        T: Getter + ?Sized,
79    {
80        match (instance.get_value(self.name.as_str()), &self.value) {
81            (None, Value::Null) => true,
82            (Some(Val::String(f)), Value::String(ref val)) => self.cmp(f, val),
83            (Some(Val::String(f)), Value::Array(ref arr)) => arr
84                .iter()
85                .filter_map(|v| v.as_str())
86                .any(|v| self.cmp(v, f)),
87            (Some(Val::Uuid(f)), Value::String(ref val)) => Some(f) == val.parse().ok(),
88            (Some(Val::Bool(f)), Value::Bool(v)) => f == *v,
89            _ => false,
90        }
91    }
92}
93
94/// Returns `true` if this value is equal to `Default::default()`.
95fn is_default<T: Default + PartialEq>(t: &T) -> bool {
96    *t == T::default()
97}
98
99macro_rules! impl_cmp_condition {
100    ($struct_name:ident, $operator:tt, $doc:literal) => {
101        #[doc = $doc]
102        ///
103        /// Strings are explicitly not supported by this.
104        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
105        pub struct $struct_name {
106            /// Path of the field that should match the value.
107            pub name: String,
108            /// The numeric value to check against.
109            pub value: Value,
110        }
111
112        impl $struct_name {
113            /// Creates a new condition that comparison condition.
114            pub fn new(field: impl Into<String>, value: impl Into<Value>) -> Self {
115                Self {
116                    name: field.into(),
117                    value: value.into(),
118                }
119            }
120
121            fn matches<T>(&self, instance: &T) -> bool
122            where
123                T: Getter + ?Sized,
124            {
125                let Some(value) = instance.get_value(self.name.as_str()) else {
126                    return false;
127                };
128
129                // Try various conversion functions in order of expensiveness and likelihood
130                // - as_i64 is not really fast, but most values in sampling rules can be i64, so we
131                //   could return early
132                // - f64 is more likely to succeed than u64, but we might lose precision
133                if let (Some(a), Some(b)) = (value.as_i64(), self.value.as_i64()) {
134                    a $operator b
135                } else if let (Some(a), Some(b)) = (value.as_u64(), self.value.as_u64()) {
136                    a $operator b
137                } else if let (Some(a), Some(b)) = (value.as_f64(), self.value.as_f64()) {
138                    a $operator b
139                } else if let (Some(a), Some(b)) = (value.as_str(), self.value.as_str()) {
140                    a $operator b
141                } else {
142                    false
143                }
144            }
145        }
146    }
147}
148
149impl_cmp_condition!(GteCondition, >=, "A condition that applies `>=`.");
150impl_cmp_condition!(LteCondition, <=, "A condition that applies `<=`.");
151impl_cmp_condition!(GtCondition, >, "A condition that applies `>`.");
152impl_cmp_condition!(LtCondition, <, "A condition that applies `<`.");
153
154/// A condition that uses glob matching.
155///
156/// This is similar to [`EqCondition`], but it allows for wildcards in `value`. This is slightly
157/// more expensive to construct and check, so preferrably use [`EqCondition`] when no wildcard
158/// matching is needed.
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct GlobCondition {
161    /// Path of the field that should match the value.
162    pub name: String,
163    /// A list of glob patterns to check.
164    ///
165    /// Note that this cannot be a single value, it must be a list of values.
166    pub value: TypedPatterns<CaseInsensitive>,
167}
168
169impl GlobCondition {
170    /// Creates a condition that matches one or more glob patterns.
171    pub fn new(field: impl Into<String>, value: impl IntoStrings) -> Self {
172        Self {
173            name: field.into(),
174            value: TypedPatterns::from(value.into_strings()),
175        }
176    }
177
178    fn matches<T>(&self, instance: &T) -> bool
179    where
180        T: Getter + ?Sized,
181    {
182        match instance.get_value(self.name.as_str()) {
183            Some(Val::String(s)) => self.value.is_match(s),
184            _ => false,
185        }
186    }
187}
188
189/// A type that can be converted to a list of strings.
190pub trait IntoStrings {
191    /// Creates a list of strings from this type.
192    fn into_strings(self) -> Vec<String>;
193}
194
195impl IntoStrings for &'_ str {
196    fn into_strings(self) -> Vec<String> {
197        vec![self.to_owned()]
198    }
199}
200
201impl IntoStrings for String {
202    fn into_strings(self) -> Vec<String> {
203        vec![self]
204    }
205}
206
207impl IntoStrings for std::borrow::Cow<'_, str> {
208    fn into_strings(self) -> Vec<String> {
209        vec![self.into_owned()]
210    }
211}
212
213impl IntoStrings for &'_ [&'_ str] {
214    fn into_strings(self) -> Vec<String> {
215        self.iter().copied().map(str::to_owned).collect()
216    }
217}
218
219impl IntoStrings for &'_ [String] {
220    fn into_strings(self) -> Vec<String> {
221        self.to_vec()
222    }
223}
224
225impl IntoStrings for Vec<&'_ str> {
226    fn into_strings(self) -> Vec<String> {
227        self.into_iter().map(str::to_owned).collect()
228    }
229}
230
231impl IntoStrings for Vec<String> {
232    fn into_strings(self) -> Vec<String> {
233        self
234    }
235}
236
237/// Combines multiple conditions using logical OR.
238///
239/// This condition matches if **any** of the inner conditions match. The default value for this
240/// condition is `false`, that is, this rule does not match if there are no inner conditions.
241///
242/// See [`RuleCondition::or`].
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244pub struct OrCondition {
245    /// Inner rules to combine.
246    pub inner: Vec<RuleCondition>,
247}
248
249impl OrCondition {
250    fn supported(&self) -> bool {
251        self.inner.iter().all(RuleCondition::supported)
252    }
253
254    fn matches<T>(&self, value: &T) -> bool
255    where
256        T: Getter + ?Sized,
257    {
258        self.inner.iter().any(|cond| cond.matches(value))
259    }
260}
261
262/// Combines multiple conditions using logical AND.
263///
264/// This condition matches if **all** of the inner conditions match. The default value for this
265/// condition is `true`, that is, this rule matches if there are no inner conditions.
266///
267/// See [`RuleCondition::and`].
268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
269pub struct AndCondition {
270    /// Inner rules to combine.
271    pub inner: Vec<RuleCondition>,
272}
273
274impl AndCondition {
275    fn supported(&self) -> bool {
276        self.inner.iter().all(RuleCondition::supported)
277    }
278    fn matches<T>(&self, value: &T) -> bool
279    where
280        T: Getter + ?Sized,
281    {
282        self.inner.iter().all(|cond| cond.matches(value))
283    }
284}
285
286/// Applies logical NOT to a condition.
287///
288/// This condition matches if the inner condition does not match.
289///
290/// See [`RuleCondition::negate`].
291#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
292pub struct NotCondition {
293    /// An inner rule to negate.
294    pub inner: Box<RuleCondition>,
295}
296
297impl NotCondition {
298    fn supported(&self) -> bool {
299        self.inner.supported()
300    }
301
302    fn matches<T>(&self, value: &T) -> bool
303    where
304        T: Getter + ?Sized,
305    {
306        !self.inner.matches(value)
307    }
308}
309
310/// Applies the ANY operation to an array field.
311///
312/// This condition matches if at least one of the elements of the array match with the
313/// `inner` condition.
314#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
315pub struct AnyCondition {
316    /// Path of the field that should match the value.
317    pub name: String,
318    /// Inner rule to match on each element.
319    pub inner: Box<RuleCondition>,
320}
321
322impl AnyCondition {
323    /// Creates a condition that matches any element in the array against the `inner` condition.
324    pub fn new(field: impl Into<String>, inner: RuleCondition) -> Self {
325        Self {
326            name: field.into(),
327            inner: Box::new(inner),
328        }
329    }
330
331    fn supported(&self) -> bool {
332        self.inner.supported()
333    }
334    fn matches<T>(&self, instance: &T) -> bool
335    where
336        T: Getter + ?Sized,
337    {
338        let Some(mut getter_iter) = instance.get_iter(self.name.as_str()) else {
339            return false;
340        };
341
342        getter_iter.any(|g| self.inner.matches(g))
343    }
344}
345
346/// Applies the ALL operation to an array field.
347///
348/// This condition matches if all the elements of the array match with the `inner` condition.
349#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
350pub struct AllCondition {
351    /// Path of the field that should match the value.
352    pub name: String,
353    /// Inner rule to match on each element.
354    pub inner: Box<RuleCondition>,
355}
356
357impl AllCondition {
358    /// Creates a condition that matches all the elements in the array against the `inner`
359    /// condition.
360    pub fn new(field: impl Into<String>, inner: RuleCondition) -> Self {
361        Self {
362            name: field.into(),
363            inner: Box::new(inner),
364        }
365    }
366
367    fn supported(&self) -> bool {
368        self.inner.supported()
369    }
370    fn matches<T>(&self, instance: &T) -> bool
371    where
372        T: Getter + ?Sized,
373    {
374        let Some(mut getter_iter) = instance.get_iter(self.name.as_str()) else {
375            return false;
376        };
377
378        getter_iter.all(|g| self.inner.matches(g))
379    }
380}
381
382/// A condition that can be evaluated on structured data.
383///
384/// The basic conditions are [`eq`](Self::eq), [`glob`](Self::glob), and the comparison operators.
385/// These conditions compare a data field specified through a path with a value or a set of values.
386/// If the field's value [matches](Self::matches) the values declared in the rule, the condition
387/// returns `true`.
388///
389/// Conditions can be combined with the logical operators [`and`](Self::and), [`or`](Self::or), and
390/// [`not` (negate)](Self::negate).
391///
392/// # Data Access
393///
394/// Rule conditions access data fields through the [`Getter`] trait. Note that getters always have a
395/// root namespace which must be part of the field's path. If path's root component does not match
396/// the one of the passed getter instance, the rule condition will not be able to retrieve data and
397/// likely not match.
398///
399/// # Serialization
400///
401/// Conditions are represented as nested JSON objects. The condition type is declared in the `op`
402/// field.
403///
404/// # Example
405///
406/// ```
407/// use relay_protocol::RuleCondition;
408///
409/// let condition = !RuleCondition::eq("obj.status", "invalid");
410/// ```
411#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
412#[serde(rename_all = "camelCase", tag = "op")]
413pub enum RuleCondition {
414    /// A condition that compares values for equality.
415    ///
416    /// This operator supports:
417    ///  - boolean
418    ///  - strings, optionally ignoring ASCII-case
419    ///  - UUIDs
420    ///
421    /// # Example
422    ///
423    /// ```
424    /// use relay_protocol::RuleCondition;
425    ///
426    /// let condition = RuleCondition::eq("obj.status", "invalid");
427    /// ```
428    Eq(EqCondition),
429
430    /// A condition that applies `>=`.
431    ///
432    /// # Example
433    ///
434    /// ```
435    /// use relay_protocol::RuleCondition;
436    ///
437    /// let condition = RuleCondition::gte("obj.length", 10);
438    /// ```
439    Gte(GteCondition),
440
441    /// A condition that applies `<=`.
442    ///
443    /// # Example
444    ///
445    /// ```
446    /// use relay_protocol::RuleCondition;
447    ///
448    /// let condition = RuleCondition::lte("obj.length", 10);
449    /// ```
450    Lte(LteCondition),
451
452    /// A condition that applies `>`.
453    ///
454    /// # Example
455    ///
456    /// ```
457    /// use relay_protocol::RuleCondition;
458    ///
459    /// let condition = RuleCondition::gt("obj.length", 10);
460    /// ```
461    Gt(GtCondition),
462
463    /// A condition that applies `<`.
464    ///
465    /// # Example
466    ///
467    /// ```
468    /// use relay_protocol::RuleCondition;
469    ///
470    /// let condition = RuleCondition::lt("obj.length", 10);
471    /// ```
472    Lt(LtCondition),
473
474    /// A condition that uses glob matching.
475    ///
476    /// # Example
477    ///
478    /// ```
479    /// use relay_protocol::RuleCondition;
480    ///
481    /// let condition = RuleCondition::glob("obj.name", "error: *");
482    /// ```
483    Glob(GlobCondition),
484
485    /// Combines multiple conditions using logical OR.
486    ///
487    /// # Example
488    ///
489    /// ```
490    /// use relay_protocol::RuleCondition;
491    ///
492    /// let condition = RuleCondition::eq("obj.status", "invalid")
493    ///     | RuleCondition::eq("obj.status", "unknown");
494    /// ```
495    Or(OrCondition),
496
497    /// Combines multiple conditions using logical AND.
498    ///
499    /// # Example
500    ///
501    /// ```
502    /// use relay_protocol::RuleCondition;
503    ///
504    /// let condition = RuleCondition::eq("obj.status", "invalid")
505    ///     & RuleCondition::gte("obj.length", 10);
506    /// ```
507    And(AndCondition),
508
509    /// Applies logical NOT to a condition.
510    ///
511    /// # Example
512    ///
513    /// ```
514    /// use relay_protocol::RuleCondition;
515    ///
516    /// let condition = !RuleCondition::eq("obj.status", "invalid");
517    /// ```
518    Not(NotCondition),
519
520    /// Loops over an array field and returns true if at least one element matches
521    /// the inner condition.
522    ///
523    /// # Example
524    ///
525    /// ```
526    /// use relay_protocol::RuleCondition;
527    ///
528    /// let condition = RuleCondition::for_any("obj.exceptions",
529    ///     RuleCondition::eq("name", "NullPointerException")
530    /// );
531    /// ```
532    Any(AnyCondition),
533
534    /// Loops over an array field and returns true if all elements match the inner condition.
535    ///
536    /// # Example
537    ///
538    /// ```
539    /// use relay_protocol::RuleCondition;
540    ///
541    /// let condition = RuleCondition::for_all("obj.exceptions",
542    ///     RuleCondition::eq("name", "NullPointerException")
543    /// );
544    /// ```
545    All(AllCondition),
546
547    /// An unsupported condition for future compatibility.
548    #[serde(other)]
549    Unsupported,
550}
551
552impl RuleCondition {
553    /// Returns a condition that always matches.
554    pub fn all() -> Self {
555        Self::And(AndCondition { inner: Vec::new() })
556    }
557
558    /// Returns a condition that never matches.
559    pub fn never() -> Self {
560        Self::Or(OrCondition { inner: Vec::new() })
561    }
562
563    /// Creates a condition that compares values for equality.
564    ///
565    /// This operator supports:
566    ///  - boolean
567    ///  - strings
568    ///  - UUIDs
569    ///
570    /// # Examples
571    ///
572    /// ```
573    /// use relay_protocol::RuleCondition;
574    ///
575    /// // Matches if the value is identical to the given string:
576    /// let condition = RuleCondition::eq("obj.status", "invalid");
577    ///
578    /// // Matches if the value is identical to any of the given strings:
579    /// let condition = RuleCondition::eq("obj.status", &["invalid", "unknown"][..]);
580    ///
581    /// // Matches a boolean flag:
582    /// let condition = RuleCondition::eq("obj.valid", false);
583    /// ```
584    pub fn eq(field: impl Into<String>, value: impl Into<Value>) -> Self {
585        Self::Eq(EqCondition::new(field, value))
586    }
587
588    /// Creates a condition that compares values for equality ignoring ASCII-case.
589    ///
590    /// # Examples
591    ///
592    /// ```
593    /// use relay_protocol::RuleCondition;
594    ///
595    /// // Matches if the value is identical to the given string:
596    /// let condition = RuleCondition::eq_ignore_case("obj.status", "invalid");
597    ///
598    /// // Matches if the value is identical to any of the given strings:
599    /// let condition = RuleCondition::eq_ignore_case("obj.status", &["invalid", "unknown"][..]);
600    /// ```
601    pub fn eq_ignore_case(field: impl Into<String>, value: impl Into<Value>) -> Self {
602        Self::Eq(EqCondition::new(field, value).ignore_case())
603    }
604
605    /// Creates a condition that matches one or more glob patterns.
606    ///
607    /// # Example
608    ///
609    /// ```
610    /// use relay_protocol::RuleCondition;
611    ///
612    /// // Match a single pattern:
613    /// let condition = RuleCondition::glob("obj.name", "error: *");
614    ///
615    /// // Match any of a list of patterns:
616    /// let condition = RuleCondition::glob("obj.name", &["error: *", "*failure*"][..]);
617    /// ```
618    pub fn glob(field: impl Into<String>, value: impl IntoStrings) -> Self {
619        Self::Glob(GlobCondition::new(field, value))
620    }
621
622    /// Creates a condition that applies `>`.
623    ///
624    /// # Example
625    ///
626    /// ```
627    /// use relay_protocol::RuleCondition;
628    ///
629    /// let condition = RuleCondition::gt("obj.length", 10);
630    /// ```
631    pub fn gt(field: impl Into<String>, value: impl Into<Value>) -> Self {
632        Self::Gt(GtCondition::new(field, value))
633    }
634
635    /// Creates a condition that applies `>=`.
636    ///
637    /// # Example
638    ///
639    /// ```
640    /// use relay_protocol::RuleCondition;
641    ///
642    /// let condition = RuleCondition::gte("obj.length", 10);
643    /// ```
644    pub fn gte(field: impl Into<String>, value: impl Into<Value>) -> Self {
645        Self::Gte(GteCondition::new(field, value))
646    }
647
648    /// Creates a condition that applies `<`.
649    ///
650    /// # Example
651    ///
652    /// ```
653    /// use relay_protocol::RuleCondition;
654    ///
655    /// let condition = RuleCondition::lt("obj.length", 10);
656    /// ```
657    pub fn lt(field: impl Into<String>, value: impl Into<Value>) -> Self {
658        Self::Lt(LtCondition::new(field, value))
659    }
660
661    /// Creates a condition that applies `<=`.
662    ///
663    /// # Example
664    ///
665    /// ```
666    /// use relay_protocol::RuleCondition;
667    ///
668    /// let condition = RuleCondition::lte("obj.length", 10);
669    /// ```
670    pub fn lte(field: impl Into<String>, value: impl Into<Value>) -> Self {
671        Self::Lte(LteCondition::new(field, value))
672    }
673
674    /// Combines this condition and another condition with a logical AND operator.
675    ///
676    /// The short-hand operator for this combinator is `&`.
677    ///
678    /// # Example
679    ///
680    /// ```
681    /// use relay_protocol::RuleCondition;
682    ///
683    /// let condition = RuleCondition::eq("obj.status", "invalid")
684    ///     & RuleCondition::gte("obj.length", 10);
685    /// ```
686    pub fn and(mut self, other: RuleCondition) -> Self {
687        if let Self::And(ref mut condition) = self {
688            condition.inner.push(other);
689            self
690        } else {
691            Self::And(AndCondition {
692                inner: vec![self, other],
693            })
694        }
695    }
696
697    /// Combines this condition and another condition with a logical OR operator.
698    ///
699    /// The short-hand operator for this combinator is `|`.
700    ///
701    /// # Example
702    ///
703    /// ```
704    /// use relay_protocol::RuleCondition;
705    ///
706    /// let condition = RuleCondition::eq("obj.status", "invalid")
707    ///     | RuleCondition::eq("obj.status", "unknown");
708    /// ```
709    pub fn or(mut self, other: RuleCondition) -> Self {
710        if let Self::Or(ref mut condition) = self {
711            condition.inner.push(other);
712            self
713        } else {
714            Self::Or(OrCondition {
715                inner: vec![self, other],
716            })
717        }
718    }
719
720    /// Negates this condition with logical NOT.
721    ///
722    /// The short-hand operator for this combinator is `!`.
723    ///
724    /// # Example
725    ///
726    /// ```
727    /// use relay_protocol::RuleCondition;
728    ///
729    /// let condition = !RuleCondition::eq("obj.status", "invalid");
730    /// ```
731    pub fn negate(self) -> Self {
732        match self {
733            Self::Not(condition) => *condition.inner,
734            other => Self::Not(NotCondition {
735                inner: Box::new(other),
736            }),
737        }
738    }
739
740    /// Creates an [`AnyCondition`].
741    ///
742    /// # Example
743    ///
744    /// ```
745    /// use relay_protocol::RuleCondition;
746    ///
747    /// let condition = RuleCondition::for_any("obj.exceptions",
748    ///     RuleCondition::eq("name", "NullPointerException")
749    /// );
750    /// ```
751    pub fn for_any(field: impl Into<String>, inner: RuleCondition) -> Self {
752        Self::Any(AnyCondition::new(field, inner))
753    }
754
755    /// Creates an [`AllCondition`].
756    ///
757    /// # Example
758    ///
759    /// ```
760    /// use relay_protocol::RuleCondition;
761    ///
762    /// let condition = RuleCondition::for_all("obj.exceptions",
763    ///     RuleCondition::eq("name", "NullPointerException")
764    /// );
765    /// ```
766    pub fn for_all(field: impl Into<String>, inner: RuleCondition) -> Self {
767        Self::All(AllCondition::new(field, inner))
768    }
769
770    /// Checks if Relay supports this condition (in other words if the condition had any unknown configuration
771    /// which was serialized as "Unsupported" (because the configuration is either faulty or was created for a
772    /// newer relay that supports some other condition types)
773    pub fn supported(&self) -> bool {
774        match self {
775            RuleCondition::Unsupported => false,
776            // we have a known condition
777            RuleCondition::Gte(_)
778            | RuleCondition::Lte(_)
779            | RuleCondition::Gt(_)
780            | RuleCondition::Lt(_)
781            | RuleCondition::Eq(_)
782            | RuleCondition::Glob(_) => true,
783            // dig down for embedded conditions
784            RuleCondition::And(rules) => rules.supported(),
785            RuleCondition::Or(rules) => rules.supported(),
786            RuleCondition::Not(rule) => rule.supported(),
787            RuleCondition::Any(rule) => rule.supported(),
788            RuleCondition::All(rule) => rule.supported(),
789        }
790    }
791
792    /// Returns `true` if the rule matches the given value instance.
793    pub fn matches<T>(&self, value: &T) -> bool
794    where
795        T: Getter + ?Sized,
796    {
797        match self {
798            RuleCondition::Eq(condition) => condition.matches(value),
799            RuleCondition::Lte(condition) => condition.matches(value),
800            RuleCondition::Gte(condition) => condition.matches(value),
801            RuleCondition::Gt(condition) => condition.matches(value),
802            RuleCondition::Lt(condition) => condition.matches(value),
803            RuleCondition::Glob(condition) => condition.matches(value),
804            RuleCondition::And(conditions) => conditions.matches(value),
805            RuleCondition::Or(conditions) => conditions.matches(value),
806            RuleCondition::Not(condition) => condition.matches(value),
807            RuleCondition::Any(condition) => condition.matches(value),
808            RuleCondition::All(condition) => condition.matches(value),
809            RuleCondition::Unsupported => false,
810        }
811    }
812}
813
814impl std::ops::BitAnd for RuleCondition {
815    type Output = Self;
816
817    fn bitand(self, rhs: Self) -> Self::Output {
818        self.and(rhs)
819    }
820}
821
822impl std::ops::BitOr for RuleCondition {
823    type Output = Self;
824
825    fn bitor(self, rhs: Self) -> Self::Output {
826        self.or(rhs)
827    }
828}
829
830impl std::ops::Not for RuleCondition {
831    type Output = Self;
832
833    fn not(self) -> Self::Output {
834        self.negate()
835    }
836}
837
838#[cfg(test)]
839mod tests {
840    use super::*;
841    use crate::GetterIter;
842
843    #[derive(Debug)]
844    struct Exception {
845        name: String,
846    }
847
848    impl Getter for Exception {
849        fn get_value(&self, path: &str) -> Option<Val<'_>> {
850            Some(match path {
851                "name" => self.name.as_str().into(),
852                _ => return None,
853            })
854        }
855    }
856
857    struct Trace {
858        transaction: String,
859        release: String,
860        environment: String,
861        user_segment: String,
862        exceptions: Vec<Exception>,
863    }
864
865    impl Getter for Trace {
866        fn get_value(&self, path: &str) -> Option<Val<'_>> {
867            Some(match path.strip_prefix("trace.")? {
868                "transaction" => self.transaction.as_str().into(),
869                "release" => self.release.as_str().into(),
870                "environment" => self.environment.as_str().into(),
871                "user.segment" => self.user_segment.as_str().into(),
872                _ => {
873                    return None;
874                }
875            })
876        }
877
878        fn get_iter(&self, path: &str) -> Option<GetterIter<'_>> {
879            Some(match path.strip_prefix("trace.")? {
880                "exceptions" => GetterIter::new(self.exceptions.iter()),
881                _ => return None,
882            })
883        }
884    }
885
886    fn mock_trace() -> Trace {
887        Trace {
888            transaction: "transaction1".to_string(),
889            release: "1.1.1".to_string(),
890            environment: "debug".to_string(),
891            user_segment: "vip".to_string(),
892            exceptions: vec![
893                Exception {
894                    name: "NullPointerException".to_string(),
895                },
896                Exception {
897                    name: "NullUser".to_string(),
898                },
899            ],
900        }
901    }
902
903    #[test]
904    fn deserialize() {
905        let serialized_rules = r#"[
906            {
907                "op":"eq",
908                "name": "field_1",
909                "value": ["UPPER","lower"],
910                "options":{
911                    "ignoreCase": true
912                }
913            },
914            {
915                "op":"eq",
916                "name": "field_2",
917                "value": ["UPPER","lower"]
918            },
919            {
920                "op":"glob",
921                "name": "field_3",
922                "value": ["1.2.*","2.*"]
923            },
924            {
925                "op":"not",
926                "inner": {
927                    "op":"glob",
928                    "name": "field_4",
929                    "value": ["1.*"]
930                }
931            },
932            {
933                "op":"and",
934                "inner": [{
935                    "op":"glob",
936                    "name": "field_5",
937                    "value": ["2.*"]
938                }]
939            },
940            {
941                "op":"or",
942                "inner": [{
943                    "op":"glob",
944                    "name": "field_6",
945                    "value": ["3.*"]
946                }]
947            },
948            {
949                "op": "any",
950                "name": "obj.exceptions",
951                "inner": {
952                    "op": "glob",
953                    "name": "value",
954                    "value": ["*Exception"]
955                }
956            },
957            {
958                "op": "all",
959                "name": "obj.exceptions",
960                "inner": {
961                    "op": "glob",
962                    "name": "value",
963                    "value": ["*Exception"]
964                }
965            }
966        ]"#;
967
968        let rules: Result<Vec<RuleCondition>, _> = serde_json::from_str(serialized_rules);
969        assert!(rules.is_ok());
970        let rules = rules.unwrap();
971        insta::assert_ron_snapshot!(rules, @r###"
972        [
973          EqCondition(
974            op: "eq",
975            name: "field_1",
976            value: [
977              "UPPER",
978              "lower",
979            ],
980            options: EqCondOptions(
981              ignoreCase: true,
982            ),
983          ),
984          EqCondition(
985            op: "eq",
986            name: "field_2",
987            value: [
988              "UPPER",
989              "lower",
990            ],
991          ),
992          GlobCondition(
993            op: "glob",
994            name: "field_3",
995            value: [
996              "1.2.*",
997              "2.*",
998            ],
999          ),
1000          NotCondition(
1001            op: "not",
1002            inner: GlobCondition(
1003              op: "glob",
1004              name: "field_4",
1005              value: [
1006                "1.*",
1007              ],
1008            ),
1009          ),
1010          AndCondition(
1011            op: "and",
1012            inner: [
1013              GlobCondition(
1014                op: "glob",
1015                name: "field_5",
1016                value: [
1017                  "2.*",
1018                ],
1019              ),
1020            ],
1021          ),
1022          OrCondition(
1023            op: "or",
1024            inner: [
1025              GlobCondition(
1026                op: "glob",
1027                name: "field_6",
1028                value: [
1029                  "3.*",
1030                ],
1031              ),
1032            ],
1033          ),
1034          AnyCondition(
1035            op: "any",
1036            name: "obj.exceptions",
1037            inner: GlobCondition(
1038              op: "glob",
1039              name: "value",
1040              value: [
1041                "*Exception",
1042              ],
1043            ),
1044          ),
1045          AllCondition(
1046            op: "all",
1047            name: "obj.exceptions",
1048            inner: GlobCondition(
1049              op: "glob",
1050              name: "value",
1051              value: [
1052                "*Exception",
1053              ],
1054            ),
1055          ),
1056        ]
1057        "###);
1058    }
1059
1060    #[test]
1061    fn unsupported_rule_deserialize() {
1062        let bad_json = r#"{
1063            "op": "BadOperator",
1064            "name": "foo",
1065            "value": "bar"
1066        }"#;
1067
1068        let rule: RuleCondition = serde_json::from_str(bad_json).unwrap();
1069        assert!(matches!(rule, RuleCondition::Unsupported));
1070    }
1071
1072    #[test]
1073    /// test matching for various rules
1074    fn test_matches() {
1075        let conditions = [
1076            (
1077                "simple",
1078                RuleCondition::glob("trace.release", "1.1.1")
1079                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1080                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip")
1081                    & RuleCondition::eq_ignore_case("trace.transaction", "transaction1"),
1082            ),
1083            (
1084                "glob releases",
1085                RuleCondition::glob("trace.release", "1.*")
1086                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1087                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1088            ),
1089            (
1090                "glob transaction",
1091                RuleCondition::glob("trace.transaction", "trans*"),
1092            ),
1093            (
1094                "multiple releases",
1095                RuleCondition::glob("trace.release", vec!["2.1.1", "1.1.*"])
1096                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1097                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1098            ),
1099            (
1100                "multiple user segments",
1101                RuleCondition::glob("trace.release", "1.1.1")
1102                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1103                    & RuleCondition::eq_ignore_case(
1104                        "trace.user.segment",
1105                        vec!["paid", "vip", "free"],
1106                    ),
1107            ),
1108            (
1109                "multiple transactions",
1110                RuleCondition::glob("trace.transaction", &["t22", "trans*", "t33"][..]),
1111            ),
1112            (
1113                "case insensitive user segments",
1114                RuleCondition::glob("trace.release", "1.1.1")
1115                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1116                    & RuleCondition::eq_ignore_case("trace.user.segment", &["ViP", "FrEe"][..]),
1117            ),
1118            (
1119                "multiple user environments",
1120                RuleCondition::glob("trace.release", "1.1.1")
1121                    & RuleCondition::eq_ignore_case(
1122                        "trace.environment",
1123                        &["integration", "debug", "production"][..],
1124                    )
1125                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1126            ),
1127            (
1128                "case insensitive environments",
1129                RuleCondition::glob("trace.release", "1.1.1")
1130                    & RuleCondition::eq_ignore_case("trace.environment", &["DeBuG", "PrOd"][..])
1131                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1132            ),
1133            (
1134                "all environments",
1135                RuleCondition::glob("trace.release", "1.1.1")
1136                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1137            ),
1138            (
1139                "undefined environments",
1140                RuleCondition::glob("trace.release", "1.1.1")
1141                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1142            ),
1143            ("match no conditions", RuleCondition::all()),
1144            ("string cmp", RuleCondition::gt("trace.transaction", "t")),
1145        ];
1146
1147        let trace = mock_trace();
1148
1149        for (rule_test_name, condition) in conditions.iter() {
1150            let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1151            assert!(condition.matches(&trace), "{failure_name}");
1152        }
1153    }
1154
1155    #[test]
1156    fn test_or_combinator() {
1157        let conditions = [
1158            (
1159                "both",
1160                true,
1161                RuleCondition::eq_ignore_case("trace.environment", "debug")
1162                    | RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1163            ),
1164            (
1165                "first",
1166                true,
1167                RuleCondition::eq_ignore_case("trace.environment", "debug")
1168                    | RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1169            ),
1170            (
1171                "second",
1172                true,
1173                RuleCondition::eq_ignore_case("trace.environment", "prod")
1174                    | RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1175            ),
1176            (
1177                "none",
1178                false,
1179                RuleCondition::eq_ignore_case("trace.environment", "prod")
1180                    | RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1181            ),
1182            (
1183                "empty",
1184                false,
1185                RuleCondition::Or(OrCondition { inner: vec![] }),
1186            ),
1187            ("never", false, RuleCondition::never()),
1188        ];
1189
1190        let trace = mock_trace();
1191
1192        for (rule_test_name, expected, condition) in conditions.iter() {
1193            let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1194            assert!(condition.matches(&trace) == *expected, "{failure_name}");
1195        }
1196    }
1197
1198    #[test]
1199    fn test_and_combinator() {
1200        let conditions = [
1201            (
1202                "both",
1203                true,
1204                RuleCondition::eq_ignore_case("trace.environment", "debug")
1205                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1206            ),
1207            (
1208                "first",
1209                false,
1210                RuleCondition::eq_ignore_case("trace.environment", "debug")
1211                    & RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1212            ),
1213            (
1214                "second",
1215                false,
1216                RuleCondition::eq_ignore_case("trace.environment", "prod")
1217                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1218            ),
1219            (
1220                "none",
1221                false,
1222                RuleCondition::eq_ignore_case("trace.environment", "prod")
1223                    & RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1224            ),
1225            (
1226                "empty",
1227                true,
1228                RuleCondition::And(AndCondition { inner: vec![] }),
1229            ),
1230            ("all", true, RuleCondition::all()),
1231        ];
1232
1233        let trace = mock_trace();
1234
1235        for (rule_test_name, expected, condition) in conditions.iter() {
1236            let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1237            assert!(condition.matches(&trace) == *expected, "{failure_name}");
1238        }
1239    }
1240
1241    #[test]
1242    fn test_not_combinator() {
1243        let conditions = [
1244            (
1245                "not true",
1246                false,
1247                !RuleCondition::eq_ignore_case("trace.environment", "debug"),
1248            ),
1249            (
1250                "not false",
1251                true,
1252                !RuleCondition::eq_ignore_case("trace.environment", "prod"),
1253            ),
1254        ];
1255
1256        let trace = mock_trace();
1257
1258        for (rule_test_name, expected, condition) in conditions.iter() {
1259            let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1260            assert!(condition.matches(&trace) == *expected, "{failure_name}");
1261        }
1262    }
1263
1264    #[test]
1265    /// test various rules that do not match
1266    fn test_does_not_match() {
1267        let conditions = [
1268            (
1269                "release",
1270                RuleCondition::glob("trace.release", "1.1.2")
1271                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1272                    & RuleCondition::eq_ignore_case("trace.user", "vip"),
1273            ),
1274            (
1275                "user segment",
1276                RuleCondition::glob("trace.release", "1.1.1")
1277                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1278                    & RuleCondition::eq_ignore_case("trace.user", "all"),
1279            ),
1280            (
1281                "environment",
1282                RuleCondition::glob("trace.release", "1.1.1")
1283                    & RuleCondition::eq_ignore_case("trace.environment", "prod")
1284                    & RuleCondition::eq_ignore_case("trace.user", "vip"),
1285            ),
1286            (
1287                "transaction",
1288                RuleCondition::glob("trace.release", "1.1.1")
1289                    & RuleCondition::glob("trace.transaction", "t22")
1290                    & RuleCondition::eq_ignore_case("trace.user", "vip"),
1291            ),
1292        ];
1293
1294        let trace = mock_trace();
1295
1296        for (rule_test_name, condition) in conditions.iter() {
1297            let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1298            assert!(!condition.matches(&trace), "{failure_name}");
1299        }
1300    }
1301
1302    #[test]
1303    fn test_any_condition_with_match() {
1304        let condition = RuleCondition::for_any(
1305            "trace.exceptions",
1306            RuleCondition::glob("name", "*Exception"),
1307        );
1308
1309        let trace = mock_trace();
1310
1311        assert!(condition.matches(&trace));
1312    }
1313
1314    #[test]
1315    fn test_any_condition_with_no_match() {
1316        let condition =
1317            RuleCondition::for_any("trace.exceptions", RuleCondition::glob("name", "Error"));
1318
1319        let trace = mock_trace();
1320
1321        assert!(!condition.matches(&trace));
1322    }
1323
1324    #[test]
1325    fn test_all_condition() {
1326        let condition =
1327            RuleCondition::for_all("trace.exceptions", RuleCondition::glob("name", "Null*"));
1328
1329        let trace = mock_trace();
1330
1331        assert!(condition.matches(&trace));
1332    }
1333
1334    #[test]
1335    fn test_all_condition_with_no_match() {
1336        let condition = RuleCondition::for_all(
1337            "trace.exceptions",
1338            RuleCondition::glob("name", "*Exception"),
1339        );
1340
1341        let trace = mock_trace();
1342
1343        assert!(!condition.matches(&trace));
1344    }
1345}