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