Skip to main content

launchdarkly_server_sdk_evaluation/
segment.rs

1use serde::{Deserialize, Serialize};
2
3use crate::contexts::attribute_reference::AttributeName;
4use crate::contexts::context::{BucketPrefix, Kind};
5use crate::rule::Clause;
6use crate::variation::VariationWeight;
7use crate::{Context, EvaluationStack, Reference, Store, Versioned};
8use serde_with::skip_serializing_none;
9
10/// Segment describes a group of contexts based on keys and/or matching rules.
11#[derive(Clone, Debug, Default, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct Segment {
14    /// The unique key of the segment.
15    pub key: String,
16    /// A list of context keys that are always matched by this segment.
17    pub included: Vec<String>,
18    /// A list of context keys that are never matched by this segment, unless the key is also in
19    /// included.
20    pub excluded: Vec<String>,
21
22    #[serde(default)]
23    included_contexts: Vec<SegmentTarget>,
24    #[serde(default)]
25    excluded_contexts: Vec<SegmentTarget>,
26
27    rules: Vec<SegmentRule>,
28    salt: String,
29
30    /// Unbounded is true if this is a segment whose included list is stored separately and is not limited in size.
31    /// Currently, the server-side Rust SDK cannot access the context list for this kind of segment; it only works when
32    /// flags are being evaluated within the LaunchDarkly service.
33    ///
34    /// The name is historical: "unbounded segments" was an earlier name for the product feature that is currently
35    /// known as "big segments". If unbounded is true, this is a big segment.
36    #[serde(default)]
37    pub unbounded: bool,
38    #[serde(default)]
39    /// Unbounded segments target specific, individual context kinds. If this value is not
40    /// provided, it is assumed to be the user kind.
41    pub unbounded_context_kind: Option<Kind>,
42    #[serde(default)]
43    generation: Option<i64>,
44
45    /// An integer that is incremented by LaunchDarkly every time the configuration of the segment
46    /// is changed.
47    pub version: u64,
48}
49
50impl Versioned for Segment {
51    fn version(&self) -> u64 {
52        self.version
53    }
54}
55
56// SegmentRule describes a rule that determines if a context is part of a segment.
57// SegmentRule is deserialized via a helper, IntermediateSegmentRule, because of semantic ambiguity
58// of the bucketBy Reference field.
59//
60// SegmentRule implements Serialize directly without a helper because References can serialize
61// themselves without any ambiguity.
62#[skip_serializing_none]
63#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
64#[serde(rename_all = "camelCase", from = "IntermediateSegmentRule")]
65struct SegmentRule {
66    // Unique identifier provided by the LaunchDarkly backend for this rule.
67    id: Option<String>,
68    // The clauses that comprise this rule.
69    clauses: Vec<Clause>,
70    // A percentage rollout allowing only a subset of contexts to be included in this segment.
71    weight: Option<VariationWeight>,
72    // Which attribute should be used to distinguish between contexts in a rollout.
73    // Can be omitted; evaluation should treat absence as 'key'.
74    bucket_by: Option<Reference>,
75    // Only present when this segment rule is a rollout, i.e., only present when weight is present.
76    rollout_context_kind: Option<Kind>,
77}
78
79// SegmentRule is deserialized via IntermediateSegmentRule, taking advantage of
80// serde's untagged enum support.
81//
82// This is necessary because SegmentRules directly contain attribute references, specifically
83// the bucketBy field. References require care when deserializing; see the Reference documentation
84// for more info.
85//
86// Serde will attempt deserialization into the first enum variant, and if it fails, the second.
87// This implies deserialization will be relatively slower for the second variant.
88#[derive(Debug, Deserialize, PartialEq)]
89#[serde(untagged)]
90enum IntermediateSegmentRule {
91    // SegmentRuleWithKind must be listed first in the enum because otherwise SegmentRuleWithoutKind
92    // could match the input (by ignoring/discarding the rollout_context_kind field).
93    ContextAware(SegmentRuleWithKind),
94    ContextOblivious(SegmentRuleWithoutKind),
95}
96
97#[derive(Debug, Deserialize, PartialEq)]
98#[serde(rename_all = "camelCase")]
99struct SegmentRuleWithKind {
100    id: Option<String>,
101    clauses: Vec<Clause>,
102    weight: Option<VariationWeight>,
103    bucket_by: Option<Reference>,
104    rollout_context_kind: Kind,
105}
106
107#[derive(Debug, Deserialize, PartialEq)]
108#[serde(rename_all = "camelCase")]
109struct SegmentRuleWithoutKind {
110    id: Option<String>,
111    clauses: Vec<Clause>,
112    weight: Option<VariationWeight>,
113    bucket_by: Option<AttributeName>,
114}
115
116impl From<IntermediateSegmentRule> for SegmentRule {
117    fn from(rule: IntermediateSegmentRule) -> SegmentRule {
118        match rule {
119            IntermediateSegmentRule::ContextAware(fields) => SegmentRule {
120                id: fields.id,
121                clauses: fields.clauses,
122                weight: fields.weight,
123                // No transformation is necessary since ContextAware implies this
124                // data is using attribute references.
125                bucket_by: fields.bucket_by,
126                rollout_context_kind: Some(fields.rollout_context_kind),
127            },
128            IntermediateSegmentRule::ContextOblivious(fields) => SegmentRule {
129                id: fields.id,
130                clauses: fields.clauses,
131                weight: fields.weight,
132                // ContextOblivious implies this data is using literal attribute names, so
133                // the AttributeName must be converted to a Reference (if present).
134                bucket_by: fields.bucket_by.map(Reference::from),
135                rollout_context_kind: None,
136            },
137        }
138    }
139}
140
141impl Segment {
142    /// Determines if the provided context is a part of this segment.
143    ///
144    /// Inclusion can be determined by specifically listing the context key in the segment, or by
145    /// matching any of the rules configured for this segment.
146    pub(crate) fn contains(
147        &self,
148        context: &Context,
149        store: &dyn Store,
150        evaluation_stack: &mut EvaluationStack,
151    ) -> Result<bool, String> {
152        if evaluation_stack.segment_chain.contains(&self.key) {
153            return Err(format!("segment rule referencing segment {} caused a circular reference; this is probably a temporary condition due to an incomplete update", self.key));
154        }
155
156        evaluation_stack.segment_chain.insert(self.key.clone());
157
158        let mut does_contain = false;
159        if self.is_contained_in(context, &self.included, &self.included_contexts) {
160            does_contain = true;
161        } else if self.is_contained_in(context, &self.excluded, &self.excluded_contexts) {
162            does_contain = false;
163        } else {
164            for rule in &self.rules {
165                let matches =
166                    rule.matches(context, store, &self.key, &self.salt, evaluation_stack)?;
167                if matches {
168                    does_contain = true;
169                    break;
170                }
171            }
172        }
173
174        evaluation_stack.segment_chain.remove(&self.key);
175
176        Ok(does_contain)
177    }
178
179    fn is_contained_in(
180        &self,
181        context: &Context,
182        user_keys: &[String],
183        context_targets: &[SegmentTarget],
184    ) -> bool {
185        for target in context_targets {
186            if let Some(context) = context.as_kind(&target.context_kind) {
187                let key = context.key();
188                if target.values.iter().any(|v| v == key) {
189                    return true;
190                }
191            }
192        }
193
194        if let Some(context) = context.as_kind(&Kind::user()) {
195            return user_keys.contains(&context.key().to_string());
196        }
197
198        false
199    }
200
201    /// Retrieve the id representing this big segment.
202    ///
203    /// This id will either be the segment key if the segment isn't a big segment, or it will be a
204    /// combination of the segment key and the segment generation id.
205    pub fn unbounded_segment_id(&self) -> String {
206        match self.generation {
207            None | Some(0) => self.key.clone(),
208            Some(generation) => format!("{}.g{}", self.key, generation),
209        }
210    }
211}
212
213impl SegmentRule {
214    /// Determines if a context matches the provided segment rule.
215    ///
216    /// A context will match if all segment clauses match; otherwise, this method returns false.
217    pub fn matches(
218        &self,
219        context: &Context,
220        store: &dyn Store,
221        key: &str,
222        salt: &str,
223        evaluation_stack: &mut EvaluationStack,
224    ) -> Result<bool, String> {
225        // rules match if _all_ of their clauses do
226        for clause in &self.clauses {
227            let matches = clause.matches(context, store, evaluation_stack)?;
228            if !matches {
229                return Ok(false);
230            }
231        }
232
233        match self.weight {
234            Some(weight) if weight >= 0.0 => {
235                let prefix = BucketPrefix::KeyAndSalt(key, salt);
236                let (bucket, _) = context.bucket(
237                    &self.bucket_by,
238                    prefix,
239                    false,
240                    self.rollout_context_kind
241                        .as_ref()
242                        .unwrap_or(&Kind::default()),
243                )?;
244                Ok(bucket < weight / 100_000.0)
245            }
246            _ => Ok(true),
247        }
248    }
249}
250
251#[derive(Clone, Debug, Serialize, Deserialize)]
252#[serde(rename_all = "camelCase")]
253pub(crate) struct SegmentTarget {
254    values: Vec<String>,
255    context_kind: Kind,
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use crate::contexts::attribute_reference::Reference;
262    use crate::eval::evaluate;
263    use crate::{proptest_generators::*, AttributeValue, ContextBuilder, Flag, FlagValue, Store};
264    use assert_json_diff::assert_json_eq;
265    use proptest::{collection::vec, option::of, prelude::*};
266    use serde_json::json;
267
268    prop_compose! {
269        // Generate an arbitrary SegmentRule with 0-3 clauses
270        fn any_segment_rule()(
271            id in of(any::<String>()),
272            clauses in vec(any_clause(), 0..3),
273            weight in of(any::<f32>()),
274            // reference is any_ref(), rather than any_valid_ref(), because we also want
275            // coverage of invalid references.
276            bucket_by in any_ref(),
277            rollout_context_kind in any_kind()
278        ) -> SegmentRule {
279            SegmentRule {
280                id,
281                clauses,
282                weight,
283                bucket_by: Some(bucket_by),
284                rollout_context_kind: Some(rollout_context_kind),
285            }
286        }
287    }
288
289    #[test]
290    fn handles_contextless_schema() {
291        let json = &r#"{
292                "key": "segment",
293                "included": ["alice"],
294                "excluded": ["bob"],
295                "rules": [],
296                "salt": "salty",
297                "version": 1
298            }"#
299        .to_string();
300
301        let segment: Segment = serde_json::from_str(json).expect("Failed to parse segment");
302        assert_eq!(1, segment.included.len());
303        assert_eq!(1, segment.excluded.len());
304
305        assert!(segment.included_contexts.is_empty());
306        assert!(segment.excluded_contexts.is_empty());
307    }
308
309    #[test]
310    fn handles_unbounded_context_kind() {
311        let json = r#"{
312                "key": "segment",
313                "included": [],
314                "excluded": [],
315                "rules": [],
316                "salt": "salty",
317                "unbounded": true,
318                "unboundedContextKind": "org",
319                "generation": 2,
320                "version": 1
321            }"#;
322
323        let segment: Segment = serde_json::from_str(json).expect("Failed to parse segment");
324        assert!(segment.unbounded);
325        assert_eq!(segment.unbounded_context_kind, Some(Kind::from("org")));
326        assert_eq!(segment.generation, Some(2));
327    }
328
329    #[test]
330    fn unbounded_context_kind_defaults_to_none() {
331        let json = r#"{
332                "key": "segment",
333                "included": [],
334                "excluded": [],
335                "rules": [],
336                "salt": "salty",
337                "unbounded": true,
338                "version": 1
339            }"#;
340
341        let segment: Segment = serde_json::from_str(json).expect("Failed to parse segment");
342        assert!(segment.unbounded);
343        assert_eq!(segment.unbounded_context_kind, None);
344    }
345
346    #[test]
347    fn unbounded_context_kind_user() {
348        let json = r#"{
349                "key": "segment",
350                "included": [],
351                "excluded": [],
352                "rules": [],
353                "salt": "salty",
354                "unbounded": true,
355                "unboundedContextKind": "user",
356                "generation": 1,
357                "version": 1
358            }"#;
359
360        let segment: Segment = serde_json::from_str(json).expect("Failed to parse segment");
361        assert_eq!(segment.unbounded_context_kind, Some(Kind::user()));
362    }
363
364    #[test]
365    fn handles_context_schema() {
366        let json = &r#"{
367                "key": "segment",
368                "included": [],
369                "excluded": [],
370                "includedContexts": [{
371                    "values": ["alice", "bob"],
372                    "contextKind": "org"
373                }],
374                "excludedContexts": [{
375                    "values": ["cris", "darren"],
376                    "contextKind": "org"
377                }],
378                "rules": [],
379                "salt": "salty",
380                "version": 1
381            }"#
382        .to_string();
383
384        let segment: Segment = serde_json::from_str(json).expect("Failed to parse segment");
385        assert!(segment.included.is_empty());
386        assert!(segment.excluded.is_empty());
387
388        assert_eq!(1, segment.included_contexts.len());
389        assert_eq!(1, segment.excluded_contexts.len());
390    }
391
392    // Treat a Segment as a Store containing only itself
393    type TestStore = Segment;
394    impl Store for TestStore {
395        fn flag(&self, _flag_key: &str) -> Option<Flag> {
396            None
397        }
398        fn segment(&self, segment_key: &str) -> Option<Segment> {
399            if self.key == segment_key {
400                Some(self.clone())
401            } else {
402                None
403            }
404        }
405    }
406
407    fn assert_segment_match(segment: &Segment, context: Context, expected: bool) {
408        let store = segment as &TestStore;
409        let flag = Flag::new_boolean_flag_with_segment_match(vec![&segment.key], Kind::user());
410        let result = evaluate(store, &flag, &context, None);
411        assert_eq!(result.value, Some(&FlagValue::Bool(expected)));
412    }
413
414    fn new_segment() -> Segment {
415        Segment {
416            key: "segkey".to_string(),
417            included: vec![],
418            excluded: vec![],
419            included_contexts: vec![],
420            excluded_contexts: vec![],
421            rules: vec![],
422            salt: "salty".to_string(),
423            unbounded: false,
424            unbounded_context_kind: None,
425            generation: Some(1),
426            version: 1,
427        }
428    }
429
430    fn jane_rule(
431        weight: Option<f32>,
432        bucket_by: Option<Reference>,
433        kind: Option<Kind>,
434    ) -> SegmentRule {
435        SegmentRule {
436            id: None,
437            clauses: vec![Clause::new_match(
438                Reference::new("name"),
439                AttributeValue::String("Jane".to_string()),
440                Kind::user(),
441            )],
442            weight,
443            bucket_by,
444            rollout_context_kind: kind,
445        }
446    }
447
448    fn thirty_percent_rule(bucket_by: Option<Reference>, kind: Option<Kind>) -> SegmentRule {
449        SegmentRule {
450            id: None,
451            clauses: vec![Clause::new_match(
452                Reference::new("key"),
453                AttributeValue::String(".".to_string()),
454                Kind::user(),
455            )],
456            weight: Some(30_000.0),
457            bucket_by,
458            rollout_context_kind: kind,
459        }
460    }
461
462    #[test]
463    fn segment_rule_parse_only_required_field_is_clauses() {
464        let rule: SegmentRule =
465            serde_json::from_value(json!({"clauses": []})).expect("should parse");
466        assert_eq!(
467            rule,
468            SegmentRule {
469                id: None,
470                clauses: vec![],
471                weight: None,
472                bucket_by: None,
473                rollout_context_kind: None,
474            }
475        );
476    }
477
478    #[test]
479    fn segment_rule_serialize_omits_optional_fields() {
480        let json = json!({"clauses": []});
481        let rule: SegmentRule = serde_json::from_value(json.clone()).expect("should parse");
482        assert_json_eq!(json, rule);
483    }
484
485    proptest! {
486        #[test]
487        fn segment_rule_parse_references_as_literal_attribute_names_when_context_kind_omitted(
488                clause_attr in any_valid_ref_string(),
489                bucket_by in any_valid_ref_string()
490            ) {
491            let omit_context_kind: SegmentRule = serde_json::from_value(json!({
492                "id" : "test",
493                "clauses":[{
494                    "attribute": clause_attr,
495                    "negate": false,
496                    "op": "matches",
497                    "values": ["xyz"],
498                }],
499                "weight": 10000,
500                "bucketBy": bucket_by,
501            }))
502            .expect("should parse");
503
504             let empty_context_kind: SegmentRule = serde_json::from_value(json!({
505                "id" : "test",
506                "clauses":[{
507                    "attribute": clause_attr,
508                    "negate": false,
509                    "op": "matches",
510                    "values": ["xyz"],
511                    "contextKind" : "",
512                }],
513                "weight": 10000,
514                "bucketBy": bucket_by,
515            }))
516            .expect("should parse");
517
518            let expected = SegmentRule {
519                id: Some("test".into()),
520                clauses: vec![Clause::new_context_oblivious_match(
521                    Reference::from(AttributeName::new(clause_attr)),
522                    "xyz".into(),
523                )],
524                weight: Some(10_000.0),
525                bucket_by: Some(Reference::from(AttributeName::new(bucket_by))),
526                rollout_context_kind: None,
527            };
528
529            prop_assert_eq!(
530                omit_context_kind,
531                expected.clone()
532            );
533
534            prop_assert_eq!(
535                empty_context_kind,
536                expected
537            );
538        }
539    }
540
541    proptest! {
542        #[test]
543        fn segment_rule_parse_references_normally_when_context_kind_present(
544                clause_attr in any_ref(),
545                bucket_by in any_ref()
546            ) {
547            let rule: SegmentRule = serde_json::from_value(json!({
548                "id" : "test",
549                "clauses":[{
550                    "attribute": clause_attr.to_string(),
551                    "negate": false,
552                    "op": "matches",
553                    "values": ["xyz"],
554                    "contextKind" : "user"
555                }],
556                "weight": 10000,
557                "bucketBy": bucket_by.to_string(),
558                "rolloutContextKind" : "user"
559            }))
560            .expect("should parse");
561
562            prop_assert_eq!(
563                rule,
564                SegmentRule {
565                    id: Some("test".into()),
566                    clauses: vec![Clause::new_match(
567                        clause_attr,
568                        "xyz".into(),
569                        Kind::user()
570                    )],
571                    weight: Some(10_000.0),
572                    bucket_by: Some(bucket_by),
573                    rollout_context_kind: Some(Kind::user()),
574                }
575            );
576        }
577    }
578
579    proptest! {
580        #[test]
581        fn arbitrary_segment_rule_serialization_roundtrip(rule in any_segment_rule()) {
582            let json = serde_json::to_value(rule).expect("an arbitrary segment rule should serialize");
583            let parsed: SegmentRule = serde_json::from_value(json.clone()).expect("an arbitrary segment rule should parse");
584            assert_json_eq!(json, parsed);
585        }
586    }
587
588    #[test]
589    fn segment_match_clause_falls_through_if_segment_not_found() {
590        let mut segment = new_segment();
591        segment.included.push("foo".to_string());
592        segment.included_contexts.push(SegmentTarget {
593            values: vec![],
594            context_kind: Kind::user(),
595        });
596        segment.key = "different-key".to_string();
597        let context = ContextBuilder::new("foo").build().unwrap();
598        assert_segment_match(&segment, context, true);
599    }
600
601    #[test]
602    fn can_match_just_one_segment_from_list() {
603        let mut segment = new_segment();
604        segment.included.push("foo".to_string());
605        segment.included_contexts.push(SegmentTarget {
606            values: vec![],
607            context_kind: Kind::user(),
608        });
609        let context = ContextBuilder::new("foo").build().unwrap();
610        let flag = Flag::new_boolean_flag_with_segment_match(
611            vec!["different-segkey", "segkey", "another-segkey"],
612            Kind::user(),
613        );
614        let result = evaluate(&segment, &flag, &context, None);
615        assert_eq!(result.value, Some(&FlagValue::Bool(true)));
616    }
617
618    #[test]
619    fn user_is_explicitly_included_in_segment() {
620        let mut segment = new_segment();
621        segment.included.push("foo".to_string());
622        segment.included.push("bar".to_string());
623        segment.included_contexts.push(SegmentTarget {
624            values: vec![],
625            context_kind: Kind::user(),
626        });
627        let context = ContextBuilder::new("bar").build().unwrap();
628        assert_segment_match(&segment, context, true);
629    }
630
631    proptest! {
632        #[test]
633        fn user_is_matched_by_segment_rule(kind in of(Just(Kind::user()))) {
634            let mut segment = new_segment();
635            segment.rules.push(jane_rule(None, None, kind));
636            let jane = ContextBuilder::new("foo").name("Jane").build().unwrap();
637            let joan = ContextBuilder::new("foo").name("Joan").build().unwrap();
638            assert_segment_match(&segment, jane, true);
639            assert_segment_match(&segment, joan, false);
640        }
641    }
642
643    proptest! {
644        #[test]
645        fn user_is_explicitly_excluded_from_segment(kind in of(Just(Kind::user()))) {
646            let mut segment = new_segment();
647            segment.rules.push(jane_rule(None, None, kind));
648            segment.excluded.push("foo".to_string());
649            segment.excluded.push("bar".to_string());
650            segment.excluded_contexts.push(SegmentTarget {
651                values: vec![],
652                context_kind: Kind::user(),
653            });
654            let jane = ContextBuilder::new("foo").name("Jane").build().unwrap();
655            assert_segment_match(&segment, jane, false);
656        }
657    }
658
659    #[test]
660    fn segment_includes_override_excludes() {
661        let mut segment = new_segment();
662        segment.included.push("bar".to_string());
663        segment.included_contexts.push(SegmentTarget {
664            values: vec![],
665            context_kind: Kind::user(),
666        });
667        segment.excluded.push("foo".to_string());
668        segment.excluded.push("bar".to_string());
669        segment.excluded_contexts.push(SegmentTarget {
670            values: vec![],
671            context_kind: Kind::user(),
672        });
673        let context = ContextBuilder::new("bar").build().unwrap();
674        assert_segment_match(&segment, context, true);
675    }
676
677    #[test]
678    fn user_is_explicitly_included_in_context_match() {
679        let mut segment = new_segment();
680        segment.included_contexts.push(SegmentTarget {
681            values: vec!["foo".to_string()],
682            context_kind: Kind::user(),
683        });
684        segment.included_contexts.push(SegmentTarget {
685            values: vec!["bar".to_string()],
686            context_kind: Kind::user(),
687        });
688        let context = ContextBuilder::new("bar").build().unwrap();
689        assert_segment_match(&segment, context, true);
690    }
691
692    #[test]
693    fn segment_include_target_does_not_match_with_mismatched_context() {
694        let mut segment = new_segment();
695        segment.included_contexts.push(SegmentTarget {
696            values: vec!["bar".to_string()],
697            context_kind: Kind::from("org"),
698        });
699        let context = ContextBuilder::new("bar").build().unwrap();
700        assert_segment_match(&segment, context, false);
701    }
702
703    proptest! {
704        #[test]
705        fn user_is_explicitly_excluded_in_context_match(kind in of(Just(Kind::user()))) {
706            let mut segment = new_segment();
707            segment.rules.push(jane_rule(None, None, kind));
708            segment.excluded_contexts.push(SegmentTarget {
709                values: vec!["foo".to_string()],
710                context_kind: Kind::user(),
711            });
712            segment.excluded_contexts.push(SegmentTarget {
713                values: vec!["bar".to_string()],
714                context_kind: Kind::user(),
715            });
716            let jane = ContextBuilder::new("foo").name("Jane").build().unwrap();
717            assert_segment_match(&segment, jane, false);
718        }
719
720        #[test]
721        fn segment_does_not_match_if_no_includes_or_rules_match(kind in of(Just(Kind::user()))) {
722            let mut segment = new_segment();
723            segment.rules.push(jane_rule(None, None, kind));
724            segment.included.push("key".to_string());
725            let context = ContextBuilder::new("other-key")
726                .name("Bob")
727                .build()
728                .unwrap();
729            assert_segment_match(&segment, context, false);
730        }
731
732        #[test]
733        fn segment_rule_can_match_user_with_percentage_rollout(kind in of(Just(Kind::user()))) {
734            let mut segment = new_segment();
735            segment.rules.push(jane_rule(Some(99_999.0), None, kind));
736            let context = ContextBuilder::new("key").name("Jane").build().unwrap();
737            assert_segment_match(&segment, context, true);
738        }
739
740        #[test]
741        fn segment_rule_can_not_match_user_with_percentage_rollout(kind in of(Just(Kind::user()))) {
742            let mut segment = new_segment();
743            segment.rules.push(jane_rule(Some(1.0), None, kind));
744            let context = ContextBuilder::new("key").name("Jane").build().unwrap();
745            assert_segment_match(&segment, context, false);
746        }
747
748        #[test]
749        fn segment_rule_can_have_percentage_rollout(kind in of(Just(Kind::user()))) {
750            let mut segment = new_segment();
751            segment.rules.push(thirty_percent_rule(None, kind));
752
753            let context_a = ContextBuilder::new("userKeyA").build().unwrap(); // bucket 0.14574753
754            let context_z = ContextBuilder::new("userKeyZ").build().unwrap(); // bucket 0.45679215
755            assert_segment_match(&segment, context_a, true);
756            assert_segment_match(&segment, context_z, false);
757        }
758
759        #[test]
760        fn segment_rule_can_have_percentage_rollout_by_any_attribute(kind in of(Just(Kind::user()))) {
761            let mut segment = new_segment();
762            segment
763                .rules
764                .push(thirty_percent_rule(Some(Reference::new("name")), kind));
765            let context_a = ContextBuilder::new("x").name("userKeyA").build().unwrap(); // bucket 0.14574753
766            let context_z = ContextBuilder::new("x").name("userKeyZ").build().unwrap(); // bucket 0.45679215
767            assert_segment_match(&segment, context_a, true);
768            assert_segment_match(&segment, context_z, false);
769        }
770    }
771
772    #[test]
773    fn unbounded_context_kind_accessor_returns_none_when_unset() {
774        let segment = new_segment();
775        assert_eq!(segment.unbounded_context_kind, None);
776    }
777}