1use std::convert::TryFrom;
2use std::fmt;
3
4use log::warn;
5use serde::de::{MapAccess, Visitor};
6use serde::{
7 ser::{SerializeMap, SerializeStruct},
8 Deserialize, Deserializer, Serialize, Serializer,
9};
10
11use crate::contexts::context::Kind;
12use crate::eval::{self, Detail, Reason};
13use crate::flag_value::FlagValue;
14use crate::rule::FlagRule;
15use crate::variation::{VariationIndex, VariationOrRollout};
16use crate::{BucketResult, Context, Versioned};
17
18#[derive(Clone, Debug, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub struct Flag {
22 pub key: String,
24
25 #[serde(default)]
28 pub version: u64,
29
30 pub(crate) on: bool,
31
32 pub(crate) targets: Vec<Target>,
33
34 #[serde(default)]
35 pub(crate) context_targets: Vec<Target>,
36 pub(crate) rules: Vec<FlagRule>,
37 pub(crate) prerequisites: Vec<Prereq>,
38
39 pub(crate) fallthrough: VariationOrRollout,
40 pub(crate) off_variation: Option<VariationIndex>,
41 variations: Vec<FlagValue>,
42
43 #[serde(flatten)]
45 client_visibility: ClientVisibility,
46
47 salt: String,
48
49 #[serde(default)]
58 pub track_events: bool,
59
60 #[serde(default)]
70 pub track_events_fallthrough: bool,
71
72 #[serde(default)]
82 pub debug_events_until_date: Option<u64>,
83
84 #[serde(
87 default,
88 rename = "migration",
89 skip_serializing_if = "is_default_migration_settings"
90 )]
91 pub migration_settings: Option<MigrationFlagParameters>,
92
93 #[serde(default, skip_serializing_if = "is_default_ratio")]
99 pub sampling_ratio: Option<u32>,
100
101 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
106 pub exclude_from_summaries: bool,
107}
108
109impl Versioned for Flag {
110 fn version(&self) -> u64 {
111 self.version
112 }
113}
114
115fn is_default_ratio(sampling_ratio: &Option<u32>) -> bool {
117 sampling_ratio.unwrap_or(1) == 1
118}
119
120fn is_default_migration_settings(settings: &Option<MigrationFlagParameters>) -> bool {
122 match settings {
123 Some(settings) => settings.is_default(),
124 None => true,
125 }
126}
127
128#[derive(Clone, Debug, Serialize, Deserialize)]
130#[serde(rename_all = "camelCase")]
131pub struct MigrationFlagParameters {
132 #[serde(skip_serializing_if = "is_default_ratio")]
136 pub check_ratio: Option<u32>,
137}
138
139impl MigrationFlagParameters {
140 fn is_default(&self) -> bool {
141 is_default_ratio(&self.check_ratio)
142 }
143}
144
145#[derive(Clone, Debug)]
146struct ClientVisibility {
147 client_side_availability: ClientSideAvailability,
148}
149
150impl<'de> Deserialize<'de> for ClientVisibility {
151 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
152 where
153 D: Deserializer<'de>,
154 {
155 #[derive(Deserialize)]
156 #[serde(field_identifier, rename_all = "camelCase")]
157 enum Field {
158 ClientSide,
159 ClientSideAvailability,
160 }
161
162 struct ClientVisibilityVisitor;
163
164 impl<'de> Visitor<'de> for ClientVisibilityVisitor {
165 type Value = ClientVisibility;
166
167 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
168 formatter.write_str("struct ClientVisibility")
169 }
170
171 fn visit_map<V>(self, mut map: V) -> Result<ClientVisibility, V::Error>
172 where
173 V: MapAccess<'de>,
174 {
175 let mut client_side = None;
176 let mut client_side_availability: Option<ClientSideAvailability> = None;
177
178 while let Some(k) = map.next_key()? {
179 match k {
180 Field::ClientSide => client_side = Some(map.next_value()?),
181 Field::ClientSideAvailability => {
182 client_side_availability = Some(map.next_value()?)
183 }
184 }
185 }
186
187 let client_side_availability = match client_side_availability {
188 Some(mut csa) => {
189 csa.explicit = true;
190 csa
191 }
192 _ => ClientSideAvailability {
193 using_environment_id: client_side.unwrap_or_default(),
194 using_mobile_key: true,
195 explicit: false,
196 },
197 };
198
199 Ok(ClientVisibility {
200 client_side_availability,
201 })
202 }
203 }
204
205 const FIELDS: &[&str] = &["clientSide", "clientSideAvailability"];
206 deserializer.deserialize_struct("ClientVisibility", FIELDS, ClientVisibilityVisitor)
207 }
208}
209
210impl Serialize for ClientVisibility {
211 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
212 where
213 S: Serializer,
214 {
215 if self.client_side_availability.explicit {
216 let mut state = serializer.serialize_struct("ClientSideAvailability", 1)?;
217 state.serialize_field("clientSideAvailability", &self.client_side_availability)?;
218 state.end()
219 } else {
220 let mut map = serializer.serialize_map(Some(1))?;
221 map.serialize_entry(
222 "clientSide",
223 &self.client_side_availability.using_environment_id,
224 )?;
225 map.end()
226 }
227 }
228}
229
230#[derive(Clone, Debug, Serialize, Deserialize)]
235pub struct Prereq {
236 pub(crate) key: String,
237 pub(crate) variation: VariationIndex,
238}
239
240#[derive(Clone, Debug, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub(crate) struct Target {
243 #[serde(default)]
244 pub(crate) context_kind: Kind,
245
246 pub(crate) values: Vec<String>,
247 pub(crate) variation: VariationIndex,
248}
249
250#[derive(Clone, Debug, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase")]
256pub struct ClientSideAvailability {
257 pub using_mobile_key: bool,
260 pub using_environment_id: bool,
263
264 #[serde(skip)]
269 explicit: bool,
270}
271
272impl Flag {
273 pub fn variation(&self, index: VariationIndex, reason: Reason) -> Detail<&FlagValue> {
275 let (value, variation_index) = match usize::try_from(index) {
276 Ok(u) => (self.variations.get(u), Some(index)),
277 Err(e) => {
278 warn!(
279 "Flag variation index could not be converted to usize. {}",
280 e
281 );
282 (None, None)
283 }
284 };
285
286 Detail {
287 value,
288 variation_index,
289 reason,
290 }
291 .should_have_value(eval::Error::MalformedFlag)
292 }
293
294 pub fn off_value(&self, reason: Reason) -> Detail<&FlagValue> {
300 match self.off_variation {
301 Some(index) => self.variation(index, reason),
302 None => Detail::empty(reason),
303 }
304 }
305
306 pub fn using_environment_id(&self) -> bool {
309 self.client_visibility
310 .client_side_availability
311 .using_environment_id
312 }
313
314 pub fn using_mobile_key(&self) -> bool {
317 self.client_visibility
318 .client_side_availability
319 .using_mobile_key
320 }
321
322 pub(crate) fn resolve_variation_or_rollout(
323 &self,
324 vr: &VariationOrRollout,
325 context: &Context,
326 ) -> Result<BucketResult, eval::Error> {
327 vr.variation(&self.key, context, &self.salt)
328 .map_err(|_| eval::Error::MalformedFlag)?
329 .ok_or(eval::Error::MalformedFlag)
330 }
331
332 pub fn is_experimentation_enabled(&self, reason: &Reason) -> bool {
337 match reason {
338 _ if reason.is_in_experiment() => true,
339 Reason::Fallthrough { .. } => self.track_events_fallthrough,
340 Reason::RuleMatch { rule_index, .. } => self
341 .rules
342 .get(*rule_index)
343 .map(|rule| rule.track_events)
344 .unwrap_or(false),
345 _ => false,
346 }
347 }
348
349 #[cfg(test)]
350 pub(crate) fn new_boolean_flag_with_segment_match(segment_keys: Vec<&str>, kind: Kind) -> Self {
351 Self {
352 key: "feature".to_string(),
353 version: 1,
354 on: true,
355 targets: vec![],
356 rules: vec![FlagRule::new_segment_match(segment_keys, kind)],
357 prerequisites: vec![],
358 fallthrough: VariationOrRollout::Variation { variation: 0 },
359 off_variation: Some(0),
360 variations: vec![FlagValue::Bool(false), FlagValue::Bool(true)],
361 client_visibility: ClientVisibility {
362 client_side_availability: ClientSideAvailability {
363 using_mobile_key: false,
364 using_environment_id: false,
365 explicit: true,
366 },
367 },
368 salt: "xyz".to_string(),
369 track_events: false,
370 track_events_fallthrough: false,
371 debug_events_until_date: None,
372 context_targets: vec![],
373 migration_settings: None,
374 sampling_ratio: None,
375 exclude_from_summaries: false,
376 }
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use crate::store::Store;
383 use crate::test_common::TestStore;
384 use crate::MigrationFlagParameters;
385 use spectral::prelude::*;
386
387 use super::Flag;
388 use crate::eval::Reason::*;
389 use test_case::test_case;
390
391 #[test_case(true)]
392 #[test_case(false)]
393 fn handles_client_side_schema(client_side: bool) {
394 let json = &format!(
395 r#"{{
396 "key": "flag",
397 "version": 42,
398 "on": false,
399 "targets": [],
400 "rules": [],
401 "prerequisites": [],
402 "fallthrough": {{"variation": 1}},
403 "offVariation": 0,
404 "variations": [false, true],
405 "clientSide": {},
406 "salt": "salty"
407 }}"#,
408 client_side
409 );
410
411 let flag: Flag = serde_json::from_str(json).unwrap();
412 let client_side_availability = &flag.client_visibility.client_side_availability;
413 assert_eq!(client_side_availability.using_environment_id, client_side);
414 assert!(client_side_availability.using_mobile_key);
415 assert!(!client_side_availability.explicit);
416
417 assert_eq!(flag.using_environment_id(), client_side);
418 }
419
420 #[test_case(true)]
421 #[test_case(false)]
422 fn can_deserialize_and_reserialize_to_old_schema(client_side: bool) {
423 let json = &format!(
424 r#"{{
425 "key": "flag",
426 "version": 42,
427 "on": false,
428 "targets": [],
429 "contextTargets": [],
430 "rules": [],
431 "prerequisites": [],
432 "fallthrough": {{
433 "variation": 1
434 }},
435 "offVariation": 0,
436 "variations": [
437 false,
438 true
439 ],
440 "clientSide": {},
441 "salt": "salty",
442 "trackEvents": false,
443 "trackEventsFallthrough": false,
444 "debugEventsUntilDate": null
445}}"#,
446 client_side
447 );
448
449 let flag: Flag = serde_json::from_str(json).unwrap();
450 let restored = serde_json::to_string_pretty(&flag).unwrap();
451
452 assert_eq!(json, &restored);
453 }
454
455 #[test_case(true)]
456 #[test_case(false)]
457 fn handles_client_side_availability_schema(using_environment_id: bool) {
458 let json = &format!(
459 r#"{{
460 "key": "flag",
461 "version": 42,
462 "on": false,
463 "targets": [],
464 "rules": [],
465 "prerequisites": [],
466 "fallthrough": {{"variation": 1}},
467 "offVariation": 0,
468 "variations": [false, true],
469 "clientSideAvailability": {{
470 "usingEnvironmentId": {},
471 "usingMobileKey": false
472 }},
473 "salt": "salty"
474 }}"#,
475 using_environment_id
476 );
477
478 let flag: Flag = serde_json::from_str(json).unwrap();
479 let client_side_availability = &flag.client_visibility.client_side_availability;
480 assert_eq!(
481 client_side_availability.using_environment_id,
482 using_environment_id
483 );
484 assert!(!client_side_availability.using_mobile_key);
485 assert!(client_side_availability.explicit);
486
487 assert_eq!(flag.using_environment_id(), using_environment_id);
488 }
489
490 #[test_case(true)]
491 #[test_case(false)]
492 fn handles_context_target_schema(using_environment_id: bool) {
493 let json = &format!(
494 r#"{{
495 "key": "flag",
496 "version": 42,
497 "on": false,
498 "targets": [{{
499 "values": ["Bob"],
500 "variation": 1
501 }}],
502 "contextTargets": [{{
503 "contextKind": "org",
504 "values": ["LaunchDarkly"],
505 "variation": 0
506 }}],
507 "rules": [],
508 "prerequisites": [],
509 "fallthrough": {{"variation": 1}},
510 "offVariation": 0,
511 "variations": [false, true],
512 "clientSideAvailability": {{
513 "usingEnvironmentId": {},
514 "usingMobileKey": false
515 }},
516 "salt": "salty"
517 }}"#,
518 using_environment_id
519 );
520
521 let flag: Flag = serde_json::from_str(json).unwrap();
522 assert_eq!(1, flag.targets.len());
523 assert!(flag.targets[0].context_kind.is_user());
524
525 assert_eq!(1, flag.context_targets.len());
526 assert_eq!("org", flag.context_targets[0].context_kind.as_ref());
527 }
528
529 #[test]
530 fn getting_variation_with_invalid_index_is_handled_appropriately() {
531 let store = TestStore::new();
532 let flag = store.flag("flag").unwrap();
533
534 let detail = flag.variation(-1, Off);
535
536 assert!(detail.value.is_none());
537 assert!(detail.variation_index.is_none());
538 assert_eq!(
539 detail.reason,
540 Error {
541 error: crate::Error::MalformedFlag
542 }
543 );
544 }
545
546 #[test_case(true, true)]
547 #[test_case(true, false)]
548 #[test_case(false, true)]
549 #[test_case(false, false)]
550 fn can_deserialize_and_reserialize_to_new_schema(
551 using_environment_id: bool,
552 using_mobile_key: bool,
553 ) {
554 let json = &format!(
555 r#"{{
556 "key": "flag",
557 "version": 42,
558 "on": false,
559 "targets": [],
560 "contextTargets": [],
561 "rules": [],
562 "prerequisites": [],
563 "fallthrough": {{
564 "variation": 1
565 }},
566 "offVariation": 0,
567 "variations": [
568 false,
569 true
570 ],
571 "clientSideAvailability": {{
572 "usingMobileKey": {},
573 "usingEnvironmentId": {}
574 }},
575 "salt": "salty",
576 "trackEvents": false,
577 "trackEventsFallthrough": false,
578 "debugEventsUntilDate": null
579}}"#,
580 using_environment_id, using_mobile_key
581 );
582
583 let flag: Flag = serde_json::from_str(json).unwrap();
584 let restored = serde_json::to_string_pretty(&flag).unwrap();
585
586 assert_eq!(json, &restored);
587 }
588
589 #[test]
590 fn is_experimentation_enabled() {
591 let store = TestStore::new();
592
593 let flag = store.flag("flag").unwrap();
594 asserting!("defaults to false")
595 .that(&flag.is_experimentation_enabled(&Off))
596 .is_false();
597 asserting!("false for fallthrough if trackEventsFallthrough is false")
598 .that(&flag.is_experimentation_enabled(&Fallthrough {
599 in_experiment: false,
600 }))
601 .is_false();
602
603 let flag = store.flag("flagWithRuleExclusion").unwrap();
604 asserting!("true for fallthrough if trackEventsFallthrough is true")
605 .that(&flag.is_experimentation_enabled(&Fallthrough {
606 in_experiment: false,
607 }))
608 .is_true();
609 asserting!("true for rule if rule.trackEvents is true")
610 .that(&flag.is_experimentation_enabled(&RuleMatch {
611 rule_index: 0,
612 rule_id: flag.rules.first().unwrap().id.clone(),
613 in_experiment: false,
614 }))
615 .is_true();
616
617 let flag = store.flag("flagWithExperiment").unwrap();
618 asserting!("true for fallthrough if reason says it is")
619 .that(&flag.is_experimentation_enabled(&Fallthrough {
620 in_experiment: true,
621 }))
622 .is_true();
623 asserting!("false for fallthrough if reason says it is")
624 .that(&flag.is_experimentation_enabled(&Fallthrough {
625 in_experiment: false,
626 }))
627 .is_false();
628 asserting!("true for rule if reason says it is")
630 .that(&flag.is_experimentation_enabled(&RuleMatch {
631 rule_index: 42,
632 rule_id: "lol".into(),
633 in_experiment: true,
634 }))
635 .is_true();
636 asserting!("false for rule if reason says it is")
637 .that(&flag.is_experimentation_enabled(&RuleMatch {
638 rule_index: 42,
639 rule_id: "lol".into(),
640 in_experiment: false,
641 }))
642 .is_false();
643 }
644
645 #[test]
646 fn sampling_ratio_is_ignored_appropriately() {
647 let store = TestStore::new();
648 let mut flag = store.flag("flag").unwrap();
649
650 flag.sampling_ratio = Some(42);
651 let with_low_sampling_ratio = serde_json::to_string_pretty(&flag).unwrap();
652 assert!(with_low_sampling_ratio.contains("\"samplingRatio\": 42"));
653
654 flag.sampling_ratio = Some(1);
655 let with_highest_ratio = serde_json::to_string_pretty(&flag).unwrap();
656 assert!(!with_highest_ratio.contains("\"samplingRatio\""));
657
658 flag.sampling_ratio = None;
659 let with_no_ratio = serde_json::to_string_pretty(&flag).unwrap();
660 assert!(!with_no_ratio.contains("\"samplingRatio\""));
661 }
662
663 #[test]
664 fn exclude_from_summaries_is_ignored_appropriately() {
665 let store = TestStore::new();
666 let mut flag = store.flag("flag").unwrap();
667
668 flag.exclude_from_summaries = true;
669 let with_exclude = serde_json::to_string_pretty(&flag).unwrap();
670 assert!(with_exclude.contains("\"excludeFromSummaries\": true"));
671
672 flag.exclude_from_summaries = false;
673 let without_exclude = serde_json::to_string_pretty(&flag).unwrap();
674 assert!(!without_exclude.contains("\"excludeFromSummaries\""));
675 }
676
677 #[test]
678 fn migration_settings_included_appropriately() {
679 let store = TestStore::new();
680 let mut flag = store.flag("flag").unwrap();
681
682 flag.migration_settings = None;
683 let without_migration_settings = serde_json::to_string_pretty(&flag).unwrap();
684 assert!(!without_migration_settings.contains("\"migration\""));
685
686 flag.migration_settings = Some(MigrationFlagParameters { check_ratio: None });
687 let without_empty_migration_settings = serde_json::to_string_pretty(&flag).unwrap();
688 assert!(!without_empty_migration_settings.contains("\"migration\""));
689
690 flag.migration_settings = Some(MigrationFlagParameters {
691 check_ratio: Some(1),
692 });
693 let with_default_ratio = serde_json::to_string_pretty(&flag).unwrap();
694 assert!(!with_default_ratio.contains("\"migration\""));
695
696 flag.migration_settings = Some(MigrationFlagParameters {
697 check_ratio: Some(42),
698 });
699 let with_specific_ratio = serde_json::to_string_pretty(&flag).unwrap();
700 assert!(with_specific_ratio.contains("\"migration\": {"));
701 assert!(with_specific_ratio.contains("\"checkRatio\": 42"));
702 }
703}