Skip to main content

crawlkit_engine/
feature_flags.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use parking_lot::RwLock;
5use serde::{Deserialize, Serialize};
6
7/// Feature flags for runtime configuration.
8///
9/// Flags are immutable per-crawl session once set.
10/// Supports TOML configuration and programmatic access.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct FeatureFlags {
13    #[serde(flatten)]
14    flags: HashMap<String, bool>,
15}
16
17impl FeatureFlags {
18    /// Create empty feature flags.
19    #[must_use]
20    pub fn new() -> Self {
21        Self {
22            flags: HashMap::new(),
23        }
24    }
25
26    /// Create from TOML configuration.
27    ///
28    /// # Errors
29    /// Returns error if TOML is invalid.
30    pub fn from_toml(toml_str: &str) -> Result<Self, toml::de::Error> {
31        toml::from_str(toml_str)
32    }
33
34    /// Set a feature flag.
35    pub fn set(&mut self, key: &str, value: bool) {
36        self.flags.insert(key.to_string(), value);
37    }
38
39    /// Get a feature flag value.
40    #[must_use]
41    pub fn get(&self, key: &str) -> bool {
42        *self.flags.get(key).unwrap_or(&false)
43    }
44
45    /// Check if a flag exists.
46    #[must_use]
47    pub fn has(&self, key: &str) -> bool {
48        self.flags.contains_key(key)
49    }
50
51    /// Get all flags.
52    #[must_use]
53    pub fn all(&self) -> &HashMap<String, bool> {
54        &self.flags
55    }
56
57    /// Merge with another set of flags (other overrides self).
58    #[must_use]
59    pub fn merge(mut self, other: FeatureFlags) -> Self {
60        for (k, v) in other.flags {
61            self.flags.insert(k, v);
62        }
63        self
64    }
65}
66
67impl Default for FeatureFlags {
68    fn default() -> Self {
69        let mut flags = HashMap::new();
70        // Default flags
71        flags.insert("js_rendering".to_string(), false);
72        flags.insert("ai_analyzers".to_string(), true);
73        flags.insert("wasm_analyzers".to_string(), true);
74        flags.insert("backlink_analysis".to_string(), false);
75        flags.insert("rum_integration".to_string(), false);
76        flags.insert("encryption_at_rest".to_string(), false);
77        flags.insert("audit_trail".to_string(), true);
78        flags.insert("observability".to_string(), true);
79        Self { flags }
80    }
81}
82
83/// Shared feature flag state for concurrent access.
84#[derive(Clone)]
85pub struct SharedFeatureFlags {
86    flags: Arc<RwLock<FeatureFlags>>,
87}
88
89impl SharedFeatureFlags {
90    /// Create shared feature flags.
91    #[must_use]
92    pub fn new(flags: FeatureFlags) -> Self {
93        Self {
94            flags: Arc::new(RwLock::new(flags)),
95        }
96    }
97
98    /// Get a flag value (thread-safe).
99    #[must_use]
100    pub fn get(&self, key: &str) -> bool {
101        self.flags.read().get(key)
102    }
103
104    /// Set a flag value (thread-safe).
105    pub fn set(&self, key: &str, value: bool) {
106        self.flags.write().set(key, value);
107    }
108
109    /// Get all flags (thread-safe).
110    #[must_use]
111    pub fn snapshot(&self) -> FeatureFlags {
112        self.flags.read().clone()
113    }
114}
115
116// ---------------------------------------------------------------------------
117// Standard feature flag keys
118// ---------------------------------------------------------------------------
119
120/// Feature flag: Enable Playwright JS rendering.
121pub const FLAG_JS_RENDERING: &str = "js_rendering";
122
123/// Feature flag: Enable AI search optimization analyzers.
124pub const FLAG_AI_ANALYZERS: &str = "ai_analyzers";
125
126/// Feature flag: Enable WASM error detection analyzers.
127pub const FLAG_WASM_ANALYZERS: &str = "wasm_analyzers";
128
129/// Feature flag: Enable backlink analysis.
130pub const FLAG_BACKLINK_ANALYSIS: &str = "backlink_analysis";
131
132/// Feature flag: Enable RUM (Real User Monitoring) integration.
133pub const FLAG_RUM_INTEGRATION: &str = "rum_integration";
134
135/// Feature flag: Enable encryption at rest.
136pub const FLAG_ENCRYPTION_AT_REST: &str = "encryption_at_rest";
137
138/// Feature flag: Enable audit trail logging.
139pub const FLAG_AUDIT_TRAIL: &str = "audit_trail";
140
141/// Feature flag: Enable observability stack.
142pub const FLAG_OBSERVABILITY: &str = "observability";
143
144// ---------------------------------------------------------------------------
145// Tests
146// ---------------------------------------------------------------------------
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_feature_flags_default() {
154        let flags = FeatureFlags::default();
155        assert!(!flags.get(FLAG_JS_RENDERING));
156        assert!(flags.get(FLAG_AI_ANALYZERS));
157        assert!(flags.get(FLAG_WASM_ANALYZERS));
158        assert!(!flags.get(FLAG_BACKLINK_ANALYSIS));
159    }
160
161    #[test]
162    fn test_feature_flags_set_get() {
163        let mut flags = FeatureFlags::new();
164        flags.set("custom_flag", true);
165        assert!(flags.get("custom_flag"));
166        flags.set("custom_flag", false);
167        assert!(!flags.get("custom_flag"));
168    }
169
170    #[test]
171    fn test_feature_flags_from_toml() {
172        let toml_str = r#"
173            js_rendering = true
174            ai_analyzers = false
175        "#;
176        let flags = FeatureFlags::from_toml(toml_str).unwrap();
177        assert!(flags.get("js_rendering"));
178        assert!(!flags.get("ai_analyzers"));
179    }
180
181    #[test]
182    fn test_feature_flags_merge() {
183        let mut base = FeatureFlags::default();
184        base.set("custom", false);
185
186        let mut override_flags = FeatureFlags::new();
187        override_flags.set("custom", true);
188        override_flags.set("js_rendering", true);
189
190        let merged = base.merge(override_flags);
191        assert!(merged.get("custom"));
192        assert!(merged.get("js_rendering"));
193    }
194
195    #[test]
196    fn test_shared_feature_flags() {
197        let flags = FeatureFlags::default();
198        let shared = SharedFeatureFlags::new(flags);
199
200        assert!(!shared.get(FLAG_JS_RENDERING));
201        shared.set(FLAG_JS_RENDERING, true);
202        assert!(shared.get(FLAG_JS_RENDERING));
203    }
204}