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