Skip to main content

karbon_framework/feature/
feature_flags.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3use tokio::sync::RwLock;
4use serde::{Serialize, Deserialize};
5use chrono::{DateTime, Utc};
6
7/// A single feature flag with optional metadata
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct FeatureFlag {
10    pub name: String,
11    pub enabled: bool,
12    pub description: String,
13    pub updated_at: DateTime<Utc>,
14}
15
16/// In-memory runtime feature flag manager.
17///
18/// ```ignore
19/// let flags = FeatureFlags::new();
20///
21/// // Register flags at startup
22/// flags.register("dark_mode", true, "Enable dark mode UI").await;
23/// flags.register("beta_search", false, "New search algorithm").await;
24///
25/// // Check in handlers
26/// if flags.is_enabled("dark_mode").await {
27///     // show dark mode
28/// }
29///
30/// // Toggle at runtime (e.g., from an admin endpoint)
31/// flags.toggle("beta_search").await;
32/// ```
33#[derive(Clone)]
34pub struct FeatureFlags {
35    store: Arc<RwLock<HashMap<String, FeatureFlag>>>,
36}
37
38impl FeatureFlags {
39    pub fn new() -> Self {
40        Self {
41            store: Arc::new(RwLock::new(HashMap::new())),
42        }
43    }
44
45    /// Register a feature flag with an initial state
46    pub async fn register(&self, name: &str, enabled: bool, description: &str) {
47        self.store.write().await.insert(name.to_string(), FeatureFlag {
48            name: name.to_string(),
49            enabled,
50            description: description.to_string(),
51            updated_at: Utc::now(),
52        });
53    }
54
55    /// Check if a feature is enabled (returns false if not registered)
56    pub async fn is_enabled(&self, name: &str) -> bool {
57        self.store.read().await
58            .get(name)
59            .is_some_and(|f| f.enabled)
60    }
61
62    /// Enable a feature flag
63    pub async fn enable(&self, name: &str) -> bool {
64        self.set(name, true).await
65    }
66
67    /// Disable a feature flag
68    pub async fn disable(&self, name: &str) -> bool {
69        self.set(name, false).await
70    }
71
72    /// Toggle a feature flag, returns the new state
73    pub async fn toggle(&self, name: &str) -> Option<bool> {
74        let mut store = self.store.write().await;
75        if let Some(flag) = store.get_mut(name) {
76            flag.enabled = !flag.enabled;
77            flag.updated_at = Utc::now();
78            Some(flag.enabled)
79        } else {
80            None
81        }
82    }
83
84    /// Set a feature flag to a specific state
85    pub async fn set(&self, name: &str, enabled: bool) -> bool {
86        let mut store = self.store.write().await;
87        if let Some(flag) = store.get_mut(name) {
88            flag.enabled = enabled;
89            flag.updated_at = Utc::now();
90            true
91        } else {
92            false
93        }
94    }
95
96    /// Get a single feature flag
97    pub async fn get(&self, name: &str) -> Option<FeatureFlag> {
98        self.store.read().await.get(name).cloned()
99    }
100
101    /// List all feature flags
102    pub async fn list(&self) -> Vec<FeatureFlag> {
103        self.store.read().await.values().cloned().collect()
104    }
105
106    /// Remove a feature flag
107    pub async fn remove(&self, name: &str) -> bool {
108        self.store.write().await.remove(name).is_some()
109    }
110
111    /// Register multiple flags at once from tuples
112    pub async fn register_many(&self, flags: &[(&str, bool, &str)]) {
113        let mut store = self.store.write().await;
114        for (name, enabled, description) in flags {
115            store.insert(name.to_string(), FeatureFlag {
116                name: name.to_string(),
117                enabled: *enabled,
118                description: description.to_string(),
119                updated_at: Utc::now(),
120            });
121        }
122    }
123}
124
125impl Default for FeatureFlags {
126    fn default() -> Self {
127        Self::new()
128    }
129}