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 generation: Option<i64>,
40
41 pub version: u64,
44}
45
46impl Versioned for Segment {
47 fn version(&self) -> u64 {
48 self.version
49 }
50}
51
52#[skip_serializing_none]
59#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
60#[serde(rename_all = "camelCase", from = "IntermediateSegmentRule")]
61struct SegmentRule {
62 id: Option<String>,
64 clauses: Vec<Clause>,
66 weight: Option<VariationWeight>,
68 bucket_by: Option<Reference>,
71 rollout_context_kind: Option<Kind>,
73}
74
75#[derive(Debug, Deserialize, PartialEq)]
85#[serde(untagged)]
86enum IntermediateSegmentRule {
87 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 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 bucket_by: fields.bucket_by.map(Reference::from),
131 rollout_context_kind: None,
132 },
133 }
134 }
135}
136
137impl Segment {
138 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 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 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 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 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 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 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(); let context_z = ContextBuilder::new("userKeyZ").build().unwrap(); 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(); let context_z = ContextBuilder::new("x").name("userKeyZ").build().unwrap(); assert_segment_match(&segment, context_a, true);
708 assert_segment_match(&segment, context_z, false);
709 }
710 }
711}