coil_observability/
feature_flags.rs1use crate::ObservabilityError;
2use crate::validation::{BrandId, CohortId, CustomerAppId, FeatureFlagId, SiteId};
3use coil_config::Environment;
4use std::collections::{BTreeMap, BTreeSet};
5use std::fmt;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum FlagTarget {
9 Environment(Environment),
10 CustomerApp(CustomerAppId),
11 Site(SiteId),
12 Brand(BrandId),
13 Cohort(CohortId),
14}
15
16impl fmt::Display for FlagTarget {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::Environment(environment) => write!(f, "environment:{environment:?}"),
20 Self::CustomerApp(app) => write!(f, "customer_app:{app}"),
21 Self::Site(site) => write!(f, "site:{site}"),
22 Self::Brand(brand) => write!(f, "brand:{brand}"),
23 Self::Cohort(cohort) => write!(f, "cohort:{cohort}"),
24 }
25 }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct FeatureFlagRule {
30 pub target: FlagTarget,
31 pub enabled: bool,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct FeatureFlag {
36 pub id: FeatureFlagId,
37 pub default_enabled: bool,
38 pub rules: Vec<FeatureFlagRule>,
39}
40
41impl FeatureFlag {
42 pub fn new(id: impl Into<String>, default_enabled: bool) -> Result<Self, ObservabilityError> {
43 Ok(Self {
44 id: FeatureFlagId::new(id)?,
45 default_enabled,
46 rules: Vec::new(),
47 })
48 }
49
50 pub fn with_rule(
51 mut self,
52 target: FlagTarget,
53 enabled: bool,
54 ) -> Result<Self, ObservabilityError> {
55 if self.rules.iter().any(|rule| rule.target == target) {
56 return Err(ObservabilityError::DuplicateFlagRule {
57 flag: self.id.to_string(),
58 scope: target.to_string(),
59 });
60 }
61
62 self.rules.push(FeatureFlagRule { target, enabled });
63 Ok(self)
64 }
65
66 pub fn enabled_for(&self, context: &FeatureFlagContext) -> bool {
67 self.rules
68 .iter()
69 .filter(|rule| context.matches(&rule.target))
70 .map(|rule| rule.enabled)
71 .next_back()
72 .unwrap_or(self.default_enabled)
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct FeatureFlagContext {
78 pub environment: Environment,
79 pub customer_app: Option<CustomerAppId>,
80 pub site: Option<SiteId>,
81 pub brand: Option<BrandId>,
82 pub cohorts: BTreeSet<CohortId>,
83}
84
85impl FeatureFlagContext {
86 pub fn matches(&self, target: &FlagTarget) -> bool {
87 match target {
88 FlagTarget::Environment(environment) => &self.environment == environment,
89 FlagTarget::CustomerApp(app) => self.customer_app.as_ref() == Some(app),
90 FlagTarget::Site(site) => self.site.as_ref() == Some(site),
91 FlagTarget::Brand(brand) => self.brand.as_ref() == Some(brand),
92 FlagTarget::Cohort(cohort) => self.cohorts.contains(cohort),
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Default)]
98pub struct FeatureFlagRegistry {
99 flags: BTreeMap<FeatureFlagId, FeatureFlag>,
100}
101
102impl FeatureFlagRegistry {
103 pub fn new() -> Self {
104 Self::default()
105 }
106
107 pub fn insert(&mut self, flag: FeatureFlag) -> Result<(), ObservabilityError> {
108 if self.flags.insert(flag.id.clone(), flag.clone()).is_some() {
109 return Err(ObservabilityError::DuplicateFlag {
110 flag: flag.id.to_string(),
111 });
112 }
113
114 Ok(())
115 }
116
117 pub fn get(&self, id: &FeatureFlagId) -> Option<&FeatureFlag> {
118 self.flags.get(id)
119 }
120}