launchdarkly_server_sdk_evaluation/
rule.rs

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/// Clause describes an individual clause within a [crate::FlagRule] or `SegmentRule`.
15// Clause is deserialized via a helper, IntermediateClause, because of semantic ambiguity
16// of the attribute Reference field.
17//
18// Clause implements Serialize directly without a helper because References can serialize
19// themselves without any ambiguity.
20#[skip_serializing_none]
21#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
22#[serde(rename_all = "camelCase", from = "IntermediateClause")]
23pub struct Clause {
24    // Kind associated with this clause.
25    context_kind: Kind,
26    // Which context attribute to test. If op does not require an attribute,
27    // then the input may be an empty string, which will construct an invalid
28    // reference.
29    attribute: Reference,
30    // True if the result of the test should be negated.
31    // Skip serializing if false because it is optional in the JSON model.
32    #[serde(skip_serializing_if = "is_false")]
33    negate: bool,
34    // The test operation.
35    op: Op,
36    // The values to test against.
37    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    // ClauseWithKind must be listed first in the enum because otherwise ClauseWithoutKind
65    // could match the input (by ignoring/discarding the context_kind field).
66    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        // Generate arbitrary clauses. The clauses op will always be fixed to Op::In,
102        // and the values array will contain between 0-5 AtrributeValue::Bool elements.
103        pub(crate) fn any_clause()(
104            kind in any_kind(),
105            // reference is any_ref(), rather than any_valid_ref(), because we also want
106            // coverage of invalid references.
107            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/// FlagRule describes a single rule within a feature flag.
124///
125/// A rule consists of a set of ANDed matching conditions ([Clause]) for a context, along with either a
126/// fixed variation or a set of rollout percentages to use if the context matches all of the clauses.
127#[derive(Clone, Debug, Serialize, Deserialize)]
128#[serde(rename_all = "camelCase")]
129pub struct FlagRule {
130    /// A randomized identifier assigned to each rule when it is created.
131    ///
132    /// This is used to populate the id property of [crate::Reason]
133    #[serde(default)]
134    pub id: String,
135    clauses: Vec<Clause>,
136
137    /// Defines what variation to return if the context matches this rule.
138    #[serde(flatten)]
139    pub variation_or_rollout: VariationOrRollout,
140
141    /// Used internally by the SDK analytics event system.
142    ///
143    /// This field is true if the current LaunchDarkly account has experimentation enabled, has
144    /// associated this flag with an experiment, and has enabled this rule for the experiment. This
145    /// tells the SDK to send full event data for any evaluation that matches this rule.
146    ///
147    /// The launchdarkly-server-sdk-evaluation package does not implement that behavior; it is only
148    /// in the data model for use by the SDK.
149    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    // Use when matching a clause that has an associated context kind.
268    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    // Use when matching a clause that isn't context-aware.
280    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    /// Determines if a context matches the provided flag rule.
293    ///
294    /// A context will match if all flag clauses match; otherwise, this method returns false.
295    pub(crate) fn matches(
296        &self,
297        context: &Context,
298        store: &dyn Store,
299        evaluation_stack: &mut EvaluationStack,
300    ) -> Result<bool, String> {
301        // rules match if _all_ of their clauses do
302        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            // string ops
338            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            // numeric ops
350            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        // strings
439        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        // numbers
448        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        // arrays
453        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        // objects
460        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        // degenerate cases
476        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        // test asymmetry
481        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        // degenerate cases
493        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        // test asymmetry
498        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        // degenerate cases
510        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        // test asymmetry
515        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                "`{}` should match `{}`",
530                text,
531                pattern
532            );
533        }
534
535        fn should_not_match(text: &str, pattern: &str) {
536            assert!(
537                !Op::Matches.matches(&astring(text), &astring(pattern)),
538                "`{}` should not match `{}`",
539                text,
540                pattern
541            );
542        }
543
544        should_match("", "");
545        should_match("a", "");
546        should_match("a", "a");
547        should_match("a", ".");
548        should_match("hello world", "hello.*rld");
549        should_match("hello world", "hello.*orl");
550        should_match("hello world", "l+");
551        should_match("hello world", "(world|planet)");
552
553        should_not_match("", ".");
554        should_not_match("", r"\");
555        should_not_match("hello world", "aloha");
556        should_not_match("hello world", "***bad regex");
557    }
558
559    #[test]
560    fn test_ops_numeric() {
561        // basic numeric comparisons
562        assert!(Op::LessThan.matches(&anum(0.0), &anum(1.0)));
563        assert!(!Op::LessThan.matches(&anum(0.0), &anum(0.0)));
564        assert!(!Op::LessThan.matches(&anum(1.0), &anum(0.0)));
565
566        assert!(Op::GreaterThan.matches(&anum(1.0), &anum(0.0)));
567        assert!(!Op::GreaterThan.matches(&anum(0.0), &anum(0.0)));
568        assert!(!Op::GreaterThan.matches(&anum(0.0), &anum(1.0)));
569
570        assert!(Op::LessThanOrEqual.matches(&anum(0.0), &anum(1.0)));
571        assert!(Op::LessThanOrEqual.matches(&anum(0.0), &anum(0.0)));
572        assert!(!Op::LessThanOrEqual.matches(&anum(1.0), &anum(0.0)));
573
574        assert!(Op::GreaterThanOrEqual.matches(&anum(1.0), &anum(0.0)));
575        assert!(Op::GreaterThanOrEqual.matches(&anum(0.0), &anum(0.0)));
576        assert!(!Op::GreaterThanOrEqual.matches(&anum(0.0), &anum(1.0)));
577
578        // no conversions
579        assert!(!Op::LessThan.matches(&astring("0"), &anum(1.0)));
580        assert!(!Op::LessThan.matches(&anum(0.0), &astring("1")));
581    }
582
583    #[test]
584    fn test_ops_time() {
585        let today = SystemTime::now();
586        let today_millis = today
587            .duration_since(SystemTime::UNIX_EPOCH)
588            .unwrap()
589            .as_millis() as f64;
590        let yesterday_millis = today_millis - 86_400_000_f64;
591
592        // basic UNIX timestamp comparisons
593        assert!(Op::Before.matches(&anum(yesterday_millis), &anum(today_millis)));
594        assert!(!Op::Before.matches(&anum(today_millis), &anum(yesterday_millis)));
595        assert!(!Op::Before.matches(&anum(today_millis), &anum(today_millis)));
596
597        assert!(Op::After.matches(&anum(today_millis), &anum(yesterday_millis)));
598        assert!(!Op::After.matches(&anum(yesterday_millis), &anum(today_millis)));
599        assert!(!Op::After.matches(&anum(today_millis), &anum(today_millis)));
600
601        // numeric strings don't get converted to millis
602        assert!(!Op::Before.matches(&astring(&yesterday_millis.to_string()), &anum(today_millis)));
603        assert!(!Op::After.matches(&anum(today_millis), &astring(&yesterday_millis.to_string())));
604
605        // date-formatted strings get parsed
606        assert!(Op::Before.matches(
607            &astring("2019-11-19T17:29:00.000000-07:00"),
608            &anum(today_millis)
609        ));
610        assert!(
611            Op::Before.matches(&astring("2019-11-19T17:29:00-07:00"), &anum(today_millis)),
612            "fractional seconds part is optional"
613        );
614
615        assert!(Op::After.matches(
616            &anum(today_millis),
617            &astring("2019-11-19T17:29:00.000000-07:00")
618        ));
619
620        // nonsense strings don't match
621        assert!(!Op::Before.matches(&astring("fish"), &anum(today_millis)));
622        assert!(!Op::After.matches(&anum(today_millis), &astring("fish")));
623    }
624
625    #[test]
626    fn test_semver_ops() {
627        assert!(Op::SemVerEqual.matches(&astring("2.0.0"), &astring("2.0.0")));
628
629        assert!(
630            Op::SemVerEqual.matches(&astring("2.0"), &astring("2.0.0")),
631            "we allow missing components (filled in with zeroes)"
632        );
633        assert!(
634            Op::SemVerEqual.matches(&astring("2"), &astring("2.0.0")),
635            "we allow missing components (filled in with zeroes)"
636        );
637
638        assert!(!Op::SemVerEqual.matches(&astring("2.0.0"), &astring("3.0.0")));
639        assert!(!Op::SemVerEqual.matches(&astring("2.0.0"), &astring("2.1.0")));
640        assert!(!Op::SemVerEqual.matches(&astring("2.0.0"), &astring("2.0.1")));
641
642        assert!(Op::SemVerGreaterThan.matches(&astring("3.0.0"), &astring("2.0.0")));
643        assert!(Op::SemVerGreaterThan.matches(&astring("2.1.0"), &astring("2.0.0")));
644        assert!(Op::SemVerGreaterThan.matches(&astring("2.0.1"), &astring("2.0.0")));
645        assert!(Op::SemVerGreaterThan
646            .matches(&astring("2.0.0-rc.10.green"), &astring("2.0.0-rc.2.green")));
647        assert!(
648            Op::SemVerGreaterThan.matches(&astring("2.0.0-rc.2.red"), &astring("2.0.0-rc.2.green")),
649            "red > green"
650        );
651        assert!(
652            Op::SemVerGreaterThan
653                .matches(&astring("2.0.0-rc.2.green.1"), &astring("2.0.0-rc.2.green")),
654            "adding more version components makes it greater"
655        );
656
657        assert!(!Op::SemVerGreaterThan.matches(&astring("2.0.0"), &astring("2.0.0")));
658        assert!(!Op::SemVerGreaterThan.matches(&astring("1.9.0"), &astring("2.0.0")));
659        assert!(
660            !Op::SemVerGreaterThan.matches(&astring("2.0.0-rc"), &astring("2.0.0")),
661            "prerelease version < released version"
662        );
663        assert!(
664            !Op::SemVerGreaterThan.matches(&astring("2.0.0+build"), &astring("2.0.0")),
665            "build metadata is ignored, these versions are equal"
666        );
667
668        assert!(!Op::SemVerEqual.matches(&astring("2.0.0"), &astring("200")));
669
670        // we don't convert
671        assert!(!Op::SemVerEqual.matches(&astring("2.0.0"), &anum(2.0)));
672    }
673
674    #[test]
675    fn test_clause_matches() {
676        let one_val_clause = Clause {
677            attribute: Reference::new("a"),
678            negate: false,
679            op: Op::In,
680            values: vec!["foo".into()],
681            context_kind: Kind::default(),
682        };
683        let many_val_clause = Clause {
684            attribute: Reference::new("a"),
685            negate: false,
686            op: Op::In,
687            values: vec!["foo".into(), "bar".into()],
688            context_kind: Kind::default(),
689        };
690        let negated_clause = Clause {
691            attribute: Reference::new("a"),
692            negate: true,
693            op: Op::In,
694            values: vec!["foo".into()],
695            context_kind: Kind::default(),
696        };
697        let negated_many_val_clause = Clause {
698            attribute: Reference::new("a"),
699            negate: true,
700            op: Op::In,
701            values: vec!["foo".into(), "bar".into()],
702            context_kind: Kind::default(),
703        };
704        let key_clause = Clause {
705            attribute: Reference::new("key"),
706            negate: false,
707            op: Op::In,
708            values: vec!["matching".into()],
709            context_kind: Kind::default(),
710        };
711
712        let mut context_builder = ContextBuilder::new("without");
713        let context_without_attribute = context_builder.build().expect("Failed to build context");
714
715        context_builder
716            .key("matching")
717            .set_value("a", AttributeValue::String("foo".to_string()));
718        let matching_context = context_builder.build().expect("Failed to build context");
719
720        context_builder
721            .key("non-matching")
722            .set_value("a", AttributeValue::String("lol".to_string()));
723        let non_matching_context = context_builder.build().expect("Failed to build context");
724
725        let mut evaluation_stack = EvaluationStack::default();
726
727        assert!(one_val_clause
728            .matches(&matching_context, &TestStore {}, &mut evaluation_stack)
729            .unwrap());
730        assert!(!one_val_clause
731            .matches(&non_matching_context, &TestStore {}, &mut evaluation_stack)
732            .unwrap());
733        assert!(!one_val_clause
734            .matches(
735                &context_without_attribute,
736                &TestStore {},
737                &mut evaluation_stack
738            )
739            .unwrap());
740
741        assert!(!negated_clause
742            .matches(&matching_context, &TestStore {}, &mut evaluation_stack)
743            .unwrap());
744        assert!(negated_clause
745            .matches(&non_matching_context, &TestStore {}, &mut evaluation_stack)
746            .unwrap());
747
748        assert!(
749            !negated_clause
750                .matches(
751                    &context_without_attribute,
752                    &TestStore {},
753                    &mut evaluation_stack
754                )
755                .unwrap(),
756            "targeting missing attribute does not match even when negated"
757        );
758
759        assert!(
760            many_val_clause
761                .matches(&matching_context, &TestStore {}, &mut evaluation_stack)
762                .unwrap(),
763            "requires only one of the values"
764        );
765        assert!(!many_val_clause
766            .matches(&non_matching_context, &TestStore {}, &mut evaluation_stack)
767            .unwrap());
768        assert!(!many_val_clause
769            .matches(
770                &context_without_attribute,
771                &TestStore {},
772                &mut evaluation_stack
773            )
774            .unwrap());
775
776        assert!(
777            !negated_many_val_clause
778                .matches(&matching_context, &TestStore {}, &mut evaluation_stack)
779                .unwrap(),
780            "requires all values are missing"
781        );
782        assert!(negated_many_val_clause
783            .matches(&non_matching_context, &TestStore {}, &mut evaluation_stack)
784            .unwrap());
785
786        assert!(
787            !negated_many_val_clause
788                .matches(
789                    &context_without_attribute,
790                    &TestStore {},
791                    &mut evaluation_stack
792                )
793                .unwrap(),
794            "targeting missing attribute does not match even when negated"
795        );
796
797        assert!(
798            key_clause
799                .matches(&matching_context, &TestStore {}, &mut evaluation_stack)
800                .unwrap(),
801            "should match key"
802        );
803        assert!(
804            !key_clause
805                .matches(&non_matching_context, &TestStore {}, &mut evaluation_stack)
806                .unwrap(),
807            "should not match non-matching key"
808        );
809
810        context_builder.key("with-many").set_value(
811            "a",
812            AttributeValue::Array(vec![
813                AttributeValue::String("foo".to_string()),
814                AttributeValue::String("bar".to_string()),
815                AttributeValue::String("lol".to_string()),
816            ]),
817        );
818        let context_with_many = context_builder.build().expect("Failed to build context");
819
820        assert!(one_val_clause
821            .matches(&context_with_many, &TestStore {}, &mut evaluation_stack)
822            .unwrap());
823        assert!(many_val_clause
824            .matches(&context_with_many, &TestStore {}, &mut evaluation_stack)
825            .unwrap());
826
827        assert!(!negated_clause
828            .matches(&context_with_many, &TestStore {}, &mut evaluation_stack)
829            .unwrap());
830        assert!(!negated_many_val_clause
831            .matches(&context_with_many, &TestStore {}, &mut evaluation_stack)
832            .unwrap());
833    }
834
835    struct AttributeTestCase {
836        matching_context: Context,
837        non_matching_context: Context,
838        context_without_attribute: Option<Context>,
839    }
840
841    #[test]
842    fn test_clause_matches_attributes() {
843        let tests: HashMap<&str, AttributeTestCase> = hashmap! {
844            "key" => AttributeTestCase {
845                matching_context: ContextBuilder::new("match").build().unwrap(),
846                non_matching_context: ContextBuilder::new("nope").build().unwrap(),
847                context_without_attribute: None,
848            },
849            "name" => AttributeTestCase {
850                matching_context: ContextBuilder::new("matching").name("match").build().unwrap(),
851                non_matching_context: ContextBuilder::new("non-matching").name("nope").build().unwrap(),
852                context_without_attribute: Some(ContextBuilder::new("without-attribute").build().unwrap()),
853            },
854        };
855
856        let mut evaluation_stack = EvaluationStack::default();
857
858        for (attr, test_case) in tests {
859            let clause = Clause {
860                attribute: Reference::new(attr),
861                negate: false,
862                op: Op::In,
863                values: vec!["match".into()],
864                context_kind: Kind::default(),
865            };
866
867            assert!(
868                clause
869                    .matches(
870                        &test_case.matching_context,
871                        &TestStore {},
872                        &mut evaluation_stack
873                    )
874                    .unwrap(),
875                "should match {}",
876                attr
877            );
878            assert!(
879                !clause
880                    .matches(
881                        &test_case.non_matching_context,
882                        &TestStore {},
883                        &mut evaluation_stack
884                    )
885                    .unwrap(),
886                "should not match non-matching {}",
887                attr
888            );
889            if let Some(context_without_attribute) = test_case.context_without_attribute {
890                assert!(
891                    !clause
892                        .matches(
893                            &context_without_attribute,
894                            &TestStore {},
895                            &mut evaluation_stack
896                        )
897                        .unwrap(),
898                    "should not match user with null {}",
899                    attr
900                );
901            }
902        }
903    }
904
905    #[test]
906    fn test_clause_matches_anonymous_attribute() {
907        let clause = Clause {
908            attribute: Reference::new("anonymous"),
909            negate: false,
910            op: Op::In,
911            values: vec![true.into()],
912            context_kind: Kind::default(),
913        };
914
915        let anon_context = ContextBuilder::new("anon").anonymous(true).build().unwrap();
916        let non_anon_context = ContextBuilder::new("nonanon")
917            .anonymous(false)
918            .build()
919            .unwrap();
920        let implicitly_non_anon_context = ContextBuilder::new("implicit").build().unwrap();
921
922        let mut evaluation_stack = EvaluationStack::default();
923        assert!(clause
924            .matches(&anon_context, &TestStore {}, &mut evaluation_stack)
925            .unwrap());
926        assert!(!clause
927            .matches(&non_anon_context, &TestStore {}, &mut evaluation_stack)
928            .unwrap());
929        assert!(!clause
930            .matches(
931                &implicitly_non_anon_context,
932                &TestStore {},
933                &mut evaluation_stack
934            )
935            .unwrap());
936    }
937
938    #[test]
939    fn test_clause_matches_custom_attributes() {
940        // check we can have an attribute called "custom"
941        for attr in &["custom", "custom1"] {
942            let clause = Clause {
943                attribute: Reference::new(attr),
944                negate: false,
945                op: Op::In,
946                values: vec!["match".into()],
947                context_kind: Kind::default(),
948            };
949
950            let matching_context = ContextBuilder::new("matching")
951                .set_value(attr, AttributeValue::String("match".into()))
952                .build()
953                .unwrap();
954            let non_matching_context = ContextBuilder::new("non-matching")
955                .set_value(attr, AttributeValue::String("nope".into()))
956                .build()
957                .unwrap();
958            let context_without_attribute = ContextBuilder::new("without_attribute")
959                .set_value(attr, AttributeValue::Null)
960                .build()
961                .unwrap();
962
963            let mut evaluation_stack = EvaluationStack::default();
964            assert!(
965                clause
966                    .matches(&matching_context, &TestStore {}, &mut evaluation_stack)
967                    .unwrap(),
968                "should match {}",
969                attr
970            );
971            assert!(
972                !clause
973                    .matches(&non_matching_context, &TestStore {}, &mut evaluation_stack)
974                    .unwrap(),
975                "should not match non-matching {}",
976                attr
977            );
978            assert!(
979                !clause
980                    .matches(
981                        &context_without_attribute,
982                        &TestStore {},
983                        &mut evaluation_stack
984                    )
985                    .unwrap(),
986                "should not match user with null {}",
987                attr
988            );
989        }
990    }
991
992    #[test]
993    fn test_null_attribute() {
994        let context_null_attr = ContextBuilder::new("key")
995            .set_value("attr", AttributeValue::Null)
996            .build()
997            .unwrap();
998
999        let context_missing_attr = ContextBuilder::new("key").build().unwrap();
1000
1001        let clause_values = vec![
1002            AttributeValue::Bool(true),
1003            AttributeValue::Bool(false),
1004            AttributeValue::Number(1.5),
1005            AttributeValue::Number(1.0),
1006            AttributeValue::Null,
1007            AttributeValue::String("abc".to_string()),
1008            AttributeValue::Array(vec![
1009                AttributeValue::String("def".to_string()),
1010                AttributeValue::Null,
1011            ]),
1012        ];
1013
1014        for op in &[
1015            Op::In,
1016            Op::StartsWith,
1017            Op::EndsWith,
1018            Op::Contains,
1019            Op::Matches,
1020            Op::LessThan,
1021            Op::LessThanOrEqual,
1022            Op::GreaterThan,
1023            Op::GreaterThanOrEqual,
1024            Op::Before,
1025            Op::After,
1026            Op::SemVerEqual,
1027            Op::SemVerGreaterThan,
1028            Op::SemVerLessThan,
1029        ] {
1030            for neg in &[true, false] {
1031                let clause = Clause {
1032                    attribute: Reference::new("attr"),
1033                    negate: *neg,
1034                    op: *op,
1035                    values: clause_values.clone(),
1036                    context_kind: Kind::default(),
1037                };
1038                let mut evaluation_stack = EvaluationStack::default();
1039                assert!(
1040                    !clause
1041                        .matches(&context_null_attr, &TestStore {}, &mut evaluation_stack)
1042                        .unwrap(),
1043                    "Null attribute matches operator {:?} when {}negated",
1044                    clause.op,
1045                    if *neg { "" } else { "not " },
1046                );
1047                assert!(
1048                    !clause
1049                        .matches(&context_missing_attr, &TestStore {}, &mut evaluation_stack)
1050                        .unwrap(),
1051                    "Missing attribute matches operator {:?} when {}negated",
1052                    clause.op,
1053                    if *neg { "" } else { "not " },
1054                );
1055            }
1056        }
1057    }
1058
1059    // The following test cases are ported from the Go implementation:
1060    // https://github.com/launchdarkly/go-server-sdk-evaluation/blob/v1/ldmodel/match_clause_operator_test.go#L28-L155
1061    fn clause_test_case<S, T>(op: Op, context_value: S, clause_value: T, expected: bool)
1062    where
1063        AttributeValue: From<S>,
1064        AttributeValue: From<T>,
1065        S: Clone,
1066        T: Clone,
1067    {
1068        let clause = Clause {
1069            attribute: Reference::new("attr"),
1070            negate: false,
1071            op,
1072            values: match clause_value.into() {
1073                AttributeValue::Array(vec) => vec,
1074                other => vec![other],
1075            },
1076            context_kind: Kind::default(),
1077        };
1078
1079        let context = ContextBuilder::new("key")
1080            .set_value("attr", context_value.into())
1081            .build()
1082            .unwrap();
1083
1084        let mut evaluation_stack = EvaluationStack::default();
1085        assert_eq!(
1086            clause
1087                .matches(&context, &TestStore {}, &mut evaluation_stack)
1088                .unwrap(),
1089            expected,
1090            "{:?} {:?} {:?} should be {}",
1091            context.get_value(&Reference::new("attr")).unwrap(),
1092            clause.op,
1093            clause.values,
1094            &expected
1095        );
1096    }
1097
1098    #[test]
1099    fn match_is_false_on_invalid_reference() {
1100        let clause = Clause {
1101            attribute: Reference::new("/"),
1102            negate: false,
1103            op: Op::In,
1104            values: vec![],
1105            context_kind: Kind::default(),
1106        };
1107
1108        let context = ContextBuilder::new("key")
1109            .set_value("attr", true.into())
1110            .build()
1111            .unwrap();
1112        let mut evaluation_stack = EvaluationStack::default();
1113        assert!(clause
1114            .matches(&context, &TestStore {}, &mut evaluation_stack)
1115            .is_err());
1116    }
1117
1118    #[test]
1119    fn match_is_false_no_context_matches() {
1120        let clause = Clause {
1121            attribute: Reference::new("attr"),
1122            negate: false,
1123            op: Op::In,
1124            values: vec![true.into()],
1125            context_kind: Kind::default(),
1126        };
1127
1128        let context = ContextBuilder::new("key")
1129            .kind("org")
1130            .set_value("attr", true.into())
1131            .build()
1132            .unwrap();
1133        let mut evaluation_stack = EvaluationStack::default();
1134        assert!(!clause
1135            .matches(&context, &TestStore {}, &mut evaluation_stack)
1136            .unwrap());
1137    }
1138
1139    #[test]
1140    fn test_numeric_clauses() {
1141        clause_test_case(Op::In, 99, 99, true);
1142        clause_test_case(Op::In, 99.0, 99, true);
1143        clause_test_case(Op::In, 99, 99.0, true);
1144        clause_test_case(Op::In, 99, vec![99, 98, 97, 96], true);
1145        clause_test_case(Op::In, 99.0001, 99.0001, true);
1146        clause_test_case(Op::In, 99.0001, vec![99.0001, 98.0, 97.0, 96.0], true);
1147        clause_test_case(Op::LessThan, 1, 1.99999, true);
1148        clause_test_case(Op::LessThan, 1.99999, 1, false);
1149        clause_test_case(Op::LessThan, 1, 2, true);
1150        clause_test_case(Op::LessThanOrEqual, 1, 1.0, true);
1151        clause_test_case(Op::GreaterThan, 2, 1.99999, true);
1152        clause_test_case(Op::GreaterThan, 1.99999, 2, false);
1153        clause_test_case(Op::GreaterThan, 2, 1, true);
1154        clause_test_case(Op::GreaterThanOrEqual, 1, 1.0, true);
1155    }
1156
1157    #[test]
1158    fn test_string_clauses() {
1159        clause_test_case(Op::In, "x", "x", true);
1160        clause_test_case(Op::In, "x", vec!["x", "a", "b", "c"], true);
1161        clause_test_case(Op::In, "x", "xyz", false);
1162        clause_test_case(Op::StartsWith, "xyz", "x", true);
1163        clause_test_case(Op::StartsWith, "x", "xyz", false);
1164        clause_test_case(Op::EndsWith, "xyz", "z", true);
1165        clause_test_case(Op::EndsWith, "z", "xyz", false);
1166        clause_test_case(Op::Contains, "xyz", "y", true);
1167        clause_test_case(Op::Contains, "y", "xyz", false);
1168    }
1169
1170    #[test]
1171    fn test_mixed_string_and_numbers() {
1172        clause_test_case(Op::In, "99", 99, false);
1173        clause_test_case(Op::In, 99, "99", false);
1174        clause_test_case(Op::Contains, "99", 99, false);
1175        clause_test_case(Op::StartsWith, "99", 99, false);
1176        clause_test_case(Op::EndsWith, "99", 99, false);
1177        clause_test_case(Op::LessThanOrEqual, "99", 99, false);
1178        clause_test_case(Op::LessThanOrEqual, 99, "99", false);
1179        clause_test_case(Op::GreaterThanOrEqual, "99", 99, false);
1180        clause_test_case(Op::GreaterThanOrEqual, 99, "99", false);
1181    }
1182
1183    #[test]
1184    fn test_boolean_equality() {
1185        clause_test_case(Op::In, true, true, true);
1186        clause_test_case(Op::In, false, false, true);
1187        clause_test_case(Op::In, true, false, false);
1188        clause_test_case(Op::In, false, true, false);
1189        clause_test_case(Op::In, true, vec![false, true], true);
1190    }
1191
1192    #[test]
1193    fn test_array_equality() {
1194        // note that the user value must be an array *of arrays*, because a single-level
1195        // array is interpreted as "any of these values"
1196        clause_test_case(Op::In, vec![vec!["x"]], vec![vec!["x"]], true);
1197        clause_test_case(Op::In, vec![vec!["x"]], vec!["x"], false);
1198        clause_test_case(
1199            Op::In,
1200            vec![vec!["x"]],
1201            vec![vec!["x"], vec!["a"], vec!["b"]],
1202            true,
1203        );
1204    }
1205
1206    #[test]
1207    fn test_object_equality() {
1208        clause_test_case(Op::In, hashmap! {"x" => "1"}, hashmap! {"x" => "1"}, true);
1209        clause_test_case(
1210            Op::In,
1211            hashmap! {"x" => "1"},
1212            vec![
1213                hashmap! {"x" => "1"},
1214                hashmap! {"a" => "2"},
1215                hashmap! {"b" => "3"},
1216            ],
1217            true,
1218        );
1219    }
1220
1221    #[test]
1222    fn test_regex_match() {
1223        clause_test_case(Op::Matches, "hello world", "hello.*rld", true);
1224        clause_test_case(Op::Matches, "hello world", "hello.*orl", true);
1225        clause_test_case(Op::Matches, "hello world", "l+", true);
1226        clause_test_case(Op::Matches, "hello world", "(world|planet)", true);
1227        clause_test_case(Op::Matches, "hello world", "aloha", false);
1228        clause_test_case(Op::Matches, "hello world", "***bad regex", false);
1229    }
1230
1231    #[test]
1232    fn test_date_clauses() {
1233        const DATE_STR1: &str = "2017-12-06T00:00:00.000-07:00";
1234        const DATE_STR2: &str = "2017-12-06T00:01:01.000-07:00";
1235        const DATE_MS1: i64 = 10000000;
1236        const DATE_MS2: i64 = 10000001;
1237        const INVALID_DATE: &str = "hey what's this?";
1238
1239        clause_test_case(Op::Before, DATE_STR1, DATE_STR2, true);
1240        clause_test_case(Op::Before, DATE_MS1, DATE_MS2, true);
1241        clause_test_case(Op::Before, DATE_STR2, DATE_STR1, false);
1242        clause_test_case(Op::Before, DATE_MS2, DATE_MS1, false);
1243        clause_test_case(Op::Before, DATE_STR1, DATE_STR1, false);
1244        clause_test_case(Op::Before, DATE_MS1, DATE_MS1, false);
1245        clause_test_case(Op::Before, AttributeValue::Null, DATE_STR1, false);
1246        clause_test_case(Op::Before, DATE_STR1, INVALID_DATE, false);
1247        clause_test_case(Op::After, DATE_STR2, DATE_STR1, true);
1248        clause_test_case(Op::After, DATE_MS2, DATE_MS1, true);
1249        clause_test_case(Op::After, DATE_STR1, DATE_STR2, false);
1250        clause_test_case(Op::After, DATE_MS1, DATE_MS2, false);
1251        clause_test_case(Op::After, DATE_STR1, DATE_STR1, false);
1252        clause_test_case(Op::After, DATE_MS1, DATE_MS1, false);
1253        clause_test_case(Op::After, AttributeValue::Null, DATE_STR1, false);
1254        clause_test_case(Op::After, DATE_STR1, INVALID_DATE, false);
1255    }
1256
1257    #[test]
1258    fn test_semver_clauses() {
1259        clause_test_case(Op::SemVerEqual, "2.0.0", "2.0.0", true);
1260        clause_test_case(Op::SemVerEqual, "2.0", "2.0.0", true);
1261        clause_test_case(Op::SemVerEqual, "2-rc1", "2.0.0-rc1", true);
1262        clause_test_case(Op::SemVerEqual, "2+build2", "2.0.0+build2", true);
1263        clause_test_case(Op::SemVerEqual, "2.0.0", "2.0.1", false);
1264        clause_test_case(Op::SemVerLessThan, "2.0.0", "2.0.1", true);
1265        clause_test_case(Op::SemVerLessThan, "2.0", "2.0.1", true);
1266        clause_test_case(Op::SemVerLessThan, "2.0.1", "2.0.0", false);
1267        clause_test_case(Op::SemVerLessThan, "2.0.1", "2.0", false);
1268        clause_test_case(Op::SemVerLessThan, "2.0.1", "xbad%ver", false);
1269        clause_test_case(Op::SemVerLessThan, "2.0.0-rc", "2.0.0-rc.beta", true);
1270        clause_test_case(Op::SemVerGreaterThan, "2.0.1", "2.0", true);
1271        clause_test_case(Op::SemVerGreaterThan, "10.0.1", "2.0", true);
1272        clause_test_case(Op::SemVerGreaterThan, "2.0.0", "2.0.1", false);
1273        clause_test_case(Op::SemVerGreaterThan, "2.0", "2.0.1", false);
1274        clause_test_case(Op::SemVerGreaterThan, "2.0.1", "xbad%ver", false);
1275        clause_test_case(Op::SemVerGreaterThan, "2.0.0-rc.1", "2.0.0-rc.0", true);
1276    }
1277
1278    #[test]
1279    fn clause_deserialize_with_attribute_missing_causes_error() {
1280        let attribute_missing = json!({
1281            "op" : "in",
1282            "values" : [],
1283        });
1284        assert!(serde_json::from_value::<IntermediateClause>(attribute_missing).is_err());
1285    }
1286
1287    #[test]
1288    fn clause_deserialize_with_op_missing_causes_error() {
1289        let op_missing = json!({
1290            "values" : [],
1291            "attribute" : "",
1292        });
1293        assert!(serde_json::from_value::<IntermediateClause>(op_missing).is_err());
1294    }
1295
1296    #[test]
1297    fn clause_deserialize_with_values_missing_causes_error() {
1298        let values_missing = json!({
1299            "op" : "in",
1300            "values" : [],
1301        });
1302        assert!(serde_json::from_value::<IntermediateClause>(values_missing).is_err());
1303    }
1304
1305    #[test]
1306    fn clause_deserialize_with_required_fields_parses_successfully() {
1307        let all_required_fields_present = json!({
1308            "attribute" : "",
1309            "op" : "in",
1310            "values" : [],
1311        });
1312
1313        assert_eq!(
1314            serde_json::from_value::<IntermediateClause>(all_required_fields_present).unwrap(),
1315            IntermediateClause::ContextOblivious(ClauseWithoutKind {
1316                attribute: AttributeName::default(),
1317                negate: false,
1318                op: Op::In,
1319                values: vec![]
1320            })
1321        );
1322    }
1323
1324    proptest! {
1325        #[test]
1326        fn arbitrary_clause_serialization_rountrip(clause in any_clause()) {
1327            let json = serde_json::to_value(clause).expect("a clause should serialize");
1328            let parsed: Clause = serde_json::from_value(json.clone()).expect("a clause should parse");
1329            assert_json_eq!(json, parsed);
1330        }
1331    }
1332
1333    #[test]
1334    fn clause_with_negate_omitted_defaults_to_false() {
1335        let negate_omitted = json!({
1336            "attribute" : "",
1337            "op" : "in",
1338            "values" : [],
1339        });
1340
1341        assert!(
1342            !serde_json::from_value::<Clause>(negate_omitted)
1343                .unwrap()
1344                .negate
1345        )
1346    }
1347
1348    #[test]
1349    fn clause_with_empty_attribute_defaults_to_invalid_attribute() {
1350        let empty_attribute = json!({
1351            "attribute" : "",
1352            "op" : "in",
1353            "values" : [],
1354        });
1355
1356        let attr = serde_json::from_value::<Clause>(empty_attribute)
1357            .unwrap()
1358            .attribute;
1359        assert_eq!(Reference::default(), attr);
1360    }
1361
1362    proptest! {
1363        #[test]
1364        fn clause_with_context_kind_implies_attribute_references(arbitrary_attribute in any::<String>()) {
1365            let with_context_kind = json!({
1366                "attribute" : arbitrary_attribute,
1367                "op" : "in",
1368                "values" : [],
1369                "contextKind" : "user",
1370            });
1371
1372            prop_assert_eq!(
1373                Reference::new(arbitrary_attribute),
1374                serde_json::from_value::<Clause>(with_context_kind)
1375                    .unwrap()
1376                    .attribute
1377            )
1378        }
1379    }
1380
1381    proptest! {
1382        #[test]
1383        fn clause_without_context_kind_implies_literal_attribute_name(arbitrary_attribute in any_valid_ref_string()) {
1384            let without_context_kind = json!({
1385                "attribute" : arbitrary_attribute,
1386                "op" : "in",
1387                "values" : [],
1388            });
1389
1390            prop_assert_eq!(
1391                Reference::from(AttributeName::new(arbitrary_attribute)),
1392                serde_json::from_value::<Clause>(without_context_kind)
1393                .unwrap()
1394                .attribute
1395            );
1396        }
1397    }
1398}