Skip to main content

oxihuman_core/
feature_flag.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Feature flag registry with toggle support.
6
7use std::collections::HashMap;
8
9/// State of a single feature flag.
10#[derive(Debug, Clone, PartialEq)]
11pub enum FlagState {
12    Enabled,
13    Disabled,
14    Experimental,
15}
16
17/// A single feature flag entry.
18#[derive(Debug, Clone)]
19pub struct FeatureFlagEntry {
20    pub name: String,
21    pub state: FlagState,
22    pub description: String,
23}
24
25/// Registry for feature flags.
26#[derive(Debug, Default)]
27pub struct FeatureFlagRegistry {
28    flags: HashMap<String, FeatureFlagEntry>,
29}
30
31impl FeatureFlagRegistry {
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    pub fn register(&mut self, name: &str, enabled: bool, description: &str) {
37        let state = if enabled {
38            FlagState::Enabled
39        } else {
40            FlagState::Disabled
41        };
42        self.flags.insert(
43            name.to_string(),
44            FeatureFlagEntry {
45                name: name.to_string(),
46                state,
47                description: description.to_string(),
48            },
49        );
50    }
51
52    pub fn toggle(&mut self, name: &str) -> bool {
53        if let Some(entry) = self.flags.get_mut(name) {
54            entry.state = match entry.state {
55                FlagState::Enabled => FlagState::Disabled,
56                FlagState::Disabled | FlagState::Experimental => FlagState::Enabled,
57            };
58            true
59        } else {
60            false
61        }
62    }
63
64    pub fn is_enabled(&self, name: &str) -> bool {
65        self.flags
66            .get(name)
67            .map(|e| e.state == FlagState::Enabled)
68            .unwrap_or(false)
69    }
70
71    pub fn flag_count(&self) -> usize {
72        self.flags.len()
73    }
74
75    pub fn all_names(&self) -> Vec<String> {
76        let mut names: Vec<String> = self.flags.keys().cloned().collect();
77        names.sort();
78        names
79    }
80}
81
82pub fn new_flag_registry() -> FeatureFlagRegistry {
83    FeatureFlagRegistry::new()
84}
85
86pub fn register_feature(registry: &mut FeatureFlagRegistry, name: &str, enabled: bool, desc: &str) {
87    registry.register(name, enabled, desc);
88}
89
90pub fn toggle_feature(registry: &mut FeatureFlagRegistry, name: &str) -> bool {
91    registry.toggle(name)
92}
93
94pub fn is_feature_enabled(registry: &FeatureFlagRegistry, name: &str) -> bool {
95    registry.is_enabled(name)
96}
97
98pub fn feature_flag_count(registry: &FeatureFlagRegistry) -> usize {
99    registry.flag_count()
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_register_and_check() {
108        let mut reg = new_flag_registry();
109        register_feature(&mut reg, "dark_mode", true, "Dark theme");
110        assert!(is_feature_enabled(&reg, "dark_mode"));
111    }
112
113    #[test]
114    fn test_disabled_flag() {
115        let mut reg = new_flag_registry();
116        register_feature(&mut reg, "beta", false, "Beta feature");
117        assert!(!is_feature_enabled(&reg, "beta"));
118    }
119
120    #[test]
121    fn test_toggle_on_to_off() {
122        let mut reg = new_flag_registry();
123        register_feature(&mut reg, "foo", true, "Foo");
124        toggle_feature(&mut reg, "foo");
125        assert!(!is_feature_enabled(&reg, "foo"));
126    }
127
128    #[test]
129    fn test_toggle_off_to_on() {
130        let mut reg = new_flag_registry();
131        register_feature(&mut reg, "bar", false, "Bar");
132        toggle_feature(&mut reg, "bar");
133        assert!(is_feature_enabled(&reg, "bar"));
134    }
135
136    #[test]
137    fn test_toggle_unknown_returns_false() {
138        let mut reg = new_flag_registry();
139        assert!(!toggle_feature(&mut reg, "nonexistent"));
140    }
141
142    #[test]
143    fn test_flag_count() {
144        let mut reg = new_flag_registry();
145        register_feature(&mut reg, "a", true, "A");
146        register_feature(&mut reg, "b", false, "B");
147        assert_eq!(feature_flag_count(&reg), 2);
148    }
149
150    #[test]
151    fn test_all_names_sorted() {
152        let mut reg = new_flag_registry();
153        register_feature(&mut reg, "zeta", true, "Z");
154        register_feature(&mut reg, "alpha", true, "A");
155        let names = reg.all_names();
156        assert_eq!(names[0], "alpha");
157    }
158
159    #[test]
160    fn test_unknown_flag_disabled() {
161        let reg = new_flag_registry();
162        assert!(!is_feature_enabled(&reg, "missing"));
163    }
164
165    #[test]
166    fn test_experimental_state() {
167        /* experimental flags toggle to enabled */
168        let mut reg = new_flag_registry();
169        reg.flags.insert(
170            "exp".to_string(),
171            FeatureFlagEntry {
172                name: "exp".to_string(),
173                state: FlagState::Experimental,
174                description: "Experimental".to_string(),
175            },
176        );
177        toggle_feature(&mut reg, "exp");
178        assert!(is_feature_enabled(&reg, "exp"));
179    }
180
181    #[test]
182    fn test_overwrite_flag() {
183        /* registering same name twice overwrites */
184        let mut reg = new_flag_registry();
185        register_feature(&mut reg, "x", false, "off");
186        register_feature(&mut reg, "x", true, "on");
187        assert!(is_feature_enabled(&reg, "x"));
188    }
189}