Skip to main content

oxihuman_core/
feature_flags.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Feature flag management system.
5
6#[allow(dead_code)]
7#[derive(Clone, PartialEq, Debug)]
8pub enum FlagValue {
9    Bool(bool),
10    Int(i64),
11    Float(f64),
12    Text(String),
13}
14
15#[allow(dead_code)]
16#[derive(Clone)]
17pub struct FeatureFlag {
18    pub name: String,
19    pub value: FlagValue,
20    pub description: String,
21    pub tags: Vec<String>,
22    pub override_env: Option<String>,
23}
24
25#[allow(dead_code)]
26pub struct FeatureFlagRegistry {
27    pub flags: Vec<FeatureFlag>,
28}
29
30#[allow(dead_code)]
31pub fn new_feature_registry() -> FeatureFlagRegistry {
32    FeatureFlagRegistry { flags: Vec::new() }
33}
34
35#[allow(dead_code)]
36pub fn register_flag(reg: &mut FeatureFlagRegistry, flag: FeatureFlag) {
37    reg.flags.push(flag);
38}
39
40#[allow(dead_code)]
41pub fn get_flag<'a>(reg: &'a FeatureFlagRegistry, name: &str) -> Option<&'a FeatureFlag> {
42    reg.flags.iter().find(|f| f.name == name)
43}
44
45#[allow(dead_code)]
46pub fn set_flag_value(reg: &mut FeatureFlagRegistry, name: &str, value: FlagValue) -> bool {
47    if let Some(flag) = reg.flags.iter_mut().find(|f| f.name == name) {
48        flag.value = value;
49        true
50    } else {
51        false
52    }
53}
54
55#[allow(dead_code)]
56pub fn is_enabled(reg: &FeatureFlagRegistry, name: &str) -> bool {
57    match get_flag(reg, name) {
58        Some(flag) => match &flag.value {
59            FlagValue::Bool(b) => *b,
60            _ => false,
61        },
62        None => false,
63    }
64}
65
66#[allow(dead_code)]
67pub fn flag_count(reg: &FeatureFlagRegistry) -> usize {
68    reg.flags.len()
69}
70
71#[allow(dead_code)]
72pub fn flags_with_tag<'a>(reg: &'a FeatureFlagRegistry, tag: &str) -> Vec<&'a FeatureFlag> {
73    reg.flags
74        .iter()
75        .filter(|f| f.tags.iter().any(|t| t == tag))
76        .collect()
77}
78
79#[allow(dead_code)]
80pub fn all_enabled_flags(reg: &FeatureFlagRegistry) -> Vec<&FeatureFlag> {
81    reg.flags
82        .iter()
83        .filter(|f| matches!(&f.value, FlagValue::Bool(true)))
84        .collect()
85}
86
87#[allow(dead_code)]
88pub fn default_bool_flag(name: &str, default: bool, description: &str) -> FeatureFlag {
89    FeatureFlag {
90        name: name.to_string(),
91        value: FlagValue::Bool(default),
92        description: description.to_string(),
93        tags: Vec::new(),
94        override_env: None,
95    }
96}
97
98#[allow(dead_code)]
99pub fn default_int_flag(name: &str, default: i64, description: &str) -> FeatureFlag {
100    FeatureFlag {
101        name: name.to_string(),
102        value: FlagValue::Int(default),
103        description: description.to_string(),
104        tags: Vec::new(),
105        override_env: None,
106    }
107}
108
109#[allow(dead_code)]
110pub fn get_flag_bool(reg: &FeatureFlagRegistry, name: &str) -> Option<bool> {
111    match get_flag(reg, name) {
112        Some(flag) => match &flag.value {
113            FlagValue::Bool(b) => Some(*b),
114            _ => None,
115        },
116        None => None,
117    }
118}
119
120#[allow(dead_code)]
121pub fn get_flag_int(reg: &FeatureFlagRegistry, name: &str) -> Option<i64> {
122    match get_flag(reg, name) {
123        Some(flag) => match &flag.value {
124            FlagValue::Int(i) => Some(*i),
125            _ => None,
126        },
127        None => None,
128    }
129}
130
131#[allow(dead_code)]
132pub fn feature_registry_to_json(reg: &FeatureFlagRegistry) -> String {
133    let mut out = String::from("{\"flags\":[");
134    for (i, flag) in reg.flags.iter().enumerate() {
135        if i > 0 {
136            out.push(',');
137        }
138        let val_str = match &flag.value {
139            FlagValue::Bool(b) => format!("{}", b),
140            FlagValue::Int(n) => format!("{}", n),
141            FlagValue::Float(f) => format!("{}", f),
142            FlagValue::Text(s) => format!("\"{}\"", s),
143        };
144        out.push_str(&format!(
145            "{{\"name\":\"{}\",\"value\":{},\"description\":\"{}\"}}",
146            flag.name, val_str, flag.description
147        ));
148    }
149    out.push_str("]}");
150    out
151}
152
153#[allow(dead_code)]
154pub fn remove_flag(reg: &mut FeatureFlagRegistry, name: &str) -> bool {
155    let before = reg.flags.len();
156    reg.flags.retain(|f| f.name != name);
157    reg.flags.len() < before
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_new_feature_registry() {
166        let reg = new_feature_registry();
167        assert_eq!(flag_count(&reg), 0);
168    }
169
170    #[test]
171    fn test_register_flag() {
172        let mut reg = new_feature_registry();
173        let flag = default_bool_flag("debug", true, "Enable debug mode");
174        register_flag(&mut reg, flag);
175        assert_eq!(flag_count(&reg), 1);
176    }
177
178    #[test]
179    fn test_get_flag() {
180        let mut reg = new_feature_registry();
181        let flag = default_bool_flag("feature_x", false, "Feature X");
182        register_flag(&mut reg, flag);
183        assert!(get_flag(&reg, "feature_x").is_some());
184        assert!(get_flag(&reg, "missing").is_none());
185    }
186
187    #[test]
188    fn test_set_flag_value() {
189        let mut reg = new_feature_registry();
190        register_flag(&mut reg, default_bool_flag("beta", false, "Beta"));
191        let ok = set_flag_value(&mut reg, "beta", FlagValue::Bool(true));
192        assert!(ok);
193        assert!(is_enabled(&reg, "beta"));
194        let not_ok = set_flag_value(&mut reg, "nonexistent", FlagValue::Bool(true));
195        assert!(!not_ok);
196    }
197
198    #[test]
199    fn test_is_enabled() {
200        let mut reg = new_feature_registry();
201        register_flag(&mut reg, default_bool_flag("on", true, "On"));
202        register_flag(&mut reg, default_bool_flag("off", false, "Off"));
203        assert!(is_enabled(&reg, "on"));
204        assert!(!is_enabled(&reg, "off"));
205        assert!(!is_enabled(&reg, "missing"));
206    }
207
208    #[test]
209    fn test_flag_count() {
210        let mut reg = new_feature_registry();
211        assert_eq!(flag_count(&reg), 0);
212        register_flag(&mut reg, default_bool_flag("a", true, "A"));
213        register_flag(&mut reg, default_bool_flag("b", false, "B"));
214        assert_eq!(flag_count(&reg), 2);
215    }
216
217    #[test]
218    fn test_flags_with_tag() {
219        let mut reg = new_feature_registry();
220        let mut flag = default_bool_flag("experimental", true, "Experimental feature");
221        flag.tags.push("experimental".to_string());
222        register_flag(&mut reg, flag);
223        register_flag(
224            &mut reg,
225            default_bool_flag("stable", true, "Stable feature"),
226        );
227        let experimental = flags_with_tag(&reg, "experimental");
228        assert_eq!(experimental.len(), 1);
229        assert_eq!(experimental[0].name, "experimental");
230    }
231
232    #[test]
233    fn test_all_enabled_flags() {
234        let mut reg = new_feature_registry();
235        register_flag(&mut reg, default_bool_flag("enabled1", true, "E1"));
236        register_flag(&mut reg, default_bool_flag("disabled1", false, "D1"));
237        register_flag(&mut reg, default_bool_flag("enabled2", true, "E2"));
238        let enabled = all_enabled_flags(&reg);
239        assert_eq!(enabled.len(), 2);
240    }
241
242    #[test]
243    fn test_get_flag_bool() {
244        let mut reg = new_feature_registry();
245        register_flag(&mut reg, default_bool_flag("flag", true, "F"));
246        assert_eq!(get_flag_bool(&reg, "flag"), Some(true));
247        assert_eq!(get_flag_bool(&reg, "missing"), None);
248    }
249
250    #[test]
251    fn test_get_flag_int() {
252        let mut reg = new_feature_registry();
253        register_flag(&mut reg, default_int_flag("max_count", 42, "Max count"));
254        assert_eq!(get_flag_int(&reg, "max_count"), Some(42));
255        assert_eq!(get_flag_int(&reg, "missing"), None);
256    }
257
258    #[test]
259    fn test_get_flag_int_wrong_type() {
260        let mut reg = new_feature_registry();
261        register_flag(&mut reg, default_bool_flag("flag", true, "F"));
262        assert_eq!(get_flag_int(&reg, "flag"), None);
263    }
264
265    #[test]
266    fn test_feature_registry_to_json() {
267        let mut reg = new_feature_registry();
268        register_flag(&mut reg, default_bool_flag("a", true, "A flag"));
269        let json = feature_registry_to_json(&reg);
270        assert!(!json.is_empty());
271        assert!(json.contains("flags"));
272        assert!(json.contains("\"a\""));
273    }
274
275    #[test]
276    fn test_remove_flag() {
277        let mut reg = new_feature_registry();
278        register_flag(&mut reg, default_bool_flag("temp", true, "Temporary"));
279        assert_eq!(flag_count(&reg), 1);
280        let removed = remove_flag(&mut reg, "temp");
281        assert!(removed);
282        assert_eq!(flag_count(&reg), 0);
283        let not_removed = remove_flag(&mut reg, "temp");
284        assert!(!not_removed);
285    }
286
287    #[test]
288    fn test_flag_value_variants() {
289        let mut reg = new_feature_registry();
290        register_flag(
291            &mut reg,
292            FeatureFlag {
293                name: "text_flag".to_string(),
294                value: FlagValue::Text("hello".to_string()),
295                description: "Text".to_string(),
296                tags: Vec::new(),
297                override_env: None,
298            },
299        );
300        register_flag(
301            &mut reg,
302            FeatureFlag {
303                name: "float_flag".to_string(),
304                value: FlagValue::Float(2.78),
305                description: "Float".to_string(),
306                tags: Vec::new(),
307                override_env: Some("MY_FLOAT_FLAG".to_string()),
308            },
309        );
310        let json = feature_registry_to_json(&reg);
311        assert!(json.contains("text_flag"));
312        assert!(json.contains("float_flag"));
313    }
314}