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#[derive(Clone, Debug, Default, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct Segment {
14 pub key: String,
16 pub included: Vec<String>,
18 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 #[serde(default)]
37 pub unbounded: bool,
38 #[serde(default)]
39 pub unbounded_context_kind: Option<Kind>,
42 #[serde(default)]
43 generation: Option<i64>,
44
45 pub version: u64,
48}
49
50impl Versioned for Segment {
51 fn version(&self) -> u64 {
52 self.version
53 }
54}
55
56#[skip_serializing_none]
63#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
64#[serde(rename_all = "camelCase", from = "IntermediateSegmentRule")]
65struct SegmentRule {
66 id: Option<String>,
68 clauses: Vec<Clause>,
70 weight: Option<VariationWeight>,
72 bucket_by: Option<Reference>,
75 rollout_context_kind: Option<Kind>,
77}
78
79#[derive(Debug, Deserialize, PartialEq)]
89#[serde(untagged)]
90enum IntermediateSegmentRule {
91 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 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 bucket_by: fields.bucket_by.map(Reference::from),
135 rollout_context_kind: None,
136 },
137 }
138 }
139}
140
141impl Segment {
142 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 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 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 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 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 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 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(); let context_z = ContextBuilder::new("userKeyZ").build().unwrap(); 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(); let context_z = ContextBuilder::new("x").name("userKeyZ").build().unwrap(); 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}