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 unbounded_context_kind: Option<Kind>,
40 #[serde(default)]
41 generation: Option<i64>,
42
43 pub version: u64,
46}
47
48impl Versioned for Segment {
49 fn version(&self) -> u64 {
50 self.version
51 }
52}
53
54#[skip_serializing_none]
61#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
62#[serde(rename_all = "camelCase", from = "IntermediateSegmentRule")]
63struct SegmentRule {
64 id: Option<String>,
66 clauses: Vec<Clause>,
68 weight: Option<VariationWeight>,
70 bucket_by: Option<Reference>,
73 rollout_context_kind: Option<Kind>,
75}
76
77#[derive(Debug, Deserialize, PartialEq)]
87#[serde(untagged)]
88enum IntermediateSegmentRule {
89 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 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 bucket_by: fields.bucket_by.map(Reference::from),
133 rollout_context_kind: None,
134 },
135 }
136 }
137}
138
139impl Segment {
140 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 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 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 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 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 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 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(); let context_z = ContextBuilder::new("userKeyZ").build().unwrap(); 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(); let context_z = ContextBuilder::new("x").name("userKeyZ").build().unwrap(); assert_segment_match(&segment, context_a, true);
766 assert_segment_match(&segment, context_z, false);
767 }
768 }
769}