1use 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#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
18#[serde(rename_all = "camelCase")]
19pub struct EqCondOptions {
20 #[serde(default)]
24 pub ignore_case: bool,
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct EqCondition {
36 pub name: String,
38
39 pub value: Value,
44
45 #[serde(default, skip_serializing_if = "is_default")]
47 pub options: EqCondOptions,
48}
49
50impl EqCondition {
51 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 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
100fn 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 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111 pub struct $struct_name {
112 pub name: String,
114 pub value: Value,
116 }
117
118 impl $struct_name {
119 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 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166pub struct GlobCondition {
167 pub name: String,
169 pub value: TypedPatterns<CaseInsensitive>,
173}
174
175impl GlobCondition {
176 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
195pub trait IntoStrings {
197 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#[derive(Debug, Clone, PartialEq, Default, Serialize)]
248#[serde(transparent)]
249pub struct IpNetworks(BTreeSet<IpNetwork>);
250
251impl IpNetworks {
252 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
282pub struct CidrCondition {
283 pub name: String,
285 pub value: IpNetworks,
289}
290
291impl CidrCondition {
292 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub enum ReleaseComparator {
323 Eq,
327 Gt,
329 Gte,
331 Lt,
333 Lte,
335 #[serde(other)]
337 Unsupported,
338}
339
340#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
350pub struct ReleaseCondition {
351 pub name: String,
353 pub comparator: ReleaseComparator,
355 #[serde(default, deserialize_with = "deserialize_release_lenient")]
357 pub value: Option<Release>,
358}
359
360impl ReleaseCondition {
361 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
410pub struct OrCondition {
411 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
435pub struct AndCondition {
436 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
458pub struct NotCondition {
459 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
481pub struct AnyCondition {
482 pub name: String,
484 pub inner: Box<RuleCondition>,
486}
487
488impl AnyCondition {
489 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
516pub struct AllCondition {
517 pub name: String,
519 pub inner: Box<RuleCondition>,
521}
522
523impl AllCondition {
524 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
578#[serde(rename_all = "camelCase", tag = "op")]
579pub enum RuleCondition {
580 Eq(EqCondition),
595
596 Gte(GteCondition),
606
607 Lte(LteCondition),
617
618 Gt(GtCondition),
628
629 Lt(LtCondition),
639
640 Glob(GlobCondition),
650
651 Cidr(CidrCondition),
661
662 Release(ReleaseCondition),
673
674 Or(OrCondition),
685
686 And(AndCondition),
697
698 Not(NotCondition),
708
709 Any(AnyCondition),
722
723 All(AllCondition),
735
736 #[serde(other)]
738 Unsupported,
739}
740
741impl RuleCondition {
742 pub fn all() -> Self {
744 Self::And(AndCondition { inner: Vec::new() })
745 }
746
747 pub fn never() -> Self {
749 Self::Or(OrCondition { inner: Vec::new() })
750 }
751
752 pub fn eq(field: impl Into<String>, value: impl Into<Value>) -> Self {
774 Self::Eq(EqCondition::new(field, value))
775 }
776
777 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 pub fn glob(field: impl Into<String>, value: impl IntoStrings) -> Self {
808 Self::Glob(GlobCondition::new(field, value))
809 }
810
811 pub fn cidr(field: impl Into<String>, value: impl IntoStrings) -> Self {
823 Self::Cidr(CidrCondition::new(field, value))
824 }
825
826 pub fn release(field: impl Into<String>, comparator: ReleaseComparator, value: &str) -> Self {
839 Self::Release(ReleaseCondition::new(field, comparator, value))
840 }
841
842 pub fn gt(field: impl Into<String>, value: impl Into<Value>) -> Self {
852 Self::Gt(GtCondition::new(field, value))
853 }
854
855 pub fn gte(field: impl Into<String>, value: impl Into<Value>) -> Self {
865 Self::Gte(GteCondition::new(field, value))
866 }
867
868 pub fn lt(field: impl Into<String>, value: impl Into<Value>) -> Self {
878 Self::Lt(LtCondition::new(field, value))
879 }
880
881 pub fn lte(field: impl Into<String>, value: impl Into<Value>) -> Self {
891 Self::Lte(LteCondition::new(field, value))
892 }
893
894 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 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 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 pub fn for_any(field: impl Into<String>, inner: RuleCondition) -> Self {
972 Self::Any(AnyCondition::new(field, inner))
973 }
974
975 pub fn for_all(field: impl Into<String>, inner: RuleCondition) -> Self {
987 Self::All(AllCondition::new(field, inner))
988 }
989
990 pub fn supported(&self) -> bool {
994 match self {
995 RuleCondition::Unsupported => false,
996 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 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 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 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 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}