1use crate::attribute_value::AttributeValue;
2use crate::contexts::attribute_reference::AttributeName;
3use crate::contexts::context::Kind;
4use crate::store::Store;
5use crate::variation::VariationOrRollout;
6use crate::{util, Context, EvaluationStack, Reference};
7use chrono::{self, Utc};
8use log::{error, warn};
9use regex::Regex;
10use serde::{Deserialize, Serialize};
11use serde_with::skip_serializing_none;
12use util::is_false;
13
14#[skip_serializing_none]
21#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
22#[serde(rename_all = "camelCase", from = "IntermediateClause")]
23pub struct Clause {
24 context_kind: Kind,
26 attribute: Reference,
30 #[serde(skip_serializing_if = "is_false")]
33 negate: bool,
34 op: Op,
36 values: Vec<AttributeValue>,
38}
39
40#[derive(Debug, Deserialize, PartialEq)]
41#[serde(rename_all = "camelCase")]
42struct ClauseWithKind {
43 context_kind: Kind,
44 attribute: Reference,
45 #[serde(default)]
46 negate: bool,
47 op: Op,
48 values: Vec<AttributeValue>,
49}
50
51#[derive(Debug, Deserialize, PartialEq)]
52#[serde(rename_all = "camelCase")]
53struct ClauseWithoutKind {
54 attribute: AttributeName,
55 #[serde(default)]
56 negate: bool,
57 op: Op,
58 values: Vec<AttributeValue>,
59}
60
61#[derive(Debug, Deserialize, PartialEq)]
62#[serde(untagged)]
63enum IntermediateClause {
64 ContextAware(ClauseWithKind),
67 ContextOblivious(ClauseWithoutKind),
68}
69
70impl From<IntermediateClause> for Clause {
71 fn from(ic: IntermediateClause) -> Self {
72 match ic {
73 IntermediateClause::ContextAware(fields) => Self {
74 context_kind: fields.context_kind,
75 attribute: fields.attribute,
76 negate: fields.negate,
77 op: fields.op,
78 values: fields.values,
79 },
80 IntermediateClause::ContextOblivious(fields) => Self {
81 context_kind: Kind::default(),
82 attribute: Reference::from(fields.attribute),
83 negate: fields.negate,
84 op: fields.op,
85 values: fields.values,
86 },
87 }
88 }
89}
90
91#[cfg(test)]
92pub(crate) mod proptest_generators {
93 use super::Clause;
94 use crate::contexts::attribute_reference::proptest_generators::*;
95 use crate::contexts::context::proptest_generators::*;
96 use crate::rule::Op;
97 use crate::AttributeValue;
98 use proptest::{collection::vec, prelude::*};
99
100 prop_compose! {
101 pub(crate) fn any_clause()(
104 kind in any_kind(),
105 reference in any_ref(),
108 negate in any::<bool>(),
109 values in vec(any::<bool>(), 0..5),
110 op in any::<Op>()
111 ) -> Clause {
112 Clause {
113 context_kind: kind,
114 attribute: reference,
115 negate,
116 op,
117 values: values.iter().map(|&b| AttributeValue::from(b)).collect()
118 }
119 }
120 }
121}
122
123#[derive(Clone, Debug, Serialize, Deserialize)]
128#[serde(rename_all = "camelCase")]
129pub struct FlagRule {
130 #[serde(default)]
134 pub id: String,
135 clauses: Vec<Clause>,
136
137 #[serde(flatten)]
139 pub variation_or_rollout: VariationOrRollout,
140
141 pub track_events: bool,
150}
151
152#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
153#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
154#[serde(rename_all = "camelCase")]
155enum Op {
156 In,
157 StartsWith,
158 EndsWith,
159 Contains,
160 Matches,
161 LessThan,
162 LessThanOrEqual,
163 GreaterThan,
164 GreaterThanOrEqual,
165 Before,
166 After,
167 SegmentMatch,
168 SemVerEqual,
169 SemVerGreaterThan,
170 SemVerLessThan,
171 #[serde(other)]
172 Unknown,
173}
174
175impl Clause {
176 pub(crate) fn matches(
177 &self,
178 context: &Context,
179 store: &dyn Store,
180 evaluation_stack: &mut EvaluationStack,
181 ) -> Result<bool, String> {
182 if let Op::SegmentMatch = self.op {
183 self.matches_segment(context, store, evaluation_stack)
184 } else {
185 self.matches_non_segment(context)
186 }
187 }
188
189 fn maybe_negate(&self, v: bool) -> bool {
190 if self.negate {
191 !v
192 } else {
193 v
194 }
195 }
196
197 pub(crate) fn matches_segment(
198 &self,
199 context: &Context,
200 store: &dyn Store,
201 evaluation_stack: &mut EvaluationStack,
202 ) -> Result<bool, String> {
203 for value in self.values.iter() {
204 if let Some(segment_key) = value.as_str() {
205 if let Some(segment) = store.segment(segment_key) {
206 let matches = segment.contains(context, store, evaluation_stack)?;
207 if matches {
208 return Ok(self.maybe_negate(true));
209 }
210 }
211 }
212 }
213
214 Ok(self.maybe_negate(false))
215 }
216
217 pub(crate) fn matches_non_segment(&self, context: &Context) -> Result<bool, String> {
218 if !self.attribute.is_valid() {
219 return Err(self.attribute.error());
220 }
221
222 if self.attribute.is_kind() {
223 for clause_value in &self.values {
224 for kind in context.kinds().iter() {
225 if self
226 .op
227 .matches(&AttributeValue::String(kind.to_string()), clause_value)
228 {
229 return Ok(self.maybe_negate(true));
230 }
231 }
232 }
233 return Ok(self.maybe_negate(false));
234 }
235
236 if let Some(actual_context) = context.as_kind(&self.context_kind) {
237 return match actual_context.get_value(&self.attribute) {
238 None | Some(AttributeValue::Null) => Ok(false),
239 Some(AttributeValue::Array(context_values)) => {
240 for clause_value in &self.values {
241 for context_value in context_values.iter() {
242 if self.op.matches(context_value, clause_value) {
243 return Ok(self.maybe_negate(true));
244 }
245 }
246 }
247
248 Ok(self.maybe_negate(false))
249 }
250 Some(context_value) => {
251 if self
252 .values
253 .iter()
254 .any(|clause_value| self.op.matches(&context_value, clause_value))
255 {
256 return Ok(self.maybe_negate(true));
257 }
258 Ok(self.maybe_negate(false))
259 }
260 };
261 }
262
263 Ok(false)
264 }
265
266 #[cfg(test)]
267 pub(crate) fn new_match(reference: Reference, value: AttributeValue, kind: Kind) -> Self {
269 Self {
270 attribute: reference,
271 negate: false,
272 op: Op::Matches,
273 values: vec![value],
274 context_kind: kind,
275 }
276 }
277
278 #[cfg(test)]
279 pub(crate) fn new_context_oblivious_match(reference: Reference, value: AttributeValue) -> Self {
281 Self {
282 attribute: reference,
283 negate: false,
284 op: Op::Matches,
285 values: vec![value],
286 context_kind: Kind::default(),
287 }
288 }
289}
290
291impl FlagRule {
292 pub(crate) fn matches(
296 &self,
297 context: &Context,
298 store: &dyn Store,
299 evaluation_stack: &mut EvaluationStack,
300 ) -> Result<bool, String> {
301 for clause in &self.clauses {
303 let result = clause.matches(context, store, evaluation_stack)?;
304 if !result {
305 return Ok(false);
306 }
307 }
308
309 Ok(true)
310 }
311
312 #[cfg(test)]
313 pub(crate) fn new_segment_match(segment_keys: Vec<&str>, kind: Kind) -> Self {
314 Self {
315 id: "rule".to_string(),
316 clauses: vec![Clause {
317 attribute: Reference::new("key"),
318 negate: false,
319 op: Op::SegmentMatch,
320 values: segment_keys
321 .iter()
322 .map(|key| AttributeValue::String(key.to_string()))
323 .collect(),
324 context_kind: kind,
325 }],
326 variation_or_rollout: VariationOrRollout::Variation { variation: 1 },
327 track_events: false,
328 }
329 }
330}
331
332impl Op {
333 fn matches(&self, lhs: &AttributeValue, rhs: &AttributeValue) -> bool {
334 match self {
335 Op::In => lhs == rhs,
336
337 Op::StartsWith => string_op(lhs, rhs, |l, r| l.starts_with(r)),
339 Op::EndsWith => string_op(lhs, rhs, |l, r| l.ends_with(r)),
340 Op::Contains => string_op(lhs, rhs, |l, r| l.contains(r)),
341 Op::Matches => string_op(lhs, rhs, |l, r| match Regex::new(r) {
342 Ok(re) => re.is_match(l),
343 Err(e) => {
344 warn!("Invalid regex for 'matches' operator ({e}): {l}");
345 false
346 }
347 }),
348
349 Op::LessThan => numeric_op(lhs, rhs, |l, r| l < r),
351 Op::LessThanOrEqual => numeric_op(lhs, rhs, |l, r| l <= r),
352 Op::GreaterThan => numeric_op(lhs, rhs, |l, r| l > r),
353 Op::GreaterThanOrEqual => numeric_op(lhs, rhs, |l, r| l >= r),
354
355 Op::Before => time_op(lhs, rhs, |l, r| l < r),
356 Op::After => time_op(lhs, rhs, |l, r| l > r),
357
358 Op::SegmentMatch => {
359 error!("segmentMatch operator should be special-cased, shouldn't get here");
360 false
361 }
362
363 Op::SemVerEqual => semver_op(lhs, rhs, |l, r| l == r),
364 Op::SemVerLessThan => semver_op(lhs, rhs, |l, r| l < r),
365 Op::SemVerGreaterThan => semver_op(lhs, rhs, |l, r| l > r),
366 Op::Unknown => false,
367 }
368 }
369}
370
371fn string_op<F: Fn(&str, &str) -> bool>(lhs: &AttributeValue, rhs: &AttributeValue, f: F) -> bool {
372 match (lhs.as_str(), rhs.as_str()) {
373 (Some(l), Some(r)) => f(l, r),
374 _ => false,
375 }
376}
377
378fn numeric_op<F: Fn(f64, f64) -> bool>(lhs: &AttributeValue, rhs: &AttributeValue, f: F) -> bool {
379 match (lhs.to_f64(), rhs.to_f64()) {
380 (Some(l), Some(r)) => f(l, r),
381 _ => false,
382 }
383}
384
385fn time_op<F: Fn(chrono::DateTime<Utc>, chrono::DateTime<Utc>) -> bool>(
386 lhs: &AttributeValue,
387 rhs: &AttributeValue,
388 f: F,
389) -> bool {
390 match (lhs.to_datetime(), rhs.to_datetime()) {
391 (Some(l), Some(r)) => f(l, r),
392 _ => false,
393 }
394}
395
396fn semver_op<F: Fn(semver::Version, semver::Version) -> bool>(
397 lhs: &AttributeValue,
398 rhs: &AttributeValue,
399 f: F,
400) -> bool {
401 match (lhs.as_semver(), rhs.as_semver()) {
402 (Some(l), Some(r)) => f(l, r),
403 _ => false,
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410 use crate::{flag::Flag, ContextBuilder, Segment};
411 use assert_json_diff::assert_json_eq;
412 use maplit::hashmap;
413 use proptest::prelude::*;
414 use serde_json::json;
415 use std::collections::HashMap;
416 use std::time::SystemTime;
417 struct TestStore;
418 use crate::proptest_generators::*;
419
420 impl Store for TestStore {
421 fn flag(&self, _flag_key: &str) -> Option<Flag> {
422 None
423 }
424 fn segment(&self, _segment_key: &str) -> Option<Segment> {
425 None
426 }
427 }
428
429 fn astring(s: &str) -> AttributeValue {
430 AttributeValue::String(s.into())
431 }
432 fn anum(f: f64) -> AttributeValue {
433 AttributeValue::Number(f)
434 }
435
436 #[test]
437 fn test_op_in() {
438 assert!(Op::In.matches(&astring("foo"), &astring("foo")));
440
441 assert!(!Op::In.matches(&astring("foo"), &astring("bar")));
442 assert!(
443 !Op::In.matches(&astring("Foo"), &astring("foo")),
444 "case sensitive"
445 );
446
447 assert!(Op::In.matches(&anum(42.0), &anum(42.0)));
449 assert!(!Op::In.matches(&anum(42.0), &anum(3.0)));
450 assert!(Op::In.matches(&anum(0.0), &anum(-0.0)));
451
452 assert!(Op::In.matches(&vec![0.0].into(), &vec![0.0].into()));
454 assert!(!Op::In.matches(&vec![0.0, 1.0].into(), &vec![0.0].into()));
455 assert!(!Op::In.matches(&vec![0.0].into(), &vec![0.0, 1.0].into()));
456 assert!(!Op::In.matches(&anum(0.0), &vec![0.0].into()));
457 assert!(!Op::In.matches(&vec![0.0].into(), &anum(0.0)));
458
459 assert!(Op::In.matches(&hashmap! {"x" => 0.0}.into(), &hashmap! {"x" => 0.0}.into()));
461 assert!(!Op::In.matches(
462 &hashmap! {"x" => 0.0, "y" => 1.0}.into(),
463 &hashmap! {"x" => 0.0}.into()
464 ));
465 assert!(!Op::In.matches(
466 &hashmap! {"x" => 0.0}.into(),
467 &hashmap! {"x" => 0.0, "y" => 1.0}.into()
468 ));
469 assert!(!Op::In.matches(&anum(0.0), &hashmap! {"x" => 0.0}.into()));
470 assert!(!Op::In.matches(&hashmap! {"x" => 0.0}.into(), &anum(0.0)));
471 }
472
473 #[test]
474 fn test_op_starts_with() {
475 assert!(Op::StartsWith.matches(&astring(""), &astring("")));
477 assert!(Op::StartsWith.matches(&astring("a"), &astring("")));
478 assert!(Op::StartsWith.matches(&astring("a"), &astring("a")));
479
480 assert!(Op::StartsWith.matches(&astring("food"), &astring("foo")));
482 assert!(!Op::StartsWith.matches(&astring("foo"), &astring("food")));
483
484 assert!(
485 !Op::StartsWith.matches(&astring("Food"), &astring("foo")),
486 "case sensitive"
487 );
488 }
489
490 #[test]
491 fn test_op_ends_with() {
492 assert!(Op::EndsWith.matches(&astring(""), &astring("")));
494 assert!(Op::EndsWith.matches(&astring("a"), &astring("")));
495 assert!(Op::EndsWith.matches(&astring("a"), &astring("a")));
496
497 assert!(Op::EndsWith.matches(&astring("food"), &astring("ood")));
499 assert!(!Op::EndsWith.matches(&astring("ood"), &astring("food")));
500
501 assert!(
502 !Op::EndsWith.matches(&astring("FOOD"), &astring("ood")),
503 "case sensitive"
504 );
505 }
506
507 #[test]
508 fn test_op_contains() {
509 assert!(Op::Contains.matches(&astring(""), &astring("")));
511 assert!(Op::Contains.matches(&astring("a"), &astring("")));
512 assert!(Op::Contains.matches(&astring("a"), &astring("a")));
513
514 assert!(Op::Contains.matches(&astring("food"), &astring("oo")));
516 assert!(!Op::Contains.matches(&astring("oo"), &astring("food")));
517
518 assert!(
519 !Op::Contains.matches(&astring("FOOD"), &astring("oo")),
520 "case sensitive"
521 );
522 }
523
524 #[test]
525 fn test_op_matches() {
526 fn should_match(text: &str, pattern: &str) {
527 assert!(
528 Op::Matches.matches(&astring(text), &astring(pattern)),
529 "`{text}` should match `{pattern}`"
530 );
531 }
532
533 fn should_not_match(text: &str, pattern: &str) {
534 assert!(
535 !Op::Matches.matches(&astring(text), &astring(pattern)),
536 "`{text}` should not match `{pattern}`"
537 );
538 }
539
540 should_match("", "");
541 should_match("a", "");
542 should_match("a", "a");
543 should_match("a", ".");
544 should_match("hello world", "hello.*rld");
545 should_match("hello world", "hello.*orl");
546 should_match("hello world", "l+");
547 should_match("hello world", "(world|planet)");
548
549 should_not_match("", ".");
550 should_not_match("", r"\");
551 should_not_match("hello world", "aloha");
552 should_not_match("hello world", "***bad regex");
553 }
554
555 #[test]
556 fn test_ops_numeric() {
557 assert!(Op::LessThan.matches(&anum(0.0), &anum(1.0)));
559 assert!(!Op::LessThan.matches(&anum(0.0), &anum(0.0)));
560 assert!(!Op::LessThan.matches(&anum(1.0), &anum(0.0)));
561
562 assert!(Op::GreaterThan.matches(&anum(1.0), &anum(0.0)));
563 assert!(!Op::GreaterThan.matches(&anum(0.0), &anum(0.0)));
564 assert!(!Op::GreaterThan.matches(&anum(0.0), &anum(1.0)));
565
566 assert!(Op::LessThanOrEqual.matches(&anum(0.0), &anum(1.0)));
567 assert!(Op::LessThanOrEqual.matches(&anum(0.0), &anum(0.0)));
568 assert!(!Op::LessThanOrEqual.matches(&anum(1.0), &anum(0.0)));
569
570 assert!(Op::GreaterThanOrEqual.matches(&anum(1.0), &anum(0.0)));
571 assert!(Op::GreaterThanOrEqual.matches(&anum(0.0), &anum(0.0)));
572 assert!(!Op::GreaterThanOrEqual.matches(&anum(0.0), &anum(1.0)));
573
574 assert!(!Op::LessThan.matches(&astring("0"), &anum(1.0)));
576 assert!(!Op::LessThan.matches(&anum(0.0), &astring("1")));
577 }
578
579 #[test]
580 fn test_ops_time() {
581 let today = SystemTime::now();
582 let today_millis = today
583 .duration_since(SystemTime::UNIX_EPOCH)
584 .unwrap()
585 .as_millis() as f64;
586 let yesterday_millis = today_millis - 86_400_000_f64;
587
588 assert!(Op::Before.matches(&anum(yesterday_millis), &anum(today_millis)));
590 assert!(!Op::Before.matches(&anum(today_millis), &anum(yesterday_millis)));
591 assert!(!Op::Before.matches(&anum(today_millis), &anum(today_millis)));
592
593 assert!(Op::After.matches(&anum(today_millis), &anum(yesterday_millis)));
594 assert!(!Op::After.matches(&anum(yesterday_millis), &anum(today_millis)));
595 assert!(!Op::After.matches(&anum(today_millis), &anum(today_millis)));
596
597 assert!(!Op::Before.matches(&astring(&yesterday_millis.to_string()), &anum(today_millis)));
599 assert!(!Op::After.matches(&anum(today_millis), &astring(&yesterday_millis.to_string())));
600
601 assert!(Op::Before.matches(
603 &astring("2019-11-19T17:29:00.000000-07:00"),
604 &anum(today_millis)
605 ));
606 assert!(
607 Op::Before.matches(&astring("2019-11-19T17:29:00-07:00"), &anum(today_millis)),
608 "fractional seconds part is optional"
609 );
610
611 assert!(Op::After.matches(
612 &anum(today_millis),
613 &astring("2019-11-19T17:29:00.000000-07:00")
614 ));
615
616 assert!(!Op::Before.matches(&astring("fish"), &anum(today_millis)));
618 assert!(!Op::After.matches(&anum(today_millis), &astring("fish")));
619 }
620
621 #[test]
622 fn test_semver_ops() {
623 assert!(Op::SemVerEqual.matches(&astring("2.0.0"), &astring("2.0.0")));
624
625 assert!(
626 Op::SemVerEqual.matches(&astring("2.0"), &astring("2.0.0")),
627 "we allow missing components (filled in with zeroes)"
628 );
629 assert!(
630 Op::SemVerEqual.matches(&astring("2"), &astring("2.0.0")),
631 "we allow missing components (filled in with zeroes)"
632 );
633
634 assert!(!Op::SemVerEqual.matches(&astring("2.0.0"), &astring("3.0.0")));
635 assert!(!Op::SemVerEqual.matches(&astring("2.0.0"), &astring("2.1.0")));
636 assert!(!Op::SemVerEqual.matches(&astring("2.0.0"), &astring("2.0.1")));
637
638 assert!(Op::SemVerGreaterThan.matches(&astring("3.0.0"), &astring("2.0.0")));
639 assert!(Op::SemVerGreaterThan.matches(&astring("2.1.0"), &astring("2.0.0")));
640 assert!(Op::SemVerGreaterThan.matches(&astring("2.0.1"), &astring("2.0.0")));
641 assert!(Op::SemVerGreaterThan
642 .matches(&astring("2.0.0-rc.10.green"), &astring("2.0.0-rc.2.green")));
643 assert!(
644 Op::SemVerGreaterThan.matches(&astring("2.0.0-rc.2.red"), &astring("2.0.0-rc.2.green")),
645 "red > green"
646 );
647 assert!(
648 Op::SemVerGreaterThan
649 .matches(&astring("2.0.0-rc.2.green.1"), &astring("2.0.0-rc.2.green")),
650 "adding more version components makes it greater"
651 );
652
653 assert!(!Op::SemVerGreaterThan.matches(&astring("2.0.0"), &astring("2.0.0")));
654 assert!(!Op::SemVerGreaterThan.matches(&astring("1.9.0"), &astring("2.0.0")));
655 assert!(
656 !Op::SemVerGreaterThan.matches(&astring("2.0.0-rc"), &astring("2.0.0")),
657 "prerelease version < released version"
658 );
659 assert!(
660 !Op::SemVerGreaterThan.matches(&astring("2.0.0+build"), &astring("2.0.0")),
661 "build metadata is ignored, these versions are equal"
662 );
663
664 assert!(!Op::SemVerEqual.matches(&astring("2.0.0"), &astring("200")));
665
666 assert!(!Op::SemVerEqual.matches(&astring("2.0.0"), &anum(2.0)));
668 }
669
670 #[test]
671 fn test_clause_matches() {
672 let one_val_clause = Clause {
673 attribute: Reference::new("a"),
674 negate: false,
675 op: Op::In,
676 values: vec!["foo".into()],
677 context_kind: Kind::default(),
678 };
679 let many_val_clause = Clause {
680 attribute: Reference::new("a"),
681 negate: false,
682 op: Op::In,
683 values: vec!["foo".into(), "bar".into()],
684 context_kind: Kind::default(),
685 };
686 let negated_clause = Clause {
687 attribute: Reference::new("a"),
688 negate: true,
689 op: Op::In,
690 values: vec!["foo".into()],
691 context_kind: Kind::default(),
692 };
693 let negated_many_val_clause = Clause {
694 attribute: Reference::new("a"),
695 negate: true,
696 op: Op::In,
697 values: vec!["foo".into(), "bar".into()],
698 context_kind: Kind::default(),
699 };
700 let key_clause = Clause {
701 attribute: Reference::new("key"),
702 negate: false,
703 op: Op::In,
704 values: vec!["matching".into()],
705 context_kind: Kind::default(),
706 };
707
708 let mut context_builder = ContextBuilder::new("without");
709 let context_without_attribute = context_builder.build().expect("Failed to build context");
710
711 context_builder
712 .key("matching")
713 .set_value("a", AttributeValue::String("foo".to_string()));
714 let matching_context = context_builder.build().expect("Failed to build context");
715
716 context_builder
717 .key("non-matching")
718 .set_value("a", AttributeValue::String("lol".to_string()));
719 let non_matching_context = context_builder.build().expect("Failed to build context");
720
721 let mut evaluation_stack = EvaluationStack::default();
722
723 assert!(one_val_clause
724 .matches(&matching_context, &TestStore {}, &mut evaluation_stack)
725 .unwrap());
726 assert!(!one_val_clause
727 .matches(&non_matching_context, &TestStore {}, &mut evaluation_stack)
728 .unwrap());
729 assert!(!one_val_clause
730 .matches(
731 &context_without_attribute,
732 &TestStore {},
733 &mut evaluation_stack
734 )
735 .unwrap());
736
737 assert!(!negated_clause
738 .matches(&matching_context, &TestStore {}, &mut evaluation_stack)
739 .unwrap());
740 assert!(negated_clause
741 .matches(&non_matching_context, &TestStore {}, &mut evaluation_stack)
742 .unwrap());
743
744 assert!(
745 !negated_clause
746 .matches(
747 &context_without_attribute,
748 &TestStore {},
749 &mut evaluation_stack
750 )
751 .unwrap(),
752 "targeting missing attribute does not match even when negated"
753 );
754
755 assert!(
756 many_val_clause
757 .matches(&matching_context, &TestStore {}, &mut evaluation_stack)
758 .unwrap(),
759 "requires only one of the values"
760 );
761 assert!(!many_val_clause
762 .matches(&non_matching_context, &TestStore {}, &mut evaluation_stack)
763 .unwrap());
764 assert!(!many_val_clause
765 .matches(
766 &context_without_attribute,
767 &TestStore {},
768 &mut evaluation_stack
769 )
770 .unwrap());
771
772 assert!(
773 !negated_many_val_clause
774 .matches(&matching_context, &TestStore {}, &mut evaluation_stack)
775 .unwrap(),
776 "requires all values are missing"
777 );
778 assert!(negated_many_val_clause
779 .matches(&non_matching_context, &TestStore {}, &mut evaluation_stack)
780 .unwrap());
781
782 assert!(
783 !negated_many_val_clause
784 .matches(
785 &context_without_attribute,
786 &TestStore {},
787 &mut evaluation_stack
788 )
789 .unwrap(),
790 "targeting missing attribute does not match even when negated"
791 );
792
793 assert!(
794 key_clause
795 .matches(&matching_context, &TestStore {}, &mut evaluation_stack)
796 .unwrap(),
797 "should match key"
798 );
799 assert!(
800 !key_clause
801 .matches(&non_matching_context, &TestStore {}, &mut evaluation_stack)
802 .unwrap(),
803 "should not match non-matching key"
804 );
805
806 context_builder.key("with-many").set_value(
807 "a",
808 AttributeValue::Array(vec![
809 AttributeValue::String("foo".to_string()),
810 AttributeValue::String("bar".to_string()),
811 AttributeValue::String("lol".to_string()),
812 ]),
813 );
814 let context_with_many = context_builder.build().expect("Failed to build context");
815
816 assert!(one_val_clause
817 .matches(&context_with_many, &TestStore {}, &mut evaluation_stack)
818 .unwrap());
819 assert!(many_val_clause
820 .matches(&context_with_many, &TestStore {}, &mut evaluation_stack)
821 .unwrap());
822
823 assert!(!negated_clause
824 .matches(&context_with_many, &TestStore {}, &mut evaluation_stack)
825 .unwrap());
826 assert!(!negated_many_val_clause
827 .matches(&context_with_many, &TestStore {}, &mut evaluation_stack)
828 .unwrap());
829 }
830
831 struct AttributeTestCase {
832 matching_context: Context,
833 non_matching_context: Context,
834 context_without_attribute: Option<Context>,
835 }
836
837 #[test]
838 fn test_clause_matches_attributes() {
839 let tests: HashMap<&str, AttributeTestCase> = hashmap! {
840 "key" => AttributeTestCase {
841 matching_context: ContextBuilder::new("match").build().unwrap(),
842 non_matching_context: ContextBuilder::new("nope").build().unwrap(),
843 context_without_attribute: None,
844 },
845 "name" => AttributeTestCase {
846 matching_context: ContextBuilder::new("matching").name("match").build().unwrap(),
847 non_matching_context: ContextBuilder::new("non-matching").name("nope").build().unwrap(),
848 context_without_attribute: Some(ContextBuilder::new("without-attribute").build().unwrap()),
849 },
850 };
851
852 let mut evaluation_stack = EvaluationStack::default();
853
854 for (attr, test_case) in tests {
855 let clause = Clause {
856 attribute: Reference::new(attr),
857 negate: false,
858 op: Op::In,
859 values: vec!["match".into()],
860 context_kind: Kind::default(),
861 };
862
863 assert!(
864 clause
865 .matches(
866 &test_case.matching_context,
867 &TestStore {},
868 &mut evaluation_stack
869 )
870 .unwrap(),
871 "should match {attr}"
872 );
873 assert!(
874 !clause
875 .matches(
876 &test_case.non_matching_context,
877 &TestStore {},
878 &mut evaluation_stack
879 )
880 .unwrap(),
881 "should not match non-matching {attr}"
882 );
883 if let Some(context_without_attribute) = test_case.context_without_attribute {
884 assert!(
885 !clause
886 .matches(
887 &context_without_attribute,
888 &TestStore {},
889 &mut evaluation_stack
890 )
891 .unwrap(),
892 "should not match user with null {attr}"
893 );
894 }
895 }
896 }
897
898 #[test]
899 fn test_clause_matches_anonymous_attribute() {
900 let clause = Clause {
901 attribute: Reference::new("anonymous"),
902 negate: false,
903 op: Op::In,
904 values: vec![true.into()],
905 context_kind: Kind::default(),
906 };
907
908 let anon_context = ContextBuilder::new("anon").anonymous(true).build().unwrap();
909 let non_anon_context = ContextBuilder::new("nonanon")
910 .anonymous(false)
911 .build()
912 .unwrap();
913 let implicitly_non_anon_context = ContextBuilder::new("implicit").build().unwrap();
914
915 let mut evaluation_stack = EvaluationStack::default();
916 assert!(clause
917 .matches(&anon_context, &TestStore {}, &mut evaluation_stack)
918 .unwrap());
919 assert!(!clause
920 .matches(&non_anon_context, &TestStore {}, &mut evaluation_stack)
921 .unwrap());
922 assert!(!clause
923 .matches(
924 &implicitly_non_anon_context,
925 &TestStore {},
926 &mut evaluation_stack
927 )
928 .unwrap());
929 }
930
931 #[test]
932 fn test_clause_matches_custom_attributes() {
933 for attr in &["custom", "custom1"] {
935 let clause = Clause {
936 attribute: Reference::new(attr),
937 negate: false,
938 op: Op::In,
939 values: vec!["match".into()],
940 context_kind: Kind::default(),
941 };
942
943 let matching_context = ContextBuilder::new("matching")
944 .set_value(attr, AttributeValue::String("match".into()))
945 .build()
946 .unwrap();
947 let non_matching_context = ContextBuilder::new("non-matching")
948 .set_value(attr, AttributeValue::String("nope".into()))
949 .build()
950 .unwrap();
951 let context_without_attribute = ContextBuilder::new("without_attribute")
952 .set_value(attr, AttributeValue::Null)
953 .build()
954 .unwrap();
955
956 let mut evaluation_stack = EvaluationStack::default();
957 assert!(
958 clause
959 .matches(&matching_context, &TestStore {}, &mut evaluation_stack)
960 .unwrap(),
961 "should match {attr}"
962 );
963 assert!(
964 !clause
965 .matches(&non_matching_context, &TestStore {}, &mut evaluation_stack)
966 .unwrap(),
967 "should not match non-matching {attr}"
968 );
969 assert!(
970 !clause
971 .matches(
972 &context_without_attribute,
973 &TestStore {},
974 &mut evaluation_stack
975 )
976 .unwrap(),
977 "should not match user with null {attr}"
978 );
979 }
980 }
981
982 #[test]
983 fn test_null_attribute() {
984 let context_null_attr = ContextBuilder::new("key")
985 .set_value("attr", AttributeValue::Null)
986 .build()
987 .unwrap();
988
989 let context_missing_attr = ContextBuilder::new("key").build().unwrap();
990
991 let clause_values = vec![
992 AttributeValue::Bool(true),
993 AttributeValue::Bool(false),
994 AttributeValue::Number(1.5),
995 AttributeValue::Number(1.0),
996 AttributeValue::Null,
997 AttributeValue::String("abc".to_string()),
998 AttributeValue::Array(vec![
999 AttributeValue::String("def".to_string()),
1000 AttributeValue::Null,
1001 ]),
1002 ];
1003
1004 for op in &[
1005 Op::In,
1006 Op::StartsWith,
1007 Op::EndsWith,
1008 Op::Contains,
1009 Op::Matches,
1010 Op::LessThan,
1011 Op::LessThanOrEqual,
1012 Op::GreaterThan,
1013 Op::GreaterThanOrEqual,
1014 Op::Before,
1015 Op::After,
1016 Op::SemVerEqual,
1017 Op::SemVerGreaterThan,
1018 Op::SemVerLessThan,
1019 ] {
1020 for neg in &[true, false] {
1021 let clause = Clause {
1022 attribute: Reference::new("attr"),
1023 negate: *neg,
1024 op: *op,
1025 values: clause_values.clone(),
1026 context_kind: Kind::default(),
1027 };
1028 let mut evaluation_stack = EvaluationStack::default();
1029 assert!(
1030 !clause
1031 .matches(&context_null_attr, &TestStore {}, &mut evaluation_stack)
1032 .unwrap(),
1033 "Null attribute matches operator {:?} when {}negated",
1034 clause.op,
1035 if *neg { "" } else { "not " },
1036 );
1037 assert!(
1038 !clause
1039 .matches(&context_missing_attr, &TestStore {}, &mut evaluation_stack)
1040 .unwrap(),
1041 "Missing attribute matches operator {:?} when {}negated",
1042 clause.op,
1043 if *neg { "" } else { "not " },
1044 );
1045 }
1046 }
1047 }
1048
1049 fn clause_test_case<S, T>(op: Op, context_value: S, clause_value: T, expected: bool)
1052 where
1053 AttributeValue: From<S>,
1054 AttributeValue: From<T>,
1055 S: Clone,
1056 T: Clone,
1057 {
1058 let clause = Clause {
1059 attribute: Reference::new("attr"),
1060 negate: false,
1061 op,
1062 values: match clause_value.into() {
1063 AttributeValue::Array(vec) => vec,
1064 other => vec![other],
1065 },
1066 context_kind: Kind::default(),
1067 };
1068
1069 let context = ContextBuilder::new("key")
1070 .set_value("attr", context_value.into())
1071 .build()
1072 .unwrap();
1073
1074 let mut evaluation_stack = EvaluationStack::default();
1075 assert_eq!(
1076 clause
1077 .matches(&context, &TestStore {}, &mut evaluation_stack)
1078 .unwrap(),
1079 expected,
1080 "{:?} {:?} {:?} should be {}",
1081 context.get_value(&Reference::new("attr")).unwrap(),
1082 clause.op,
1083 clause.values,
1084 &expected
1085 );
1086 }
1087
1088 #[test]
1089 fn match_is_false_on_invalid_reference() {
1090 let clause = Clause {
1091 attribute: Reference::new("/"),
1092 negate: false,
1093 op: Op::In,
1094 values: vec![],
1095 context_kind: Kind::default(),
1096 };
1097
1098 let context = ContextBuilder::new("key")
1099 .set_value("attr", true.into())
1100 .build()
1101 .unwrap();
1102 let mut evaluation_stack = EvaluationStack::default();
1103 assert!(clause
1104 .matches(&context, &TestStore {}, &mut evaluation_stack)
1105 .is_err());
1106 }
1107
1108 #[test]
1109 fn match_is_false_no_context_matches() {
1110 let clause = Clause {
1111 attribute: Reference::new("attr"),
1112 negate: false,
1113 op: Op::In,
1114 values: vec![true.into()],
1115 context_kind: Kind::default(),
1116 };
1117
1118 let context = ContextBuilder::new("key")
1119 .kind("org")
1120 .set_value("attr", true.into())
1121 .build()
1122 .unwrap();
1123 let mut evaluation_stack = EvaluationStack::default();
1124 assert!(!clause
1125 .matches(&context, &TestStore {}, &mut evaluation_stack)
1126 .unwrap());
1127 }
1128
1129 #[test]
1130 fn test_numeric_clauses() {
1131 clause_test_case(Op::In, 99, 99, true);
1132 clause_test_case(Op::In, 99.0, 99, true);
1133 clause_test_case(Op::In, 99, 99.0, true);
1134 clause_test_case(Op::In, 99, vec![99, 98, 97, 96], true);
1135 clause_test_case(Op::In, 99.0001, 99.0001, true);
1136 clause_test_case(Op::In, 99.0001, vec![99.0001, 98.0, 97.0, 96.0], true);
1137 clause_test_case(Op::LessThan, 1, 1.99999, true);
1138 clause_test_case(Op::LessThan, 1.99999, 1, false);
1139 clause_test_case(Op::LessThan, 1, 2, true);
1140 clause_test_case(Op::LessThanOrEqual, 1, 1.0, true);
1141 clause_test_case(Op::GreaterThan, 2, 1.99999, true);
1142 clause_test_case(Op::GreaterThan, 1.99999, 2, false);
1143 clause_test_case(Op::GreaterThan, 2, 1, true);
1144 clause_test_case(Op::GreaterThanOrEqual, 1, 1.0, true);
1145 }
1146
1147 #[test]
1148 fn test_string_clauses() {
1149 clause_test_case(Op::In, "x", "x", true);
1150 clause_test_case(Op::In, "x", vec!["x", "a", "b", "c"], true);
1151 clause_test_case(Op::In, "x", "xyz", false);
1152 clause_test_case(Op::StartsWith, "xyz", "x", true);
1153 clause_test_case(Op::StartsWith, "x", "xyz", false);
1154 clause_test_case(Op::EndsWith, "xyz", "z", true);
1155 clause_test_case(Op::EndsWith, "z", "xyz", false);
1156 clause_test_case(Op::Contains, "xyz", "y", true);
1157 clause_test_case(Op::Contains, "y", "xyz", false);
1158 }
1159
1160 #[test]
1161 fn test_mixed_string_and_numbers() {
1162 clause_test_case(Op::In, "99", 99, false);
1163 clause_test_case(Op::In, 99, "99", false);
1164 clause_test_case(Op::Contains, "99", 99, false);
1165 clause_test_case(Op::StartsWith, "99", 99, false);
1166 clause_test_case(Op::EndsWith, "99", 99, false);
1167 clause_test_case(Op::LessThanOrEqual, "99", 99, false);
1168 clause_test_case(Op::LessThanOrEqual, 99, "99", false);
1169 clause_test_case(Op::GreaterThanOrEqual, "99", 99, false);
1170 clause_test_case(Op::GreaterThanOrEqual, 99, "99", false);
1171 }
1172
1173 #[test]
1174 fn test_boolean_equality() {
1175 clause_test_case(Op::In, true, true, true);
1176 clause_test_case(Op::In, false, false, true);
1177 clause_test_case(Op::In, true, false, false);
1178 clause_test_case(Op::In, false, true, false);
1179 clause_test_case(Op::In, true, vec![false, true], true);
1180 }
1181
1182 #[test]
1183 fn test_array_equality() {
1184 clause_test_case(Op::In, vec![vec!["x"]], vec![vec!["x"]], true);
1187 clause_test_case(Op::In, vec![vec!["x"]], vec!["x"], false);
1188 clause_test_case(
1189 Op::In,
1190 vec![vec!["x"]],
1191 vec![vec!["x"], vec!["a"], vec!["b"]],
1192 true,
1193 );
1194 }
1195
1196 #[test]
1197 fn test_object_equality() {
1198 clause_test_case(Op::In, hashmap! {"x" => "1"}, hashmap! {"x" => "1"}, true);
1199 clause_test_case(
1200 Op::In,
1201 hashmap! {"x" => "1"},
1202 vec![
1203 hashmap! {"x" => "1"},
1204 hashmap! {"a" => "2"},
1205 hashmap! {"b" => "3"},
1206 ],
1207 true,
1208 );
1209 }
1210
1211 #[test]
1212 fn test_regex_match() {
1213 clause_test_case(Op::Matches, "hello world", "hello.*rld", true);
1214 clause_test_case(Op::Matches, "hello world", "hello.*orl", true);
1215 clause_test_case(Op::Matches, "hello world", "l+", true);
1216 clause_test_case(Op::Matches, "hello world", "(world|planet)", true);
1217 clause_test_case(Op::Matches, "hello world", "aloha", false);
1218 clause_test_case(Op::Matches, "hello world", "***bad regex", false);
1219 }
1220
1221 #[test]
1222 fn test_date_clauses() {
1223 const DATE_STR1: &str = "2017-12-06T00:00:00.000-07:00";
1224 const DATE_STR2: &str = "2017-12-06T00:01:01.000-07:00";
1225 const DATE_MS1: i64 = 10000000;
1226 const DATE_MS2: i64 = 10000001;
1227 const INVALID_DATE: &str = "hey what's this?";
1228
1229 clause_test_case(Op::Before, DATE_STR1, DATE_STR2, true);
1230 clause_test_case(Op::Before, DATE_MS1, DATE_MS2, true);
1231 clause_test_case(Op::Before, DATE_STR2, DATE_STR1, false);
1232 clause_test_case(Op::Before, DATE_MS2, DATE_MS1, false);
1233 clause_test_case(Op::Before, DATE_STR1, DATE_STR1, false);
1234 clause_test_case(Op::Before, DATE_MS1, DATE_MS1, false);
1235 clause_test_case(Op::Before, AttributeValue::Null, DATE_STR1, false);
1236 clause_test_case(Op::Before, DATE_STR1, INVALID_DATE, false);
1237 clause_test_case(Op::After, DATE_STR2, DATE_STR1, true);
1238 clause_test_case(Op::After, DATE_MS2, DATE_MS1, true);
1239 clause_test_case(Op::After, DATE_STR1, DATE_STR2, false);
1240 clause_test_case(Op::After, DATE_MS1, DATE_MS2, false);
1241 clause_test_case(Op::After, DATE_STR1, DATE_STR1, false);
1242 clause_test_case(Op::After, DATE_MS1, DATE_MS1, false);
1243 clause_test_case(Op::After, AttributeValue::Null, DATE_STR1, false);
1244 clause_test_case(Op::After, DATE_STR1, INVALID_DATE, false);
1245 }
1246
1247 #[test]
1248 fn test_semver_clauses() {
1249 clause_test_case(Op::SemVerEqual, "2.0.0", "2.0.0", true);
1250 clause_test_case(Op::SemVerEqual, "2.0", "2.0.0", true);
1251 clause_test_case(Op::SemVerEqual, "2-rc1", "2.0.0-rc1", true);
1252 clause_test_case(Op::SemVerEqual, "2+build2", "2.0.0+build2", true);
1253 clause_test_case(Op::SemVerEqual, "2.0.0", "2.0.1", false);
1254 clause_test_case(Op::SemVerLessThan, "2.0.0", "2.0.1", true);
1255 clause_test_case(Op::SemVerLessThan, "2.0", "2.0.1", true);
1256 clause_test_case(Op::SemVerLessThan, "2.0.1", "2.0.0", false);
1257 clause_test_case(Op::SemVerLessThan, "2.0.1", "2.0", false);
1258 clause_test_case(Op::SemVerLessThan, "2.0.1", "xbad%ver", false);
1259 clause_test_case(Op::SemVerLessThan, "2.0.0-rc", "2.0.0-rc.beta", true);
1260 clause_test_case(Op::SemVerGreaterThan, "2.0.1", "2.0", true);
1261 clause_test_case(Op::SemVerGreaterThan, "10.0.1", "2.0", true);
1262 clause_test_case(Op::SemVerGreaterThan, "2.0.0", "2.0.1", false);
1263 clause_test_case(Op::SemVerGreaterThan, "2.0", "2.0.1", false);
1264 clause_test_case(Op::SemVerGreaterThan, "2.0.1", "xbad%ver", false);
1265 clause_test_case(Op::SemVerGreaterThan, "2.0.0-rc.1", "2.0.0-rc.0", true);
1266 }
1267
1268 #[test]
1269 fn clause_deserialize_with_attribute_missing_causes_error() {
1270 let attribute_missing = json!({
1271 "op" : "in",
1272 "values" : [],
1273 });
1274 assert!(serde_json::from_value::<IntermediateClause>(attribute_missing).is_err());
1275 }
1276
1277 #[test]
1278 fn clause_deserialize_with_op_missing_causes_error() {
1279 let op_missing = json!({
1280 "values" : [],
1281 "attribute" : "",
1282 });
1283 assert!(serde_json::from_value::<IntermediateClause>(op_missing).is_err());
1284 }
1285
1286 #[test]
1287 fn clause_deserialize_with_values_missing_causes_error() {
1288 let values_missing = json!({
1289 "op" : "in",
1290 "values" : [],
1291 });
1292 assert!(serde_json::from_value::<IntermediateClause>(values_missing).is_err());
1293 }
1294
1295 #[test]
1296 fn clause_deserialize_with_required_fields_parses_successfully() {
1297 let all_required_fields_present = json!({
1298 "attribute" : "",
1299 "op" : "in",
1300 "values" : [],
1301 });
1302
1303 assert_eq!(
1304 serde_json::from_value::<IntermediateClause>(all_required_fields_present).unwrap(),
1305 IntermediateClause::ContextOblivious(ClauseWithoutKind {
1306 attribute: AttributeName::default(),
1307 negate: false,
1308 op: Op::In,
1309 values: vec![]
1310 })
1311 );
1312 }
1313
1314 proptest! {
1315 #[test]
1316 fn arbitrary_clause_serialization_rountrip(clause in any_clause()) {
1317 let json = serde_json::to_value(clause).expect("a clause should serialize");
1318 let parsed: Clause = serde_json::from_value(json.clone()).expect("a clause should parse");
1319 assert_json_eq!(json, parsed);
1320 }
1321 }
1322
1323 #[test]
1324 fn clause_with_negate_omitted_defaults_to_false() {
1325 let negate_omitted = json!({
1326 "attribute" : "",
1327 "op" : "in",
1328 "values" : [],
1329 });
1330
1331 assert!(
1332 !serde_json::from_value::<Clause>(negate_omitted)
1333 .unwrap()
1334 .negate
1335 )
1336 }
1337
1338 #[test]
1339 fn clause_with_empty_attribute_defaults_to_invalid_attribute() {
1340 let empty_attribute = json!({
1341 "attribute" : "",
1342 "op" : "in",
1343 "values" : [],
1344 });
1345
1346 let attr = serde_json::from_value::<Clause>(empty_attribute)
1347 .unwrap()
1348 .attribute;
1349 assert_eq!(Reference::default(), attr);
1350 }
1351
1352 proptest! {
1353 #[test]
1354 fn clause_with_context_kind_implies_attribute_references(arbitrary_attribute in any::<String>()) {
1355 let with_context_kind = json!({
1356 "attribute" : arbitrary_attribute,
1357 "op" : "in",
1358 "values" : [],
1359 "contextKind" : "user",
1360 });
1361
1362 prop_assert_eq!(
1363 Reference::new(arbitrary_attribute),
1364 serde_json::from_value::<Clause>(with_context_kind)
1365 .unwrap()
1366 .attribute
1367 )
1368 }
1369 }
1370
1371 proptest! {
1372 #[test]
1373 fn clause_without_context_kind_implies_literal_attribute_name(arbitrary_attribute in any_valid_ref_string()) {
1374 let without_context_kind = json!({
1375 "attribute" : arbitrary_attribute,
1376 "op" : "in",
1377 "values" : [],
1378 });
1379
1380 prop_assert_eq!(
1381 Reference::from(AttributeName::new(arbitrary_attribute)),
1382 serde_json::from_value::<Clause>(without_context_kind)
1383 .unwrap()
1384 .attribute
1385 );
1386 }
1387 }
1388}