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(val)) => self.cmp(f, val),
83            (Some(Val::String(f)), Value::Array(arr)) => arr
84                .iter()
85                .filter_map(|v| v.as_str())
86                .any(|v| self.cmp(v, f)),
87            (Some(Val::HexId(f)), Value::String(val)) => f.match_str(val),
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 uuid::Uuid;
841
842    use super::*;
843    use crate::{GetterIter, HexId};
844
845    #[derive(Debug)]
846    struct Exception {
847        name: String,
848    }
849
850    impl Getter for Exception {
851        fn get_value(&self, path: &str) -> Option<Val<'_>> {
852            Some(match path {
853                "name" => self.name.as_str().into(),
854                _ => return None,
855            })
856        }
857    }
858
859    struct Trace {
860        trace_id: Uuid,
861        span_id: [u8; 4],
862        transaction: String,
863        release: String,
864        environment: String,
865        user_segment: String,
866        exceptions: Vec<Exception>,
867    }
868
869    impl Getter for Trace {
870        fn get_value(&self, path: &str) -> Option<Val<'_>> {
871            Some(match path.strip_prefix("trace.")? {
872                "trace_id" => (&self.trace_id).into(),
873                "span_id" => Val::HexId(HexId(&self.span_id[..])),
874                "transaction" => self.transaction.as_str().into(),
875                "release" => self.release.as_str().into(),
876                "environment" => self.environment.as_str().into(),
877                "user.segment" => self.user_segment.as_str().into(),
878                _ => {
879                    return None;
880                }
881            })
882        }
883
884        fn get_iter(&self, path: &str) -> Option<GetterIter<'_>> {
885            Some(match path.strip_prefix("trace.")? {
886                "exceptions" => GetterIter::new(self.exceptions.iter()),
887                _ => return None,
888            })
889        }
890    }
891
892    fn mock_trace() -> Trace {
893        Trace {
894            trace_id: "6b7d15b8-cee2-4354-9fee-dae7ef43e434".parse().unwrap(),
895            span_id: [0xde, 0xad, 0xbe, 0xef],
896            transaction: "transaction1".to_string(),
897            release: "1.1.1".to_string(),
898            environment: "debug".to_string(),
899            user_segment: "vip".to_string(),
900            exceptions: vec![
901                Exception {
902                    name: "NullPointerException".to_string(),
903                },
904                Exception {
905                    name: "NullUser".to_string(),
906                },
907            ],
908        }
909    }
910
911    #[test]
912    fn deserialize() {
913        let serialized_rules = r#"[
914            {
915                "op":"eq",
916                "name": "field_1",
917                "value": ["UPPER","lower"],
918                "options":{
919                    "ignoreCase": true
920                }
921            },
922            {
923                "op":"eq",
924                "name": "field_2",
925                "value": ["UPPER","lower"]
926            },
927            {
928                "op":"glob",
929                "name": "field_3",
930                "value": ["1.2.*","2.*"]
931            },
932            {
933                "op":"not",
934                "inner": {
935                    "op":"glob",
936                    "name": "field_4",
937                    "value": ["1.*"]
938                }
939            },
940            {
941                "op":"and",
942                "inner": [{
943                    "op":"glob",
944                    "name": "field_5",
945                    "value": ["2.*"]
946                }]
947            },
948            {
949                "op":"or",
950                "inner": [{
951                    "op":"glob",
952                    "name": "field_6",
953                    "value": ["3.*"]
954                }]
955            },
956            {
957                "op": "any",
958                "name": "obj.exceptions",
959                "inner": {
960                    "op": "glob",
961                    "name": "value",
962                    "value": ["*Exception"]
963                }
964            },
965            {
966                "op": "all",
967                "name": "obj.exceptions",
968                "inner": {
969                    "op": "glob",
970                    "name": "value",
971                    "value": ["*Exception"]
972                }
973            }
974        ]"#;
975
976        let rules: Result<Vec<RuleCondition>, _> = serde_json::from_str(serialized_rules);
977        assert!(rules.is_ok());
978        let rules = rules.unwrap();
979        insta::assert_ron_snapshot!(rules, @r###"
980        [
981          EqCondition(
982            op: "eq",
983            name: "field_1",
984            value: [
985              "UPPER",
986              "lower",
987            ],
988            options: EqCondOptions(
989              ignoreCase: true,
990            ),
991          ),
992          EqCondition(
993            op: "eq",
994            name: "field_2",
995            value: [
996              "UPPER",
997              "lower",
998            ],
999          ),
1000          GlobCondition(
1001            op: "glob",
1002            name: "field_3",
1003            value: [
1004              "1.2.*",
1005              "2.*",
1006            ],
1007          ),
1008          NotCondition(
1009            op: "not",
1010            inner: GlobCondition(
1011              op: "glob",
1012              name: "field_4",
1013              value: [
1014                "1.*",
1015              ],
1016            ),
1017          ),
1018          AndCondition(
1019            op: "and",
1020            inner: [
1021              GlobCondition(
1022                op: "glob",
1023                name: "field_5",
1024                value: [
1025                  "2.*",
1026                ],
1027              ),
1028            ],
1029          ),
1030          OrCondition(
1031            op: "or",
1032            inner: [
1033              GlobCondition(
1034                op: "glob",
1035                name: "field_6",
1036                value: [
1037                  "3.*",
1038                ],
1039              ),
1040            ],
1041          ),
1042          AnyCondition(
1043            op: "any",
1044            name: "obj.exceptions",
1045            inner: GlobCondition(
1046              op: "glob",
1047              name: "value",
1048              value: [
1049                "*Exception",
1050              ],
1051            ),
1052          ),
1053          AllCondition(
1054            op: "all",
1055            name: "obj.exceptions",
1056            inner: GlobCondition(
1057              op: "glob",
1058              name: "value",
1059              value: [
1060                "*Exception",
1061              ],
1062            ),
1063          ),
1064        ]
1065        "###);
1066    }
1067
1068    #[test]
1069    fn unsupported_rule_deserialize() {
1070        let bad_json = r#"{
1071            "op": "BadOperator",
1072            "name": "foo",
1073            "value": "bar"
1074        }"#;
1075
1076        let rule: RuleCondition = serde_json::from_str(bad_json).unwrap();
1077        assert!(matches!(rule, RuleCondition::Unsupported));
1078    }
1079
1080    #[test]
1081    /// test matching for various rules
1082    fn test_matches() {
1083        let conditions = [
1084            (
1085                "simple",
1086                RuleCondition::glob("trace.release", "1.1.1")
1087                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1088                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip")
1089                    & RuleCondition::eq_ignore_case("trace.transaction", "transaction1"),
1090            ),
1091            (
1092                "glob releases",
1093                RuleCondition::glob("trace.release", "1.*")
1094                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1095                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1096            ),
1097            (
1098                "glob transaction",
1099                RuleCondition::glob("trace.transaction", "trans*"),
1100            ),
1101            (
1102                "multiple releases",
1103                RuleCondition::glob("trace.release", vec!["2.1.1", "1.1.*"])
1104                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1105                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1106            ),
1107            (
1108                "multiple user segments",
1109                RuleCondition::glob("trace.release", "1.1.1")
1110                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1111                    & RuleCondition::eq_ignore_case(
1112                        "trace.user.segment",
1113                        vec!["paid", "vip", "free"],
1114                    ),
1115            ),
1116            (
1117                "multiple transactions",
1118                RuleCondition::glob("trace.transaction", &["t22", "trans*", "t33"][..]),
1119            ),
1120            (
1121                "case insensitive user segments",
1122                RuleCondition::glob("trace.release", "1.1.1")
1123                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1124                    & RuleCondition::eq_ignore_case("trace.user.segment", &["ViP", "FrEe"][..]),
1125            ),
1126            (
1127                "multiple user environments",
1128                RuleCondition::glob("trace.release", "1.1.1")
1129                    & RuleCondition::eq_ignore_case(
1130                        "trace.environment",
1131                        &["integration", "debug", "production"][..],
1132                    )
1133                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1134            ),
1135            (
1136                "case insensitive environments",
1137                RuleCondition::glob("trace.release", "1.1.1")
1138                    & RuleCondition::eq_ignore_case("trace.environment", &["DeBuG", "PrOd"][..])
1139                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1140            ),
1141            (
1142                "all environments",
1143                RuleCondition::glob("trace.release", "1.1.1")
1144                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1145            ),
1146            (
1147                "undefined environments",
1148                RuleCondition::glob("trace.release", "1.1.1")
1149                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1150            ),
1151            (
1152                "trace/span ID bytes",
1153                RuleCondition::eq("trace.trace_id", "6b7d15b8cee243549feedae7ef43e434")
1154                    & RuleCondition::eq("trace.span_id", "DEADBEEF"),
1155            ),
1156            ("match no conditions", RuleCondition::all()),
1157            ("string cmp", RuleCondition::gt("trace.transaction", "t")),
1158        ];
1159
1160        let trace = mock_trace();
1161
1162        for (rule_test_name, condition) in conditions.iter() {
1163            let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1164            assert!(condition.matches(&trace), "{failure_name}");
1165        }
1166    }
1167
1168    #[test]
1169    fn test_or_combinator() {
1170        let conditions = [
1171            (
1172                "both",
1173                true,
1174                RuleCondition::eq_ignore_case("trace.environment", "debug")
1175                    | RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1176            ),
1177            (
1178                "first",
1179                true,
1180                RuleCondition::eq_ignore_case("trace.environment", "debug")
1181                    | RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1182            ),
1183            (
1184                "second",
1185                true,
1186                RuleCondition::eq_ignore_case("trace.environment", "prod")
1187                    | RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1188            ),
1189            (
1190                "none",
1191                false,
1192                RuleCondition::eq_ignore_case("trace.environment", "prod")
1193                    | RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1194            ),
1195            (
1196                "empty",
1197                false,
1198                RuleCondition::Or(OrCondition { inner: vec![] }),
1199            ),
1200            ("never", false, RuleCondition::never()),
1201        ];
1202
1203        let trace = mock_trace();
1204
1205        for (rule_test_name, expected, condition) in conditions.iter() {
1206            let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1207            assert!(condition.matches(&trace) == *expected, "{failure_name}");
1208        }
1209    }
1210
1211    #[test]
1212    fn test_and_combinator() {
1213        let conditions = [
1214            (
1215                "both",
1216                true,
1217                RuleCondition::eq_ignore_case("trace.environment", "debug")
1218                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1219            ),
1220            (
1221                "first",
1222                false,
1223                RuleCondition::eq_ignore_case("trace.environment", "debug")
1224                    & RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1225            ),
1226            (
1227                "second",
1228                false,
1229                RuleCondition::eq_ignore_case("trace.environment", "prod")
1230                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1231            ),
1232            (
1233                "none",
1234                false,
1235                RuleCondition::eq_ignore_case("trace.environment", "prod")
1236                    & RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1237            ),
1238            (
1239                "empty",
1240                true,
1241                RuleCondition::And(AndCondition { inner: vec![] }),
1242            ),
1243            ("all", true, RuleCondition::all()),
1244        ];
1245
1246        let trace = mock_trace();
1247
1248        for (rule_test_name, expected, condition) in conditions.iter() {
1249            let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1250            assert!(condition.matches(&trace) == *expected, "{failure_name}");
1251        }
1252    }
1253
1254    #[test]
1255    fn test_not_combinator() {
1256        let conditions = [
1257            (
1258                "not true",
1259                false,
1260                !RuleCondition::eq_ignore_case("trace.environment", "debug"),
1261            ),
1262            (
1263                "not false",
1264                true,
1265                !RuleCondition::eq_ignore_case("trace.environment", "prod"),
1266            ),
1267        ];
1268
1269        let trace = mock_trace();
1270
1271        for (rule_test_name, expected, condition) in conditions.iter() {
1272            let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1273            assert!(condition.matches(&trace) == *expected, "{failure_name}");
1274        }
1275    }
1276
1277    #[test]
1278    /// test various rules that do not match
1279    fn test_does_not_match() {
1280        let conditions = [
1281            (
1282                "release",
1283                RuleCondition::glob("trace.release", "1.1.2")
1284                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1285                    & RuleCondition::eq_ignore_case("trace.user", "vip"),
1286            ),
1287            (
1288                "user segment",
1289                RuleCondition::glob("trace.release", "1.1.1")
1290                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1291                    & RuleCondition::eq_ignore_case("trace.user", "all"),
1292            ),
1293            (
1294                "environment",
1295                RuleCondition::glob("trace.release", "1.1.1")
1296                    & RuleCondition::eq_ignore_case("trace.environment", "prod")
1297                    & RuleCondition::eq_ignore_case("trace.user", "vip"),
1298            ),
1299            (
1300                "transaction",
1301                RuleCondition::glob("trace.release", "1.1.1")
1302                    & RuleCondition::glob("trace.transaction", "t22")
1303                    & RuleCondition::eq_ignore_case("trace.user", "vip"),
1304            ),
1305            ("span ID", RuleCondition::eq("trace.span_id", "deadbeer")),
1306        ];
1307
1308        let trace = mock_trace();
1309
1310        for (rule_test_name, condition) in conditions.iter() {
1311            let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1312            assert!(!condition.matches(&trace), "{failure_name}");
1313        }
1314    }
1315
1316    #[test]
1317    fn test_any_condition_with_match() {
1318        let condition = RuleCondition::for_any(
1319            "trace.exceptions",
1320            RuleCondition::glob("name", "*Exception"),
1321        );
1322
1323        let trace = mock_trace();
1324
1325        assert!(condition.matches(&trace));
1326    }
1327
1328    #[test]
1329    fn test_any_condition_with_no_match() {
1330        let condition =
1331            RuleCondition::for_any("trace.exceptions", RuleCondition::glob("name", "Error"));
1332
1333        let trace = mock_trace();
1334
1335        assert!(!condition.matches(&trace));
1336    }
1337
1338    #[test]
1339    fn test_all_condition() {
1340        let condition =
1341            RuleCondition::for_all("trace.exceptions", RuleCondition::glob("name", "Null*"));
1342
1343        let trace = mock_trace();
1344
1345        assert!(condition.matches(&trace));
1346    }
1347
1348    #[test]
1349    fn test_all_condition_with_no_match() {
1350        let condition = RuleCondition::for_all(
1351            "trace.exceptions",
1352            RuleCondition::glob("name", "*Exception"),
1353        );
1354
1355        let trace = mock_trace();
1356
1357        assert!(!condition.matches(&trace));
1358    }
1359}