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 pub(crate) variations: Vec<FlagValue>,
42
43 #[serde(flatten)]
45 pub(crate) client_visibility: ClientVisibility,
46
47 pub(crate) 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, Default)]
146pub(crate) struct ClientVisibility {
147 pub(crate) 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")]
256#[derive(Default)]
257pub struct ClientSideAvailability {
258 pub using_mobile_key: bool,
261 pub using_environment_id: bool,
264
265 #[serde(skip)]
270 explicit: bool,
271}
272
273impl Flag {
274 pub fn variation(&self, index: VariationIndex, reason: Reason) -> Detail<&FlagValue> {
276 let (value, variation_index) = match usize::try_from(index) {
277 Ok(u) => (self.variations.get(u), Some(index)),
278 Err(e) => {
279 warn!("Flag variation index could not be converted to usize. {e}");
280 (None, None)
281 }
282 };
283
284 Detail {
285 value,
286 variation_index,
287 reason,
288 }
289 .should_have_value(eval::Error::MalformedFlag)
290 }
291
292 pub fn off_value(&self, reason: Reason) -> Detail<&FlagValue> {
298 match self.off_variation {
299 Some(index) => self.variation(index, reason),
300 None => Detail::empty(reason),
301 }
302 }
303
304 pub fn using_environment_id(&self) -> bool {
307 self.client_visibility
308 .client_side_availability
309 .using_environment_id
310 }
311
312 pub fn using_mobile_key(&self) -> bool {
315 self.client_visibility
316 .client_side_availability
317 .using_mobile_key
318 }
319
320 pub(crate) fn resolve_variation_or_rollout(
321 &self,
322 vr: &VariationOrRollout,
323 context: &Context,
324 ) -> Result<BucketResult, eval::Error> {
325 vr.variation(&self.key, context, &self.salt)
326 .map_err(|_| eval::Error::MalformedFlag)?
327 .ok_or(eval::Error::MalformedFlag)
328 }
329
330 pub fn is_experimentation_enabled(&self, reason: &Reason) -> bool {
335 match reason {
336 _ if reason.is_in_experiment() => true,
337 Reason::Fallthrough { .. } => self.track_events_fallthrough,
338 Reason::RuleMatch { rule_index, .. } => self
339 .rules
340 .get(*rule_index)
341 .map(|rule| rule.track_events)
342 .unwrap_or(false),
343 _ => false,
344 }
345 }
346
347 #[cfg(test)]
348 pub(crate) fn new_boolean_flag_with_segment_match(segment_keys: Vec<&str>, kind: Kind) -> Self {
349 Self {
350 key: "feature".to_string(),
351 version: 1,
352 on: true,
353 targets: vec![],
354 rules: vec![FlagRule::new_segment_match(segment_keys, kind)],
355 prerequisites: vec![],
356 fallthrough: VariationOrRollout::Variation { variation: 0 },
357 off_variation: Some(0),
358 variations: vec![FlagValue::Bool(false), FlagValue::Bool(true)],
359 client_visibility: ClientVisibility {
360 client_side_availability: ClientSideAvailability {
361 using_mobile_key: false,
362 using_environment_id: false,
363 explicit: true,
364 },
365 },
366 salt: "xyz".to_string(),
367 track_events: false,
368 track_events_fallthrough: false,
369 debug_events_until_date: None,
370 context_targets: vec![],
371 migration_settings: None,
372 sampling_ratio: None,
373 exclude_from_summaries: false,
374 }
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use crate::store::Store;
381 use crate::test_common::TestStore;
382 use crate::MigrationFlagParameters;
383 use spectral::prelude::*;
384
385 use super::Flag;
386 use crate::eval::Reason::*;
387 use test_case::test_case;
388
389 #[test_case(true)]
390 #[test_case(false)]
391 fn handles_client_side_schema(client_side: bool) {
392 let json = &format!(
393 r#"{{
394 "key": "flag",
395 "version": 42,
396 "on": false,
397 "targets": [],
398 "rules": [],
399 "prerequisites": [],
400 "fallthrough": {{"variation": 1}},
401 "offVariation": 0,
402 "variations": [false, true],
403 "clientSide": {client_side},
404 "salt": "salty"
405 }}"#
406 );
407
408 let flag: Flag = serde_json::from_str(json).unwrap();
409 let client_side_availability = &flag.client_visibility.client_side_availability;
410 assert_eq!(client_side_availability.using_environment_id, client_side);
411 assert!(client_side_availability.using_mobile_key);
412 assert!(!client_side_availability.explicit);
413
414 assert_eq!(flag.using_environment_id(), client_side);
415 }
416
417 #[test_case(true)]
418 #[test_case(false)]
419 fn can_deserialize_and_reserialize_to_old_schema(client_side: bool) {
420 let json = &format!(
421 r#"{{
422 "key": "flag",
423 "version": 42,
424 "on": false,
425 "targets": [],
426 "contextTargets": [],
427 "rules": [],
428 "prerequisites": [],
429 "fallthrough": {{
430 "variation": 1
431 }},
432 "offVariation": 0,
433 "variations": [
434 false,
435 true
436 ],
437 "clientSide": {client_side},
438 "salt": "salty",
439 "trackEvents": false,
440 "trackEventsFallthrough": false,
441 "debugEventsUntilDate": null
442}}"#
443 );
444
445 let flag: Flag = serde_json::from_str(json).unwrap();
446 let restored = serde_json::to_string_pretty(&flag).unwrap();
447
448 assert_eq!(json, &restored);
449 }
450
451 #[test_case(true)]
452 #[test_case(false)]
453 fn handles_client_side_availability_schema(using_environment_id: bool) {
454 let json = &format!(
455 r#"{{
456 "key": "flag",
457 "version": 42,
458 "on": false,
459 "targets": [],
460 "rules": [],
461 "prerequisites": [],
462 "fallthrough": {{"variation": 1}},
463 "offVariation": 0,
464 "variations": [false, true],
465 "clientSideAvailability": {{
466 "usingEnvironmentId": {using_environment_id},
467 "usingMobileKey": false
468 }},
469 "salt": "salty"
470 }}"#
471 );
472
473 let flag: Flag = serde_json::from_str(json).unwrap();
474 let client_side_availability = &flag.client_visibility.client_side_availability;
475 assert_eq!(
476 client_side_availability.using_environment_id,
477 using_environment_id
478 );
479 assert!(!client_side_availability.using_mobile_key);
480 assert!(client_side_availability.explicit);
481
482 assert_eq!(flag.using_environment_id(), using_environment_id);
483 }
484
485 #[test_case(true)]
486 #[test_case(false)]
487 fn handles_context_target_schema(using_environment_id: bool) {
488 let json = &format!(
489 r#"{{
490 "key": "flag",
491 "version": 42,
492 "on": false,
493 "targets": [{{
494 "values": ["Bob"],
495 "variation": 1
496 }}],
497 "contextTargets": [{{
498 "contextKind": "org",
499 "values": ["LaunchDarkly"],
500 "variation": 0
501 }}],
502 "rules": [],
503 "prerequisites": [],
504 "fallthrough": {{"variation": 1}},
505 "offVariation": 0,
506 "variations": [false, true],
507 "clientSideAvailability": {{
508 "usingEnvironmentId": {using_environment_id},
509 "usingMobileKey": false
510 }},
511 "salt": "salty"
512 }}"#
513 );
514
515 let flag: Flag = serde_json::from_str(json).unwrap();
516 assert_eq!(1, flag.targets.len());
517 assert!(flag.targets[0].context_kind.is_user());
518
519 assert_eq!(1, flag.context_targets.len());
520 assert_eq!("org", flag.context_targets[0].context_kind.as_ref());
521 }
522
523 #[test]
524 fn getting_variation_with_invalid_index_is_handled_appropriately() {
525 let store = TestStore::new();
526 let flag = store.flag("flag").unwrap();
527
528 let detail = flag.variation(-1, Off);
529
530 assert!(detail.value.is_none());
531 assert!(detail.variation_index.is_none());
532 assert_eq!(
533 detail.reason,
534 Error {
535 error: crate::Error::MalformedFlag
536 }
537 );
538 }
539
540 #[test_case(true, true)]
541 #[test_case(true, false)]
542 #[test_case(false, true)]
543 #[test_case(false, false)]
544 fn can_deserialize_and_reserialize_to_new_schema(
545 using_environment_id: bool,
546 using_mobile_key: bool,
547 ) {
548 let json = &format!(
549 r#"{{
550 "key": "flag",
551 "version": 42,
552 "on": false,
553 "targets": [],
554 "contextTargets": [],
555 "rules": [],
556 "prerequisites": [],
557 "fallthrough": {{
558 "variation": 1
559 }},
560 "offVariation": 0,
561 "variations": [
562 false,
563 true
564 ],
565 "clientSideAvailability": {{
566 "usingMobileKey": {using_environment_id},
567 "usingEnvironmentId": {using_mobile_key}
568 }},
569 "salt": "salty",
570 "trackEvents": false,
571 "trackEventsFallthrough": false,
572 "debugEventsUntilDate": null
573}}"#
574 );
575
576 let flag: Flag = serde_json::from_str(json).unwrap();
577 let restored = serde_json::to_string_pretty(&flag).unwrap();
578
579 assert_eq!(json, &restored);
580 }
581
582 #[test]
583 fn is_experimentation_enabled() {
584 let store = TestStore::new();
585
586 let flag = store.flag("flag").unwrap();
587 asserting!("defaults to false")
588 .that(&flag.is_experimentation_enabled(&Off))
589 .is_false();
590 asserting!("false for fallthrough if trackEventsFallthrough is false")
591 .that(&flag.is_experimentation_enabled(&Fallthrough {
592 in_experiment: false,
593 }))
594 .is_false();
595
596 let flag = store.flag("flagWithRuleExclusion").unwrap();
597 asserting!("true for fallthrough if trackEventsFallthrough is true")
598 .that(&flag.is_experimentation_enabled(&Fallthrough {
599 in_experiment: false,
600 }))
601 .is_true();
602 asserting!("true for rule if rule.trackEvents is true")
603 .that(&flag.is_experimentation_enabled(&RuleMatch {
604 rule_index: 0,
605 rule_id: flag.rules.first().unwrap().id.clone(),
606 in_experiment: false,
607 }))
608 .is_true();
609
610 let flag = store.flag("flagWithExperiment").unwrap();
611 asserting!("true for fallthrough if reason says it is")
612 .that(&flag.is_experimentation_enabled(&Fallthrough {
613 in_experiment: true,
614 }))
615 .is_true();
616 asserting!("false for fallthrough if reason says it is")
617 .that(&flag.is_experimentation_enabled(&Fallthrough {
618 in_experiment: false,
619 }))
620 .is_false();
621 asserting!("true for rule if reason says it is")
623 .that(&flag.is_experimentation_enabled(&RuleMatch {
624 rule_index: 42,
625 rule_id: "lol".into(),
626 in_experiment: true,
627 }))
628 .is_true();
629 asserting!("false 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: false,
634 }))
635 .is_false();
636 }
637
638 #[test]
639 fn sampling_ratio_is_ignored_appropriately() {
640 let store = TestStore::new();
641 let mut flag = store.flag("flag").unwrap();
642
643 flag.sampling_ratio = Some(42);
644 let with_low_sampling_ratio = serde_json::to_string_pretty(&flag).unwrap();
645 assert!(with_low_sampling_ratio.contains("\"samplingRatio\": 42"));
646
647 flag.sampling_ratio = Some(1);
648 let with_highest_ratio = serde_json::to_string_pretty(&flag).unwrap();
649 assert!(!with_highest_ratio.contains("\"samplingRatio\""));
650
651 flag.sampling_ratio = None;
652 let with_no_ratio = serde_json::to_string_pretty(&flag).unwrap();
653 assert!(!with_no_ratio.contains("\"samplingRatio\""));
654 }
655
656 #[test]
657 fn exclude_from_summaries_is_ignored_appropriately() {
658 let store = TestStore::new();
659 let mut flag = store.flag("flag").unwrap();
660
661 flag.exclude_from_summaries = true;
662 let with_exclude = serde_json::to_string_pretty(&flag).unwrap();
663 assert!(with_exclude.contains("\"excludeFromSummaries\": true"));
664
665 flag.exclude_from_summaries = false;
666 let without_exclude = serde_json::to_string_pretty(&flag).unwrap();
667 assert!(!without_exclude.contains("\"excludeFromSummaries\""));
668 }
669
670 #[test]
671 fn migration_settings_included_appropriately() {
672 let store = TestStore::new();
673 let mut flag = store.flag("flag").unwrap();
674
675 flag.migration_settings = None;
676 let without_migration_settings = serde_json::to_string_pretty(&flag).unwrap();
677 assert!(!without_migration_settings.contains("\"migration\""));
678
679 flag.migration_settings = Some(MigrationFlagParameters { check_ratio: None });
680 let without_empty_migration_settings = serde_json::to_string_pretty(&flag).unwrap();
681 assert!(!without_empty_migration_settings.contains("\"migration\""));
682
683 flag.migration_settings = Some(MigrationFlagParameters {
684 check_ratio: Some(1),
685 });
686 let with_default_ratio = serde_json::to_string_pretty(&flag).unwrap();
687 assert!(!with_default_ratio.contains("\"migration\""));
688
689 flag.migration_settings = Some(MigrationFlagParameters {
690 check_ratio: Some(42),
691 });
692 let with_specific_ratio = serde_json::to_string_pretty(&flag).unwrap();
693 assert!(with_specific_ratio.contains("\"migration\": {"));
694 assert!(with_specific_ratio.contains("\"checkRatio\": 42"));
695 }
696}