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