enum_toggles/
lib.rs

1//! This crate provides a toggle manager that can load from a file.
2//! Toggle states are read-only and accessed in O(1) time.
3//! There's a direct relationship where each string name corresponds to a unique name in the enum.
4//!
5//! # Example
6//!
7//! - File `toggles.yaml` conains:
8//! ```yaml
9//! FeatureA: 0
10//! FeatureB: 1
11//! ```
12//!
13//! - Basic usage
14//! ```rust
15//! use enum_toggles::EnumToggles;
16//! use strum_macros::{AsRefStr, EnumIter};
17//!
18//! #[derive(AsRefStr, EnumIter, PartialEq)]
19//! enum MyToggle {
20//!     FeatureA,
21//!     FeatureB,
22//! }
23//!
24//! let mut toggles: EnumToggles::<MyToggle> = EnumToggles::new();
25//! toggles.set(MyToggle::FeatureA as usize, true);
26//! toggles.set_by_name("FeatureB", true); // Mapped to MyToggle::FeatureB
27//! // toggles.load_from_file("toggles.yaml"); // Load toggles state from file
28//! println!("{:?}", toggles);
29//! ```
30//!
31//! - With concucrency context
32//! ```rust
33//! use enum_toggles::EnumToggles;
34//! use log::warn;
35//! use std::env;
36//! use std::ops::Deref;
37//! use std::sync::LazyLock;
38//! use strum_macros::{AsRefStr, EnumIter};
39//!
40//! #[derive(AsRefStr, EnumIter, PartialEq)]
41//! enum MyToggle {
42//!     FeatureA,
43//!     FeatureB,
44//! }
45//!
46//! pub static TOGGLES: LazyLock<EnumToggles<MyToggle>> = LazyLock::new(|| {
47//!     let mut toggle:EnumToggles<MyToggle> = EnumToggles::new();
48//!     let filepath = env::var("TOGGLES_FILE");
49//!     match filepath {
50//!         Ok(path) => {
51//!             if !path.is_empty() {
52//!                 toggle.load_from_file(&path)
53//!             }
54//!         }
55//!         Err(_) => warn!("Environment variable TOGGLES_FILE not set"),
56//!     }
57//!     toggle
58//! });
59//!
60//! println!("{:?}", TOGGLES.deref());
61//! ```
62//!
63
64use bitvec::prelude::*;
65use std::fs;
66use std::{collections::HashMap, fmt};
67use yaml_rust::{Yaml, YamlLoader};
68
69/// Contains the toggle value for each item of the enum T.
70pub struct EnumToggles<T> {
71    toggles_value: BitVec,
72    _marker: std::marker::PhantomData<T>,
73}
74
75impl<T> Default for EnumToggles<T>
76where
77    T: strum::IntoEnumIterator + AsRef<str> + 'static,
78{
79    fn default() -> Self {
80        EnumToggles {
81            toggles_value: bitvec![0; T::iter().count()],
82            _marker: std::marker::PhantomData,
83        }
84    }
85}
86
87/// Handle the toggle value of an enum T.
88impl<T> EnumToggles<T>
89where
90    T: strum::IntoEnumIterator + AsRef<str> + PartialEq + 'static,
91{
92    /// Create a new instance of `EnumToggles` with all toggles set to false.
93    ///
94    /// This operation is *O*(*n*).
95    pub fn new() -> Self {
96        let mut toggles: EnumToggles<T> = EnumToggles {
97            toggles_value: bitvec![0; T::iter().count()],
98            _marker: std::marker::PhantomData,
99        };
100        toggles.toggles_value.fill(false);
101        toggles
102    }
103
104    /// Set all toggles value defiend in the yaml file.
105    pub fn load_from_file(&mut self, filepath: &str) {
106        match fs::read_to_string(filepath) {
107            Ok(content) => {
108                let docs = YamlLoader::load_from_str(&content).unwrap();
109                let doc = &docs[0];
110
111                if let Yaml::Hash(ref h) = doc {
112                    for (key, value) in h {
113                        self.set_by_name(
114                            key.as_str().unwrap_or("<non-string>"),
115                            value.as_i64().unwrap_or(0) == 1,
116                        );
117                    }
118                }
119            }
120            Err(e) => println!("Error reading file: {}", e),
121        }
122    }
123
124    /// Set the bool value of all toggles based on a HashMap.
125    ///
126    /// This operation is *O*(*n²*).
127    pub fn set_all(&mut self, init: HashMap<String, bool>) {
128        self.toggles_value.fill(false);
129        for toggle in T::iter() {
130            if init.contains_key(toggle.as_ref()) {
131                if let Some(toggle_id) = T::iter().position(|x| x == toggle) {
132                    self.set(toggle_id, init[toggle.as_ref()]);
133                }
134            }
135        }
136    }
137
138    /// Set the bool value of a toggle by its name.
139    ///
140    /// This operation is *O*(*n*).
141    pub fn set_by_name(&mut self, toggle_name: &str, value: bool) {
142        if let Some(toggle) = T::iter().find(|t| toggle_name == t.as_ref()) {
143            if let Some(toggle_id) = T::iter().position(|x| x == toggle) {
144                self.set(toggle_id, value);
145            }
146        }
147    }
148
149    /// Set the bool value of a toggle by toggle id.
150    ///
151    /// This operation is *O*(*1*).
152    pub fn set(&mut self, toggle_id: usize, value: bool) {
153        if toggle_id >= self.toggles_value.len() {
154            panic!(
155                "Out-of-bounds access. The provided toggle_id is {}, but the array size is {}. Please use the default enum value.",
156                toggle_id,
157                self.toggles_value.len()
158            );
159        }
160        self.toggles_value.set(toggle_id, value);
161    }
162
163    /// Get the bool value of a toggle by toggle id.
164    ///
165    /// This operation is *O*(*1*).
166    pub fn get(&self, toggle_id: usize) -> bool {
167        self.toggles_value[toggle_id]
168    }
169}
170
171/// Diplay all toggles and their values.
172impl<T> fmt::Debug for EnumToggles<T>
173where
174    T: strum::IntoEnumIterator + AsRef<str> + PartialEq + 'static,
175{
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        for toggle in T::iter() {
178            if let Some(toggle_id) = T::iter().position(|x| x == toggle) {
179                let name = toggle.as_ref();
180                writeln!(f, "{} {} ", self.get(toggle_id) as u8, name)?;
181            }
182        }
183        Ok(())
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use std::io::Write;
191    use strum::IntoEnumIterator;
192    use strum_macros::{AsRefStr, EnumIter};
193
194    #[derive(AsRefStr, EnumIter, PartialEq)]
195    pub enum TestToggles {
196        Toggle1,
197        Toggle2,
198    }
199
200    #[test]
201    fn test_default() {
202        let toggles: EnumToggles<TestToggles> = EnumToggles::default();
203        assert_eq!(toggles.toggles_value.len(), TestToggles::iter().count());
204    }
205
206    #[test]
207    fn test_set_all() {
208        let mut toggles: EnumToggles<TestToggles> = EnumToggles::new();
209        toggles.set_all(HashMap::from([("Toggle1".to_string(), true)]));
210        assert_eq!(toggles.get(TestToggles::Toggle1 as usize), true);
211        assert_eq!(toggles.get(TestToggles::Toggle2 as usize), false);
212    }
213
214    #[test]
215    fn test_set_by_name() {
216        let mut toggles: EnumToggles<TestToggles> = EnumToggles::new();
217        assert_eq!(toggles.get(TestToggles::Toggle1 as usize), false);
218        toggles.set_by_name("Toggle1", true);
219        assert_eq!(toggles.get(TestToggles::Toggle1 as usize), true);
220
221        toggles.set_by_name("Undefined_Toggle", true);
222    }
223
224    #[test]
225    fn test_display() {
226        let toggles: EnumToggles<TestToggles> = EnumToggles::new();
227        assert_eq!(format!("{:?}", toggles).is_empty(), false);
228    }
229
230    #[test]
231    fn test_load_from_file() {
232        // Create a temporary file
233        let mut temp_file =
234            tempfile::NamedTempFile::new().expect("Unable to create temporary file");
235
236        // Write some data to the file
237        writeln!(temp_file, "Toggle1: 1").expect("Unable to write to temporary file");
238        writeln!(temp_file, "Toggle2: 0").expect("Unable to write to temporary file");
239        writeln!(temp_file, "VAR1: 0").expect("Unable to write to temporary file");
240        writeln!(temp_file, "").expect("Unable to write to temporary file");
241
242        // Get the path of the temporary file
243        let filepath = temp_file.path().to_str().unwrap();
244
245        // Create a Toggles instance and load from the file
246        let mut toggles: EnumToggles<TestToggles> = EnumToggles::new();
247        toggles.load_from_file(filepath);
248
249        // Verify that the toggles were set correctly
250        assert_eq!(toggles.get(TestToggles::Toggle1 as usize), true);
251        assert_eq!(toggles.get(TestToggles::Toggle2 as usize), false);
252    }
253
254    #[derive(AsRefStr, EnumIter, PartialEq)]
255    pub enum DeviantToggles {
256        Toggle1 = 5,
257        Toggle2 = 10,
258    }
259
260    #[test]
261    #[should_panic(
262        expected = "Out-of-bounds access. The provided toggle_id is 5, but the array size is 2. Please use the default enum value."
263    )]
264    fn test_deviant_toggles() {
265        let mut toggles: EnumToggles<DeviantToggles> = EnumToggles::new();
266        toggles.set(DeviantToggles::Toggle1 as usize, true);
267    }
268}