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