Skip to main content

relay_protocol/
condition.rs

1//! Types to specify conditions on data.
2//!
3//! The root type is [`RuleCondition`].
4
5use std::collections::BTreeSet;
6use std::net::IpAddr;
7
8use ipnetwork::IpNetwork;
9use relay_pattern::{CaseInsensitive, Gas, TypedPatterns};
10use serde::{Deserialize, Deserializer, Serialize};
11use serde_json::Value;
12
13pub use crate::release::Release;
14use crate::{Getter, Val};
15
16/// Options for [`EqCondition`].
17#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
18#[serde(rename_all = "camelCase")]
19pub struct EqCondOptions {
20    /// If `true`, string values are compared in case-insensitive mode.
21    ///
22    /// This has no effect on numeric or boolean comparisons.
23    #[serde(default)]
24    pub ignore_case: bool,
25}
26
27/// A condition that compares values for equality.
28///
29/// This operator supports:
30///  - boolean
31///  - strings, optionally ignoring ASCII-case
32///  - UUIDs
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct EqCondition {
36    /// Path of the field that should match the value.
37    pub name: String,
38
39    /// The value to check against.
40    ///
41    /// When comparing with a string field, this value can be an array. The condition matches if any
42    /// of the provided values matches the field.
43    pub value: Value,
44
45    /// Configuration options for the condition.
46    #[serde(default, skip_serializing_if = "is_default")]
47    pub options: EqCondOptions,
48}
49
50impl EqCondition {
51    /// Creates a new condition that checks for equality.
52    ///
53    /// By default, this condition will perform a case-sensitive check. To ignore ASCII case, use
54    /// [`EqCondition::ignore_case`].
55    ///
56    /// The main way to create this conditions is [`RuleCondition::eq`].
57    pub fn new(field: impl Into<String>, value: impl Into<Value>) -> Self {
58        Self {
59            name: field.into(),
60            value: value.into(),
61            options: EqCondOptions { ignore_case: false },
62        }
63    }
64
65    /// Enables case-insensitive comparisions for this rule.
66    ///
67    /// To create such a condition directly, use [`RuleCondition::eq_ignore_case`].
68    pub fn ignore_case(mut self) -> Self {
69        self.options.ignore_case = true;
70        self
71    }
72
73    fn cmp(&self, left: &str, right: &str) -> bool {
74        if self.options.ignore_case {
75            unicase::eq(left, right)
76        } else {
77            left == right
78        }
79    }
80
81    fn matches<T>(&self, instance: &T) -> bool
82    where
83        T: Getter + ?Sized,
84    {
85        match (instance.get_value(self.name.as_str()), &self.value) {
86            (None, Value::Null) => true,
87            (Some(Val::String(f)), Value::String(val)) => self.cmp(f, val),
88            (Some(Val::String(f)), Value::Array(arr)) => arr
89                .iter()
90                .filter_map(|v| v.as_str())
91                .any(|v| self.cmp(v, f)),
92            (Some(Val::HexId(f)), Value::String(val)) => f.match_str(val),
93            (Some(Val::IpAddr(f)), Value::String(val)) => val.parse::<IpAddr>() == Ok(f),
94            (Some(Val::Bool(f)), Value::Bool(v)) => f == *v,
95            _ => false,
96        }
97    }
98}
99
100/// Returns `true` if this value is equal to `Default::default()`.
101fn is_default<T: Default + PartialEq>(t: &T) -> bool {
102    *t == T::default()
103}
104
105macro_rules! impl_cmp_condition {
106    ($struct_name:ident, $operator:tt, $doc:literal) => {
107        #[doc = $doc]
108        ///
109        /// Strings are explicitly not supported by this.
110        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111        pub struct $struct_name {
112            /// Path of the field that should match the value.
113            pub name: String,
114            /// The numeric value to check against.
115            pub value: Value,
116        }
117
118        impl $struct_name {
119            /// Creates a new condition that comparison condition.
120            pub fn new(field: impl Into<String>, value: impl Into<Value>) -> Self {
121                Self {
122                    name: field.into(),
123                    value: value.into(),
124                }
125            }
126
127            fn matches<T>(&self, instance: &T) -> bool
128            where
129                T: Getter + ?Sized,
130            {
131                let Some(value) = instance.get_value(self.name.as_str()) else {
132                    return false;
133                };
134
135                // Try various conversion functions in order of expensiveness and likelihood
136                // - as_i64 is not really fast, but most values in sampling rules can be i64, so we
137                //   could return early
138                // - f64 is more likely to succeed than u64, but we might lose precision
139                if let (Some(a), Some(b)) = (value.as_i64(), self.value.as_i64()) {
140                    a $operator b
141                } else if let (Some(a), Some(b)) = (value.as_u64(), self.value.as_u64()) {
142                    a $operator b
143                } else if let (Some(a), Some(b)) = (value.as_f64(), self.value.as_f64()) {
144                    a $operator b
145                } else if let (Some(a), Some(b)) = (value.as_str(), self.value.as_str()) {
146                    a $operator b
147                } else {
148                    false
149                }
150            }
151        }
152    }
153}
154
155impl_cmp_condition!(GteCondition, >=, "A condition that applies `>=`.");
156impl_cmp_condition!(LteCondition, <=, "A condition that applies `<=`.");
157impl_cmp_condition!(GtCondition, >, "A condition that applies `>`.");
158impl_cmp_condition!(LtCondition, <, "A condition that applies `<`.");
159
160/// A condition that uses glob matching.
161///
162/// This is similar to [`EqCondition`], but it allows for wildcards in `value`. This is slightly
163/// more expensive to construct and check, so preferrably use [`EqCondition`] when no wildcard
164/// matching is needed.
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166pub struct GlobCondition {
167    /// Path of the field that should match the value.
168    pub name: String,
169    /// A list of glob patterns to check.
170    ///
171    /// Note that this cannot be a single value, it must be a list of values.
172    pub value: TypedPatterns<CaseInsensitive>,
173}
174
175impl GlobCondition {
176    /// Creates a condition that matches one or more glob patterns.
177    pub fn new(field: impl Into<String>, value: impl IntoStrings) -> Self {
178        Self {
179            name: field.into(),
180            value: TypedPatterns::from(value.into_strings()),
181        }
182    }
183
184    fn matches<T>(&self, instance: &T) -> bool
185    where
186        T: Getter + ?Sized,
187    {
188        match instance.get_value(self.name.as_str()) {
189            Some(Val::String(s)) => self.value.is_match(s, Gas::CONSTRAINED).unwrap_or(false),
190            _ => false,
191        }
192    }
193}
194
195/// A type that can be converted to a list of strings.
196pub trait IntoStrings {
197    /// Creates a list of strings from this type.
198    fn into_strings(self) -> Vec<String>;
199}
200
201impl IntoStrings for &'_ str {
202    fn into_strings(self) -> Vec<String> {
203        vec![self.to_owned()]
204    }
205}
206
207impl IntoStrings for String {
208    fn into_strings(self) -> Vec<String> {
209        vec![self]
210    }
211}
212
213impl IntoStrings for std::borrow::Cow<'_, str> {
214    fn into_strings(self) -> Vec<String> {
215        vec![self.into_owned()]
216    }
217}
218
219impl IntoStrings for &'_ [&'_ str] {
220    fn into_strings(self) -> Vec<String> {
221        self.iter().copied().map(str::to_owned).collect()
222    }
223}
224
225impl IntoStrings for &'_ [String] {
226    fn into_strings(self) -> Vec<String> {
227        self.to_vec()
228    }
229}
230
231impl IntoStrings for Vec<&'_ str> {
232    fn into_strings(self) -> Vec<String> {
233        self.into_iter().map(str::to_owned).collect()
234    }
235}
236
237impl IntoStrings for Vec<String> {
238    fn into_strings(self) -> Vec<String> {
239        self
240    }
241}
242
243/// A set of IP addresses and CIDR ranges.
244///
245/// Serialized as a list of strings such as `"10.0.0.1"` or `"10.0.0.0/8"`. Entries that do not
246/// parse as an address or a range are skipped while deserializing, like invalid glob patterns.
247#[derive(Debug, Clone, PartialEq, Default, Serialize)]
248#[serde(transparent)]
249pub struct IpNetworks(BTreeSet<IpNetwork>);
250
251impl IpNetworks {
252    /// Returns `true` if any of the networks contains the address.
253    pub fn contains(&self, ip: IpAddr) -> bool {
254        self.0.iter().any(|network| network.contains(ip))
255    }
256}
257
258impl<S: AsRef<str>> FromIterator<S> for IpNetworks {
259    fn from_iter<I: IntoIterator<Item = S>>(iter: I) -> Self {
260        Self(
261            iter.into_iter()
262                .filter_map(|entry| entry.as_ref().parse().ok())
263                .collect(),
264        )
265    }
266}
267
268impl<'de> Deserialize<'de> for IpNetworks {
269    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
270        Ok(Vec::<String>::deserialize(deserializer)?
271            .into_iter()
272            .collect())
273    }
274}
275
276/// A condition that checks whether an IP address lies in any of the given networks.
277///
278/// The field must hold an IPv4 or IPv6 address, either as [`Val::IpAddr`] or as a string. Each
279/// entry in `value` is a single address or a CIDR range; the condition matches if the address is
280/// contained in any of them.
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
282pub struct CidrCondition {
283    /// Path of the field that holds the IP address.
284    pub name: String,
285    /// IP addresses and CIDR ranges to check against.
286    ///
287    /// Note that this cannot be a single value, it must be a list of values.
288    pub value: IpNetworks,
289}
290
291impl CidrCondition {
292    /// Creates a condition that matches addresses inside one or more networks.
293    ///
294    /// Entries that do not parse as an address or a CIDR range are skipped.
295    pub fn new(field: impl Into<String>, value: impl IntoStrings) -> Self {
296        Self {
297            name: field.into(),
298            value: value.into_strings().into_iter().collect(),
299        }
300    }
301
302    fn matches<T>(&self, instance: &T) -> bool
303    where
304        T: Getter + ?Sized,
305    {
306        let ip = match instance.get_value(self.name.as_str()) {
307            Some(Val::IpAddr(ip)) => ip,
308            Some(Val::String(s)) => match s.parse() {
309                Ok(ip) => ip,
310                Err(_) => return false,
311            },
312            _ => return false,
313        };
314
315        self.value.contains(ip)
316    }
317}
318
319/// The comparison a [`ReleaseCondition`] applies.
320#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub enum ReleaseComparator {
323    /// The version of the field is equal to the value.
324    ///
325    /// Unlike [`EqCondition`], this ignores the build code of the release.
326    Eq,
327    /// The version of the field is greater than the value.
328    Gt,
329    /// The version of the field is greater than or equal to the value.
330    Gte,
331    /// The version of the field is less than the value.
332    Lt,
333    /// The version of the field is less than or equal to the value.
334    Lte,
335    /// An unsupported comparator for future compatibility. It never matches.
336    #[serde(other)]
337    Unsupported,
338}
339
340/// A condition that compares the version of a release.
341///
342/// The field must hold a release, which compares against `value` as [`Release::compare`]
343/// describes. If `value` names a package, the condition only matches releases of that package.
344///
345/// The condition does not match if the field has no version, such as a commit hash. A configured
346/// release without a version deserializes to a `value` of `None`. Such a condition never matches
347/// and is not [supported](RuleCondition::supported), so that the author of the condition can
348/// reject it.
349#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
350pub struct ReleaseCondition {
351    /// Path of the field that holds the release.
352    pub name: String,
353    /// The comparison to apply between the field and the value.
354    pub comparator: ReleaseComparator,
355    /// The release to compare the field against, such as `1.2.0` or `myapp@1.2.0`.
356    #[serde(default, deserialize_with = "deserialize_release_lenient")]
357    pub value: Option<Release>,
358}
359
360impl ReleaseCondition {
361    /// Creates a condition that compares the version of a release.
362    pub fn new(field: impl Into<String>, comparator: ReleaseComparator, value: &str) -> Self {
363        Self {
364            name: field.into(),
365            comparator,
366            value: Release::parse(value),
367        }
368    }
369
370    fn supported(&self) -> bool {
371        self.comparator != ReleaseComparator::Unsupported && self.value.is_some()
372    }
373
374    fn matches<T>(&self, instance: &T) -> bool
375    where
376        T: Getter + ?Sized,
377    {
378        let Some(Val::String(release)) = instance.get_value(self.name.as_str()) else {
379            return false;
380        };
381        let Some(ordering) = self.value.as_ref().and_then(|value| value.compare(release)) else {
382            return false;
383        };
384
385        match self.comparator {
386            ReleaseComparator::Eq => ordering.is_eq(),
387            ReleaseComparator::Gt => ordering.is_gt(),
388            ReleaseComparator::Gte => ordering.is_ge(),
389            ReleaseComparator::Lt => ordering.is_lt(),
390            ReleaseComparator::Lte => ordering.is_le(),
391            ReleaseComparator::Unsupported => false,
392        }
393    }
394}
395
396fn deserialize_release_lenient<'de, D: Deserializer<'de>>(
397    deserializer: D,
398) -> Result<Option<Release>, D::Error> {
399    let release = Option::<String>::deserialize(deserializer)?;
400    Ok(release.and_then(|release| Release::parse(&release)))
401}
402
403/// Combines multiple conditions using logical OR.
404///
405/// This condition matches if **any** of the inner conditions match. The default value for this
406/// condition is `false`, that is, this rule does not match if there are no inner conditions.
407///
408/// See [`RuleCondition::or`].
409#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
410pub struct OrCondition {
411    /// Inner rules to combine.
412    pub inner: Vec<RuleCondition>,
413}
414
415impl OrCondition {
416    fn supported(&self) -> bool {
417        self.inner.iter().all(RuleCondition::supported)
418    }
419
420    fn matches<T>(&self, value: &T) -> bool
421    where
422        T: Getter + ?Sized,
423    {
424        self.inner.iter().any(|cond| cond.matches(value))
425    }
426}
427
428/// Combines multiple conditions using logical AND.
429///
430/// This condition matches if **all** of the inner conditions match. The default value for this
431/// condition is `true`, that is, this rule matches if there are no inner conditions.
432///
433/// See [`RuleCondition::and`].
434#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
435pub struct AndCondition {
436    /// Inner rules to combine.
437    pub inner: Vec<RuleCondition>,
438}
439
440impl AndCondition {
441    fn supported(&self) -> bool {
442        self.inner.iter().all(RuleCondition::supported)
443    }
444    fn matches<T>(&self, value: &T) -> bool
445    where
446        T: Getter + ?Sized,
447    {
448        self.inner.iter().all(|cond| cond.matches(value))
449    }
450}
451
452/// Applies logical NOT to a condition.
453///
454/// This condition matches if the inner condition does not match.
455///
456/// See [`RuleCondition::negate`].
457#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
458pub struct NotCondition {
459    /// An inner rule to negate.
460    pub inner: Box<RuleCondition>,
461}
462
463impl NotCondition {
464    fn supported(&self) -> bool {
465        self.inner.supported()
466    }
467
468    fn matches<T>(&self, value: &T) -> bool
469    where
470        T: Getter + ?Sized,
471    {
472        !self.inner.matches(value)
473    }
474}
475
476/// Applies the ANY operation to an array field.
477///
478/// This condition matches if at least one of the elements of the array match with the
479/// `inner` condition.
480#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
481pub struct AnyCondition {
482    /// Path of the field that should match the value.
483    pub name: String,
484    /// Inner rule to match on each element.
485    pub inner: Box<RuleCondition>,
486}
487
488impl AnyCondition {
489    /// Creates a condition that matches any element in the array against the `inner` condition.
490    pub fn new(field: impl Into<String>, inner: RuleCondition) -> Self {
491        Self {
492            name: field.into(),
493            inner: Box::new(inner),
494        }
495    }
496
497    fn supported(&self) -> bool {
498        self.inner.supported()
499    }
500    fn matches<T>(&self, instance: &T) -> bool
501    where
502        T: Getter + ?Sized,
503    {
504        let Some(mut getter_iter) = instance.get_iter(self.name.as_str()) else {
505            return false;
506        };
507
508        getter_iter.any(|g| self.inner.matches(g))
509    }
510}
511
512/// Applies the ALL operation to an array field.
513///
514/// This condition matches if all the elements of the array match with the `inner` condition.
515#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
516pub struct AllCondition {
517    /// Path of the field that should match the value.
518    pub name: String,
519    /// Inner rule to match on each element.
520    pub inner: Box<RuleCondition>,
521}
522
523impl AllCondition {
524    /// Creates a condition that matches all the elements in the array against the `inner`
525    /// condition.
526    pub fn new(field: impl Into<String>, inner: RuleCondition) -> Self {
527        Self {
528            name: field.into(),
529            inner: Box::new(inner),
530        }
531    }
532
533    fn supported(&self) -> bool {
534        self.inner.supported()
535    }
536    fn matches<T>(&self, instance: &T) -> bool
537    where
538        T: Getter + ?Sized,
539    {
540        let Some(mut getter_iter) = instance.get_iter(self.name.as_str()) else {
541            return false;
542        };
543
544        getter_iter.all(|g| self.inner.matches(g))
545    }
546}
547
548/// A condition that can be evaluated on structured data.
549///
550/// The basic conditions are [`eq`](Self::eq), [`glob`](Self::glob), and the comparison operators.
551/// These conditions compare a data field specified through a path with a value or a set of values.
552/// If the field's value [matches](Self::matches) the values declared in the rule, the condition
553/// returns `true`.
554///
555/// Conditions can be combined with the logical operators [`and`](Self::and), [`or`](Self::or), and
556/// [`not` (negate)](Self::negate).
557///
558/// # Data Access
559///
560/// Rule conditions access data fields through the [`Getter`] trait. Note that getters always have a
561/// root namespace which must be part of the field's path. If path's root component does not match
562/// the one of the passed getter instance, the rule condition will not be able to retrieve data and
563/// likely not match.
564///
565/// # Serialization
566///
567/// Conditions are represented as nested JSON objects. The condition type is declared in the `op`
568/// field.
569///
570/// # Example
571///
572/// ```
573/// use relay_protocol::RuleCondition;
574///
575/// let condition = !RuleCondition::eq("obj.status", "invalid");
576/// ```
577#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
578#[serde(rename_all = "camelCase", tag = "op")]
579pub enum RuleCondition {
580    /// A condition that compares values for equality.
581    ///
582    /// This operator supports:
583    ///  - boolean
584    ///  - strings, optionally ignoring ASCII-case
585    ///  - UUIDs
586    ///
587    /// # Example
588    ///
589    /// ```
590    /// use relay_protocol::RuleCondition;
591    ///
592    /// let condition = RuleCondition::eq("obj.status", "invalid");
593    /// ```
594    Eq(EqCondition),
595
596    /// A condition that applies `>=`.
597    ///
598    /// # Example
599    ///
600    /// ```
601    /// use relay_protocol::RuleCondition;
602    ///
603    /// let condition = RuleCondition::gte("obj.length", 10);
604    /// ```
605    Gte(GteCondition),
606
607    /// A condition that applies `<=`.
608    ///
609    /// # Example
610    ///
611    /// ```
612    /// use relay_protocol::RuleCondition;
613    ///
614    /// let condition = RuleCondition::lte("obj.length", 10);
615    /// ```
616    Lte(LteCondition),
617
618    /// A condition that applies `>`.
619    ///
620    /// # Example
621    ///
622    /// ```
623    /// use relay_protocol::RuleCondition;
624    ///
625    /// let condition = RuleCondition::gt("obj.length", 10);
626    /// ```
627    Gt(GtCondition),
628
629    /// A condition that applies `<`.
630    ///
631    /// # Example
632    ///
633    /// ```
634    /// use relay_protocol::RuleCondition;
635    ///
636    /// let condition = RuleCondition::lt("obj.length", 10);
637    /// ```
638    Lt(LtCondition),
639
640    /// A condition that uses glob matching.
641    ///
642    /// # Example
643    ///
644    /// ```
645    /// use relay_protocol::RuleCondition;
646    ///
647    /// let condition = RuleCondition::glob("obj.name", "error: *");
648    /// ```
649    Glob(GlobCondition),
650
651    /// A condition that checks whether an IP address lies in any of the given networks.
652    ///
653    /// # Example
654    ///
655    /// ```
656    /// use relay_protocol::RuleCondition;
657    ///
658    /// let condition = RuleCondition::cidr("obj.ip", &["10.0.0.0/8", "192.168.1.1"][..]);
659    /// ```
660    Cidr(CidrCondition),
661
662    /// A condition that compares the version of a release.
663    ///
664    /// # Example
665    ///
666    /// ```
667    /// use relay_protocol::RuleCondition;
668    /// use relay_protocol::condition::ReleaseComparator;
669    ///
670    /// let condition = RuleCondition::release("obj.release", ReleaseComparator::Gte, "1.2.0");
671    /// ```
672    Release(ReleaseCondition),
673
674    /// Combines multiple conditions using logical OR.
675    ///
676    /// # Example
677    ///
678    /// ```
679    /// use relay_protocol::RuleCondition;
680    ///
681    /// let condition = RuleCondition::eq("obj.status", "invalid")
682    ///     | RuleCondition::eq("obj.status", "unknown");
683    /// ```
684    Or(OrCondition),
685
686    /// Combines multiple conditions using logical AND.
687    ///
688    /// # Example
689    ///
690    /// ```
691    /// use relay_protocol::RuleCondition;
692    ///
693    /// let condition = RuleCondition::eq("obj.status", "invalid")
694    ///     & RuleCondition::gte("obj.length", 10);
695    /// ```
696    And(AndCondition),
697
698    /// Applies logical NOT to a condition.
699    ///
700    /// # Example
701    ///
702    /// ```
703    /// use relay_protocol::RuleCondition;
704    ///
705    /// let condition = !RuleCondition::eq("obj.status", "invalid");
706    /// ```
707    Not(NotCondition),
708
709    /// Loops over an array field and returns true if at least one element matches
710    /// the inner condition.
711    ///
712    /// # Example
713    ///
714    /// ```
715    /// use relay_protocol::RuleCondition;
716    ///
717    /// let condition = RuleCondition::for_any("obj.exceptions",
718    ///     RuleCondition::eq("name", "NullPointerException")
719    /// );
720    /// ```
721    Any(AnyCondition),
722
723    /// Loops over an array field and returns true if all elements match the inner condition.
724    ///
725    /// # Example
726    ///
727    /// ```
728    /// use relay_protocol::RuleCondition;
729    ///
730    /// let condition = RuleCondition::for_all("obj.exceptions",
731    ///     RuleCondition::eq("name", "NullPointerException")
732    /// );
733    /// ```
734    All(AllCondition),
735
736    /// An unsupported condition for future compatibility.
737    #[serde(other)]
738    Unsupported,
739}
740
741impl RuleCondition {
742    /// Returns a condition that always matches.
743    pub fn all() -> Self {
744        Self::And(AndCondition { inner: Vec::new() })
745    }
746
747    /// Returns a condition that never matches.
748    pub fn never() -> Self {
749        Self::Or(OrCondition { inner: Vec::new() })
750    }
751
752    /// Creates a condition that compares values for equality.
753    ///
754    /// This operator supports:
755    ///  - boolean
756    ///  - strings
757    ///  - UUIDs
758    ///
759    /// # Examples
760    ///
761    /// ```
762    /// use relay_protocol::RuleCondition;
763    ///
764    /// // Matches if the value is identical to the given string:
765    /// let condition = RuleCondition::eq("obj.status", "invalid");
766    ///
767    /// // Matches if the value is identical to any of the given strings:
768    /// let condition = RuleCondition::eq("obj.status", &["invalid", "unknown"][..]);
769    ///
770    /// // Matches a boolean flag:
771    /// let condition = RuleCondition::eq("obj.valid", false);
772    /// ```
773    pub fn eq(field: impl Into<String>, value: impl Into<Value>) -> Self {
774        Self::Eq(EqCondition::new(field, value))
775    }
776
777    /// Creates a condition that compares values for equality ignoring ASCII-case.
778    ///
779    /// # Examples
780    ///
781    /// ```
782    /// use relay_protocol::RuleCondition;
783    ///
784    /// // Matches if the value is identical to the given string:
785    /// let condition = RuleCondition::eq_ignore_case("obj.status", "invalid");
786    ///
787    /// // Matches if the value is identical to any of the given strings:
788    /// let condition = RuleCondition::eq_ignore_case("obj.status", &["invalid", "unknown"][..]);
789    /// ```
790    pub fn eq_ignore_case(field: impl Into<String>, value: impl Into<Value>) -> Self {
791        Self::Eq(EqCondition::new(field, value).ignore_case())
792    }
793
794    /// Creates a condition that matches one or more glob patterns.
795    ///
796    /// # Example
797    ///
798    /// ```
799    /// use relay_protocol::RuleCondition;
800    ///
801    /// // Match a single pattern:
802    /// let condition = RuleCondition::glob("obj.name", "error: *");
803    ///
804    /// // Match any of a list of patterns:
805    /// let condition = RuleCondition::glob("obj.name", &["error: *", "*failure*"][..]);
806    /// ```
807    pub fn glob(field: impl Into<String>, value: impl IntoStrings) -> Self {
808        Self::Glob(GlobCondition::new(field, value))
809    }
810
811    /// Creates a condition that matches IP addresses inside one or more networks.
812    ///
813    /// Each entry is a single address or a CIDR range. Entries that do not parse are ignored.
814    ///
815    /// # Example
816    ///
817    /// ```
818    /// use relay_protocol::RuleCondition;
819    ///
820    /// let condition = RuleCondition::cidr("obj.ip", &["10.0.0.0/8", "192.168.1.1"][..]);
821    /// ```
822    pub fn cidr(field: impl Into<String>, value: impl IntoStrings) -> Self {
823        Self::Cidr(CidrCondition::new(field, value))
824    }
825
826    /// Creates a condition that compares the version of a release.
827    ///
828    /// # Example
829    ///
830    /// ```
831    /// use relay_protocol::RuleCondition;
832    /// use relay_protocol::condition::ReleaseComparator;
833    ///
834    /// // Match a range of versions:
835    /// let condition = RuleCondition::release("obj.release", ReleaseComparator::Gte, "1.2.0")
836    ///     & RuleCondition::release("obj.release", ReleaseComparator::Lt, "2.0.0");
837    /// ```
838    pub fn release(field: impl Into<String>, comparator: ReleaseComparator, value: &str) -> Self {
839        Self::Release(ReleaseCondition::new(field, comparator, value))
840    }
841
842    /// Creates a condition that applies `>`.
843    ///
844    /// # Example
845    ///
846    /// ```
847    /// use relay_protocol::RuleCondition;
848    ///
849    /// let condition = RuleCondition::gt("obj.length", 10);
850    /// ```
851    pub fn gt(field: impl Into<String>, value: impl Into<Value>) -> Self {
852        Self::Gt(GtCondition::new(field, value))
853    }
854
855    /// Creates a condition that applies `>=`.
856    ///
857    /// # Example
858    ///
859    /// ```
860    /// use relay_protocol::RuleCondition;
861    ///
862    /// let condition = RuleCondition::gte("obj.length", 10);
863    /// ```
864    pub fn gte(field: impl Into<String>, value: impl Into<Value>) -> Self {
865        Self::Gte(GteCondition::new(field, value))
866    }
867
868    /// Creates a condition that applies `<`.
869    ///
870    /// # Example
871    ///
872    /// ```
873    /// use relay_protocol::RuleCondition;
874    ///
875    /// let condition = RuleCondition::lt("obj.length", 10);
876    /// ```
877    pub fn lt(field: impl Into<String>, value: impl Into<Value>) -> Self {
878        Self::Lt(LtCondition::new(field, value))
879    }
880
881    /// Creates a condition that applies `<=`.
882    ///
883    /// # Example
884    ///
885    /// ```
886    /// use relay_protocol::RuleCondition;
887    ///
888    /// let condition = RuleCondition::lte("obj.length", 10);
889    /// ```
890    pub fn lte(field: impl Into<String>, value: impl Into<Value>) -> Self {
891        Self::Lte(LteCondition::new(field, value))
892    }
893
894    /// Combines this condition and another condition with a logical AND operator.
895    ///
896    /// The short-hand operator for this combinator is `&`.
897    ///
898    /// # Example
899    ///
900    /// ```
901    /// use relay_protocol::RuleCondition;
902    ///
903    /// let condition = RuleCondition::eq("obj.status", "invalid")
904    ///     & RuleCondition::gte("obj.length", 10);
905    /// ```
906    pub fn and(mut self, other: RuleCondition) -> Self {
907        if let Self::And(ref mut condition) = self {
908            condition.inner.push(other);
909            self
910        } else {
911            Self::And(AndCondition {
912                inner: vec![self, other],
913            })
914        }
915    }
916
917    /// Combines this condition and another condition with a logical OR operator.
918    ///
919    /// The short-hand operator for this combinator is `|`.
920    ///
921    /// # Example
922    ///
923    /// ```
924    /// use relay_protocol::RuleCondition;
925    ///
926    /// let condition = RuleCondition::eq("obj.status", "invalid")
927    ///     | RuleCondition::eq("obj.status", "unknown");
928    /// ```
929    pub fn or(mut self, other: RuleCondition) -> Self {
930        if let Self::Or(ref mut condition) = self {
931            condition.inner.push(other);
932            self
933        } else {
934            Self::Or(OrCondition {
935                inner: vec![self, other],
936            })
937        }
938    }
939
940    /// Negates this condition with logical NOT.
941    ///
942    /// The short-hand operator for this combinator is `!`.
943    ///
944    /// # Example
945    ///
946    /// ```
947    /// use relay_protocol::RuleCondition;
948    ///
949    /// let condition = !RuleCondition::eq("obj.status", "invalid");
950    /// ```
951    pub fn negate(self) -> Self {
952        match self {
953            Self::Not(condition) => *condition.inner,
954            other => Self::Not(NotCondition {
955                inner: Box::new(other),
956            }),
957        }
958    }
959
960    /// Creates an [`AnyCondition`].
961    ///
962    /// # Example
963    ///
964    /// ```
965    /// use relay_protocol::RuleCondition;
966    ///
967    /// let condition = RuleCondition::for_any("obj.exceptions",
968    ///     RuleCondition::eq("name", "NullPointerException")
969    /// );
970    /// ```
971    pub fn for_any(field: impl Into<String>, inner: RuleCondition) -> Self {
972        Self::Any(AnyCondition::new(field, inner))
973    }
974
975    /// Creates an [`AllCondition`].
976    ///
977    /// # Example
978    ///
979    /// ```
980    /// use relay_protocol::RuleCondition;
981    ///
982    /// let condition = RuleCondition::for_all("obj.exceptions",
983    ///     RuleCondition::eq("name", "NullPointerException")
984    /// );
985    /// ```
986    pub fn for_all(field: impl Into<String>, inner: RuleCondition) -> Self {
987        Self::All(AllCondition::new(field, inner))
988    }
989
990    /// Checks if Relay supports this condition (in other words if the condition had any unknown configuration
991    /// which was serialized as "Unsupported" (because the configuration is either faulty or was created for a
992    /// newer relay that supports some other condition types)
993    pub fn supported(&self) -> bool {
994        match self {
995            RuleCondition::Unsupported => false,
996            // we have a known condition
997            RuleCondition::Gte(_)
998            | RuleCondition::Lte(_)
999            | RuleCondition::Gt(_)
1000            | RuleCondition::Lt(_)
1001            | RuleCondition::Eq(_)
1002            | RuleCondition::Glob(_)
1003            | RuleCondition::Cidr(_) => true,
1004            RuleCondition::Release(condition) => condition.supported(),
1005            // dig down for embedded conditions
1006            RuleCondition::And(rules) => rules.supported(),
1007            RuleCondition::Or(rules) => rules.supported(),
1008            RuleCondition::Not(rule) => rule.supported(),
1009            RuleCondition::Any(rule) => rule.supported(),
1010            RuleCondition::All(rule) => rule.supported(),
1011        }
1012    }
1013
1014    /// Returns `true` if the rule matches the given value instance.
1015    pub fn matches<T>(&self, value: &T) -> bool
1016    where
1017        T: Getter + ?Sized,
1018    {
1019        match self {
1020            RuleCondition::Eq(condition) => condition.matches(value),
1021            RuleCondition::Lte(condition) => condition.matches(value),
1022            RuleCondition::Gte(condition) => condition.matches(value),
1023            RuleCondition::Gt(condition) => condition.matches(value),
1024            RuleCondition::Lt(condition) => condition.matches(value),
1025            RuleCondition::Glob(condition) => condition.matches(value),
1026            RuleCondition::Cidr(condition) => condition.matches(value),
1027            RuleCondition::Release(condition) => condition.matches(value),
1028            RuleCondition::And(conditions) => conditions.matches(value),
1029            RuleCondition::Or(conditions) => conditions.matches(value),
1030            RuleCondition::Not(condition) => condition.matches(value),
1031            RuleCondition::Any(condition) => condition.matches(value),
1032            RuleCondition::All(condition) => condition.matches(value),
1033            RuleCondition::Unsupported => false,
1034        }
1035    }
1036}
1037
1038impl std::ops::BitAnd for RuleCondition {
1039    type Output = Self;
1040
1041    fn bitand(self, rhs: Self) -> Self::Output {
1042        self.and(rhs)
1043    }
1044}
1045
1046impl std::ops::BitOr for RuleCondition {
1047    type Output = Self;
1048
1049    fn bitor(self, rhs: Self) -> Self::Output {
1050        self.or(rhs)
1051    }
1052}
1053
1054impl std::ops::Not for RuleCondition {
1055    type Output = Self;
1056
1057    fn not(self) -> Self::Output {
1058        self.negate()
1059    }
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064    use uuid::Uuid;
1065
1066    use super::*;
1067    use crate::{GetterIter, HexId};
1068
1069    #[derive(Debug)]
1070    struct Exception {
1071        name: String,
1072    }
1073
1074    impl Getter for Exception {
1075        fn get_value(&self, path: &str) -> Option<Val<'_>> {
1076            Some(match path {
1077                "name" => self.name.as_str().into(),
1078                _ => return None,
1079            })
1080        }
1081    }
1082
1083    struct Trace {
1084        trace_id: Uuid,
1085        span_id: [u8; 4],
1086        transaction: String,
1087        release: String,
1088        environment: String,
1089        user_segment: String,
1090        user_ip: String,
1091        client_ip: IpAddr,
1092        exceptions: Vec<Exception>,
1093    }
1094
1095    impl Getter for Trace {
1096        fn get_value(&self, path: &str) -> Option<Val<'_>> {
1097            Some(match path.strip_prefix("trace.")? {
1098                "trace_id" => (&self.trace_id).into(),
1099                "span_id" => Val::HexId(HexId(&self.span_id[..])),
1100                "transaction" => self.transaction.as_str().into(),
1101                "release" => self.release.as_str().into(),
1102                "environment" => self.environment.as_str().into(),
1103                "user.segment" => self.user_segment.as_str().into(),
1104                "user.ip" => self.user_ip.as_str().into(),
1105                "client_ip" => self.client_ip.into(),
1106                _ => {
1107                    return None;
1108                }
1109            })
1110        }
1111
1112        fn get_iter(&self, path: &str) -> Option<GetterIter<'_>> {
1113            Some(match path.strip_prefix("trace.")? {
1114                "exceptions" => GetterIter::new(self.exceptions.iter()),
1115                _ => return None,
1116            })
1117        }
1118    }
1119
1120    fn mock_trace() -> Trace {
1121        Trace {
1122            trace_id: "6b7d15b8-cee2-4354-9fee-dae7ef43e434".parse().unwrap(),
1123            span_id: [0xde, 0xad, 0xbe, 0xef],
1124            transaction: "transaction1".to_owned(),
1125            release: "1.1.1".to_owned(),
1126            environment: "debug".to_owned(),
1127            user_segment: "vip".to_owned(),
1128            user_ip: "10.1.2.3".to_owned(),
1129            client_ip: "2001:db8::1".parse().unwrap(),
1130            exceptions: vec![
1131                Exception {
1132                    name: "NullPointerException".to_owned(),
1133                },
1134                Exception {
1135                    name: "NullUser".to_owned(),
1136                },
1137            ],
1138        }
1139    }
1140
1141    #[test]
1142    fn deserialize() {
1143        let serialized_rules = r#"[
1144            {
1145                "op":"eq",
1146                "name": "field_1",
1147                "value": ["UPPER","lower"],
1148                "options":{
1149                    "ignoreCase": true
1150                }
1151            },
1152            {
1153                "op":"eq",
1154                "name": "field_2",
1155                "value": ["UPPER","lower"]
1156            },
1157            {
1158                "op":"glob",
1159                "name": "field_3",
1160                "value": ["1.2.*","2.*"]
1161            },
1162            {
1163                "op":"cidr",
1164                "name": "field_ip",
1165                "value": ["192.168.1.1","10.0.0.0/8","192.168.1.1/32","not-an-ip"]
1166            },
1167            {
1168                "op":"release",
1169                "name": "field_release",
1170                "comparator": "gte",
1171                "value": "1.2.0"
1172            },
1173            {
1174                "op":"not",
1175                "inner": {
1176                    "op":"glob",
1177                    "name": "field_4",
1178                    "value": ["1.*"]
1179                }
1180            },
1181            {
1182                "op":"and",
1183                "inner": [{
1184                    "op":"glob",
1185                    "name": "field_5",
1186                    "value": ["2.*"]
1187                }]
1188            },
1189            {
1190                "op":"or",
1191                "inner": [{
1192                    "op":"glob",
1193                    "name": "field_6",
1194                    "value": ["3.*"]
1195                }]
1196            },
1197            {
1198                "op": "any",
1199                "name": "obj.exceptions",
1200                "inner": {
1201                    "op": "glob",
1202                    "name": "value",
1203                    "value": ["*Exception"]
1204                }
1205            },
1206            {
1207                "op": "all",
1208                "name": "obj.exceptions",
1209                "inner": {
1210                    "op": "glob",
1211                    "name": "value",
1212                    "value": ["*Exception"]
1213                }
1214            }
1215        ]"#;
1216
1217        let rules: Result<Vec<RuleCondition>, _> = serde_json::from_str(serialized_rules);
1218        assert!(rules.is_ok());
1219        let rules = rules.unwrap();
1220        insta::assert_ron_snapshot!(rules, @r#"
1221        [
1222          EqCondition(
1223            op: "eq",
1224            name: "field_1",
1225            value: [
1226              "UPPER",
1227              "lower",
1228            ],
1229            options: EqCondOptions(
1230              ignoreCase: true,
1231            ),
1232          ),
1233          EqCondition(
1234            op: "eq",
1235            name: "field_2",
1236            value: [
1237              "UPPER",
1238              "lower",
1239            ],
1240          ),
1241          GlobCondition(
1242            op: "glob",
1243            name: "field_3",
1244            value: [
1245              "1.2.*",
1246              "2.*",
1247            ],
1248          ),
1249          CidrCondition(
1250            op: "cidr",
1251            name: "field_ip",
1252            value: [
1253              "10.0.0.0/8",
1254              "192.168.1.1/32",
1255            ],
1256          ),
1257          ReleaseCondition(
1258            op: "release",
1259            name: "field_release",
1260            comparator: gte,
1261            value: Some("1.2.0"),
1262          ),
1263          NotCondition(
1264            op: "not",
1265            inner: GlobCondition(
1266              op: "glob",
1267              name: "field_4",
1268              value: [
1269                "1.*",
1270              ],
1271            ),
1272          ),
1273          AndCondition(
1274            op: "and",
1275            inner: [
1276              GlobCondition(
1277                op: "glob",
1278                name: "field_5",
1279                value: [
1280                  "2.*",
1281                ],
1282              ),
1283            ],
1284          ),
1285          OrCondition(
1286            op: "or",
1287            inner: [
1288              GlobCondition(
1289                op: "glob",
1290                name: "field_6",
1291                value: [
1292                  "3.*",
1293                ],
1294              ),
1295            ],
1296          ),
1297          AnyCondition(
1298            op: "any",
1299            name: "obj.exceptions",
1300            inner: GlobCondition(
1301              op: "glob",
1302              name: "value",
1303              value: [
1304                "*exception",
1305              ],
1306            ),
1307          ),
1308          AllCondition(
1309            op: "all",
1310            name: "obj.exceptions",
1311            inner: GlobCondition(
1312              op: "glob",
1313              name: "value",
1314              value: [
1315                "*exception",
1316              ],
1317            ),
1318          ),
1319        ]
1320        "#);
1321    }
1322
1323    #[test]
1324    fn unsupported_rule_deserialize() {
1325        let bad_json = r#"{
1326            "op": "BadOperator",
1327            "name": "foo",
1328            "value": "bar"
1329        }"#;
1330
1331        let rule: RuleCondition = serde_json::from_str(bad_json).unwrap();
1332        assert!(matches!(rule, RuleCondition::Unsupported));
1333    }
1334
1335    #[test]
1336    /// test matching for various rules
1337    fn test_matches() {
1338        let conditions = [
1339            (
1340                "simple",
1341                RuleCondition::glob("trace.release", "1.1.1")
1342                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1343                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip")
1344                    & RuleCondition::eq_ignore_case("trace.transaction", "transaction1"),
1345            ),
1346            (
1347                "glob releases",
1348                RuleCondition::glob("trace.release", "1.*")
1349                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1350                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1351            ),
1352            (
1353                "glob transaction",
1354                RuleCondition::glob("trace.transaction", "trans*"),
1355            ),
1356            (
1357                "multiple releases",
1358                RuleCondition::glob("trace.release", vec!["2.1.1", "1.1.*"])
1359                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1360                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1361            ),
1362            (
1363                "multiple user segments",
1364                RuleCondition::glob("trace.release", "1.1.1")
1365                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1366                    & RuleCondition::eq_ignore_case(
1367                        "trace.user.segment",
1368                        vec!["paid", "vip", "free"],
1369                    ),
1370            ),
1371            (
1372                "multiple transactions",
1373                RuleCondition::glob("trace.transaction", &["t22", "trans*", "t33"][..]),
1374            ),
1375            (
1376                "case insensitive user segments",
1377                RuleCondition::glob("trace.release", "1.1.1")
1378                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1379                    & RuleCondition::eq_ignore_case("trace.user.segment", &["ViP", "FrEe"][..]),
1380            ),
1381            (
1382                "multiple user environments",
1383                RuleCondition::glob("trace.release", "1.1.1")
1384                    & RuleCondition::eq_ignore_case(
1385                        "trace.environment",
1386                        &["integration", "debug", "production"][..],
1387                    )
1388                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1389            ),
1390            (
1391                "case insensitive environments",
1392                RuleCondition::glob("trace.release", "1.1.1")
1393                    & RuleCondition::eq_ignore_case("trace.environment", &["DeBuG", "PrOd"][..])
1394                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1395            ),
1396            (
1397                "all environments",
1398                RuleCondition::glob("trace.release", "1.1.1")
1399                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1400            ),
1401            (
1402                "undefined environments",
1403                RuleCondition::glob("trace.release", "1.1.1")
1404                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1405            ),
1406            (
1407                "trace/span ID bytes",
1408                RuleCondition::eq("trace.trace_id", "6b7d15b8cee243549feedae7ef43e434")
1409                    & RuleCondition::eq("trace.span_id", "DEADBEEF"),
1410            ),
1411            ("match no conditions", RuleCondition::all()),
1412            ("string cmp", RuleCondition::gt("trace.transaction", "t")),
1413        ];
1414
1415        let trace = mock_trace();
1416
1417        for (rule_test_name, condition) in conditions.iter() {
1418            let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1419            assert!(condition.matches(&trace), "{failure_name}");
1420        }
1421    }
1422
1423    #[test]
1424    fn test_cidr_condition() {
1425        let trace = mock_trace();
1426
1427        assert!(RuleCondition::cidr("trace.user.ip", "10.0.0.0/8").matches(&trace));
1428        assert!(RuleCondition::cidr("trace.user.ip", "10.1.2.3").matches(&trace));
1429        assert!(
1430            RuleCondition::cidr("trace.user.ip", &["192.168.0.0/16", "10.1.0.0/16"][..])
1431                .matches(&trace)
1432        );
1433        assert!(!RuleCondition::cidr("trace.user.ip", "192.168.0.0/16").matches(&trace));
1434        assert!(!RuleCondition::cidr("trace.user.ip", "2001:db8::/32").matches(&trace));
1435        assert!(!RuleCondition::cidr("trace.user.ip", Vec::<String>::new()).matches(&trace));
1436        assert!(!RuleCondition::cidr("trace.missing", "10.0.0.0/8").matches(&trace));
1437        assert!(!RuleCondition::cidr("trace.release", "10.0.0.0/8").matches(&trace));
1438
1439        assert!(RuleCondition::cidr("trace.client_ip", "2001:db8::/32").matches(&trace));
1440        assert!(RuleCondition::cidr("trace.client_ip", "2001:db8::1").matches(&trace));
1441        assert!(!RuleCondition::cidr("trace.client_ip", "10.0.0.0/8").matches(&trace));
1442    }
1443
1444    #[test]
1445    fn test_release_condition() {
1446        use ReleaseComparator::{Eq, Gt, Gte, Lt, Lte};
1447
1448        let mut trace = mock_trace();
1449        trace.release = "myapp@1.10.0+build".to_owned();
1450
1451        let cases = [
1452            (Eq, "1.9.0", false),
1453            (Eq, "1.10.0", true),
1454            (Eq, "1.11.0", false),
1455            (Gt, "1.9.0", true),
1456            (Gt, "1.10.0", false),
1457            (Gt, "1.11.0", false),
1458            (Gte, "1.9.0", true),
1459            (Gte, "1.10.0", true),
1460            (Gte, "1.11.0", false),
1461            (Lt, "1.9.0", false),
1462            (Lt, "1.10.0", false),
1463            (Lt, "1.11.0", true),
1464            (Lte, "1.9.0", false),
1465            (Lte, "1.10.0", true),
1466            (Lte, "1.11.0", true),
1467        ];
1468
1469        for (comparator, value, expected) in cases {
1470            let condition = RuleCondition::release("trace.release", comparator, value);
1471            assert!(condition.supported());
1472            assert_eq!(
1473                condition.matches(&trace),
1474                expected,
1475                "{comparator:?} {value}"
1476            );
1477        }
1478    }
1479
1480    #[test]
1481    fn test_release_condition_without_version() {
1482        let gte = |field, value| RuleCondition::release(field, ReleaseComparator::Gte, value);
1483        let mut trace = mock_trace();
1484
1485        assert!(!gte("trace.release", "a4b7e0f9c2d1").matches(&trace));
1486        assert!(!gte("trace.release", "a4b7e0f9c2d1").supported());
1487        assert!(!gte("trace.release", "myapp@1.0.0").matches(&trace));
1488        assert!(!gte("trace.missing", "1.0.0").matches(&trace));
1489        assert!(!gte("trace.client_ip", "1.0.0").matches(&trace));
1490
1491        trace.release = "a4b7e0f9c2d1".to_owned();
1492        assert!(!gte("trace.release", "0.0.1").matches(&trace));
1493    }
1494
1495    #[test]
1496    fn test_release_condition_unknown_comparator() {
1497        let json = r#"{"op":"release","name":"trace.release","comparator":"ne","value":"9.9.9"}"#;
1498        let condition: RuleCondition = serde_json::from_str(json).unwrap();
1499
1500        assert!(!condition.supported());
1501        assert!(!condition.matches(&mock_trace()));
1502    }
1503
1504    #[test]
1505    fn test_eq_condition_ip_addr() {
1506        let trace = mock_trace();
1507
1508        assert!(RuleCondition::eq("trace.client_ip", "2001:db8::1").matches(&trace));
1509        assert!(RuleCondition::eq("trace.client_ip", "2001:db8:0::1").matches(&trace));
1510        assert!(!RuleCondition::eq("trace.client_ip", "2001:db8::2").matches(&trace));
1511        assert!(!RuleCondition::eq("trace.client_ip", "not-an-ip").matches(&trace));
1512    }
1513
1514    #[test]
1515    fn test_cidr_condition_skips_invalid_entries() {
1516        let condition: RuleCondition = serde_json::from_str(
1517            r#"{"op": "cidr", "name": "trace.user.ip", "value": ["garbage", "10.0.0.0/8"]}"#,
1518        )
1519        .unwrap();
1520
1521        assert!(condition.matches(&mock_trace()));
1522        assert_eq!(
1523            serde_json::to_string(&condition).unwrap(),
1524            r#"{"op":"cidr","name":"trace.user.ip","value":["10.0.0.0/8"]}"#
1525        );
1526    }
1527
1528    #[test]
1529    fn test_or_combinator() {
1530        let conditions = [
1531            (
1532                "both",
1533                true,
1534                RuleCondition::eq_ignore_case("trace.environment", "debug")
1535                    | RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1536            ),
1537            (
1538                "first",
1539                true,
1540                RuleCondition::eq_ignore_case("trace.environment", "debug")
1541                    | RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1542            ),
1543            (
1544                "second",
1545                true,
1546                RuleCondition::eq_ignore_case("trace.environment", "prod")
1547                    | RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1548            ),
1549            (
1550                "none",
1551                false,
1552                RuleCondition::eq_ignore_case("trace.environment", "prod")
1553                    | RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1554            ),
1555            (
1556                "empty",
1557                false,
1558                RuleCondition::Or(OrCondition { inner: vec![] }),
1559            ),
1560            ("never", false, RuleCondition::never()),
1561        ];
1562
1563        let trace = mock_trace();
1564
1565        for (rule_test_name, expected, condition) in conditions.iter() {
1566            let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1567            assert!(condition.matches(&trace) == *expected, "{failure_name}");
1568        }
1569    }
1570
1571    #[test]
1572    fn test_and_combinator() {
1573        let conditions = [
1574            (
1575                "both",
1576                true,
1577                RuleCondition::eq_ignore_case("trace.environment", "debug")
1578                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1579            ),
1580            (
1581                "first",
1582                false,
1583                RuleCondition::eq_ignore_case("trace.environment", "debug")
1584                    & RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1585            ),
1586            (
1587                "second",
1588                false,
1589                RuleCondition::eq_ignore_case("trace.environment", "prod")
1590                    & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1591            ),
1592            (
1593                "none",
1594                false,
1595                RuleCondition::eq_ignore_case("trace.environment", "prod")
1596                    & RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1597            ),
1598            (
1599                "empty",
1600                true,
1601                RuleCondition::And(AndCondition { inner: vec![] }),
1602            ),
1603            ("all", true, RuleCondition::all()),
1604        ];
1605
1606        let trace = mock_trace();
1607
1608        for (rule_test_name, expected, condition) in conditions.iter() {
1609            let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1610            assert!(condition.matches(&trace) == *expected, "{failure_name}");
1611        }
1612    }
1613
1614    #[test]
1615    fn test_not_combinator() {
1616        let conditions = [
1617            (
1618                "not true",
1619                false,
1620                !RuleCondition::eq_ignore_case("trace.environment", "debug"),
1621            ),
1622            (
1623                "not false",
1624                true,
1625                !RuleCondition::eq_ignore_case("trace.environment", "prod"),
1626            ),
1627        ];
1628
1629        let trace = mock_trace();
1630
1631        for (rule_test_name, expected, condition) in conditions.iter() {
1632            let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1633            assert!(condition.matches(&trace) == *expected, "{failure_name}");
1634        }
1635    }
1636
1637    #[test]
1638    /// test various rules that do not match
1639    fn test_does_not_match() {
1640        let conditions = [
1641            (
1642                "release",
1643                RuleCondition::glob("trace.release", "1.1.2")
1644                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1645                    & RuleCondition::eq_ignore_case("trace.user", "vip"),
1646            ),
1647            (
1648                "user segment",
1649                RuleCondition::glob("trace.release", "1.1.1")
1650                    & RuleCondition::eq_ignore_case("trace.environment", "debug")
1651                    & RuleCondition::eq_ignore_case("trace.user", "all"),
1652            ),
1653            (
1654                "environment",
1655                RuleCondition::glob("trace.release", "1.1.1")
1656                    & RuleCondition::eq_ignore_case("trace.environment", "prod")
1657                    & RuleCondition::eq_ignore_case("trace.user", "vip"),
1658            ),
1659            (
1660                "transaction",
1661                RuleCondition::glob("trace.release", "1.1.1")
1662                    & RuleCondition::glob("trace.transaction", "t22")
1663                    & RuleCondition::eq_ignore_case("trace.user", "vip"),
1664            ),
1665            ("span ID", RuleCondition::eq("trace.span_id", "deadbeer")),
1666        ];
1667
1668        let trace = mock_trace();
1669
1670        for (rule_test_name, condition) in conditions.iter() {
1671            let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1672            assert!(!condition.matches(&trace), "{failure_name}");
1673        }
1674    }
1675
1676    #[test]
1677    fn test_any_condition_with_match() {
1678        let condition = RuleCondition::for_any(
1679            "trace.exceptions",
1680            RuleCondition::glob("name", "*Exception"),
1681        );
1682
1683        let trace = mock_trace();
1684
1685        assert!(condition.matches(&trace));
1686    }
1687
1688    #[test]
1689    fn test_any_condition_with_no_match() {
1690        let condition =
1691            RuleCondition::for_any("trace.exceptions", RuleCondition::glob("name", "Error"));
1692
1693        let trace = mock_trace();
1694
1695        assert!(!condition.matches(&trace));
1696    }
1697
1698    #[test]
1699    fn test_all_condition() {
1700        let condition =
1701            RuleCondition::for_all("trace.exceptions", RuleCondition::glob("name", "Null*"));
1702
1703        let trace = mock_trace();
1704
1705        assert!(condition.matches(&trace));
1706    }
1707
1708    #[test]
1709    fn test_all_condition_with_no_match() {
1710        let condition = RuleCondition::for_all(
1711            "trace.exceptions",
1712            RuleCondition::glob("name", "*Exception"),
1713        );
1714
1715        let trace = mock_trace();
1716
1717        assert!(!condition.matches(&trace));
1718    }
1719}