Skip to main content

rullst_core/feature/
memory.rs

1use async_trait::async_trait;
2use dashmap::DashMap;
3
4use super::driver::FeatureDriver;
5use super::resolvers::{calculate_hash_bucket, resolve_variant};
6
7// ─── Memory Driver ──────────────────────────────────────────────────────────
8
9struct MemoryFlagRule {
10    enabled: bool,
11    rollout_percentage: Option<u32>,
12    variants: Option<Vec<(String, u32)>>,
13}
14
15/// Memory-backed feature flag driver. Perfect for programmatic overrides and tests.
16#[non_exhaustive]
17pub struct MemoryFeatureDriver {
18    rules: DashMap<String, MemoryFlagRule>,
19}
20
21impl MemoryFeatureDriver {
22    /// Creates a new `MemoryFeatureDriver`.
23    pub fn new() -> Self {
24        Self {
25            rules: DashMap::new(),
26        }
27    }
28
29    /// Explicitly override a flag state.
30    pub fn override_enabled(&self, flag: &str, enabled: bool) {
31        self.rules.insert(
32            flag.to_string(),
33            MemoryFlagRule {
34                enabled,
35                rollout_percentage: None,
36                variants: None,
37            },
38        );
39    }
40
41    /// Explicitly override a percentage rollout rule (e.g. 30%).
42    pub fn override_rollout(&self, flag: &str, percentage: u32) {
43        self.rules.insert(
44            flag.to_string(),
45            MemoryFlagRule {
46                enabled: true,
47                rollout_percentage: Some(percentage),
48                variants: None,
49            },
50        );
51    }
52
53    /// Explicitly override an A/B split configuration (e.g. [("a", 50), ("b", 50)]).
54    pub fn override_variants(&self, flag: &str, variants: Vec<(String, u32)>) {
55        self.rules.insert(
56            flag.to_string(),
57            MemoryFlagRule {
58                enabled: true,
59                rollout_percentage: None,
60                variants: Some(variants),
61            },
62        );
63    }
64}
65
66impl Default for MemoryFeatureDriver {
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72#[async_trait]
73impl FeatureDriver for MemoryFeatureDriver {
74    async fn enabled(&self, flag: &str) -> Option<bool> {
75        self.rules
76            .get(flag)
77            .map(|r| r.enabled && r.rollout_percentage.is_none())
78    }
79
80    #[cfg_attr(mutants, mutants::skip)]
81    async fn enabled_for(&self, flag: &str, identifier: &str) -> Option<bool> {
82        let rule = self.rules.get(flag)?;
83        if !rule.enabled {
84            return Some(false);
85        }
86        if let Some(pct) = rule.rollout_percentage {
87            let bucket = calculate_hash_bucket(flag, identifier);
88            return Some(bucket < pct);
89        }
90        Some(rule.enabled)
91    }
92
93    #[cfg_attr(mutants, mutants::skip)]
94    async fn variant(&self, flag: &str, identifier: &str) -> Option<String> {
95        let rule = self.rules.get(flag)?;
96        if !rule.enabled {
97            return Some("disabled".to_string());
98        }
99        if let Some(ref variants) = rule.variants {
100            let bucket = calculate_hash_bucket(flag, identifier);
101            return resolve_variant(variants, bucket);
102        }
103        if let Some(pct) = rule.rollout_percentage {
104            let bucket = calculate_hash_bucket(flag, identifier);
105            return Some(if bucket < pct {
106                "enabled".to_string()
107            } else {
108                "disabled".to_string()
109            });
110        }
111        Some(if rule.enabled {
112            "enabled".to_string()
113        } else {
114            "disabled".to_string()
115        })
116    }
117}