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.txt` conains:
8//! ```txt
9//! 0 FeatureA
10//! 1 FeatureB
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.txt"); // 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 once_cell::sync::Lazy;
36//! use std::env;
37//! use std::ops::Deref;
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: Lazy<EnumToggles<MyToggle>> = Lazy::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 log::error;
66use std::io::BufRead;
67use std::{collections::HashMap, fmt};
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 file.
105    pub fn load_from_file(&mut self, filepath: &str) {
106        let file = std::fs::File::open(filepath).expect("Unable to open file");
107        let reader = std::io::BufReader::new(file);
108        for line in reader.lines() {
109            match line {
110                Ok(line) => {
111                    let parts: Vec<&str> = line.split_whitespace().collect();
112                    if parts.len() == 2 {
113                        if let Ok(value) = parts[0].parse::<u8>() {
114                            self.set_by_name(parts[1], value != 0);
115                        }
116                    }
117                }
118                Err(e) => {
119                    error!("Error reading line: {e}");
120                }
121            }
122        }
123    }
124
125    /// Set the bool value of all toggles based on a HashMap.
126    ///
127    /// This operation is *O*(*n²*).
128    pub fn set_all(&mut self, init: HashMap<String, bool>) {
129        self.toggles_value.fill(false);
130        for toggle in T::iter() {
131            if init.contains_key(toggle.as_ref()) {
132                if let Some(toggle_id) = T::iter().position(|x| x == toggle) {
133                    self.set(toggle_id, init[toggle.as_ref()]);
134                }
135            }
136        }
137    }
138
139    /// Set the bool value of a toggle by its name.
140    ///
141    /// This operation is *O*(*n*).
142    pub fn set_by_name(&mut self, toggle_name: &str, value: bool) {
143        if let Some(toggle) = T::iter().find(|t| toggle_name == t.as_ref()) {
144            if let Some(toggle_id) = T::iter().position(|x| x == toggle) {
145                self.set(toggle_id, value);
146            }
147        }
148    }
149
150    /// Set the bool value of a toggle by toggle id.
151    ///
152    /// This operation is *O*(*1*).
153    pub fn set(&mut self, toggle_id: usize, value: bool) {
154        if toggle_id >= self.toggles_value.len() {
155            panic!(
156                "Out-of-bounds access. The provided toggle_id is {}, but the array size is {}. Please use the default enum value.",
157                toggle_id,
158                self.toggles_value.len()
159            );
160        }
161        self.toggles_value.set(toggle_id, value);
162    }
163
164    /// Get the bool value of a toggle by toggle id.
165    ///
166    /// This operation is *O*(*1*).
167    pub fn get(&self, toggle_id: usize) -> bool {
168        self.toggles_value[toggle_id]
169    }
170}
171
172/// Diplay all toggles and their values.
173impl<T> fmt::Display for EnumToggles<T>
174where
175    T: strum::IntoEnumIterator + AsRef<str> + PartialEq + 'static,
176{
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        for toggle in T::iter() {
179            if let Some(toggle_id) = T::iter().position(|x| x == toggle) {
180                let name = toggle.as_ref();
181                writeln!(f, "{} {} ", self.get(toggle_id) as u8, name)?;
182            }
183        }
184        Ok(())
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use std::io::Write;
192    use strum::IntoEnumIterator;
193    use strum_macros::{AsRefStr, EnumIter};
194
195    #[derive(AsRefStr, EnumIter, PartialEq)]
196    pub enum TestToggles {
197        Toggle1,
198        Toggle2,
199    }
200
201    #[test]
202    fn default() {
203        let toggles: EnumToggles<TestToggles> = EnumToggles::default();
204        assert_eq!(toggles.toggles_value.len(), TestToggles::iter().count());
205    }
206
207    #[test]
208    fn set_all() {
209        let mut toggles: EnumToggles<TestToggles> = EnumToggles::new();
210        toggles.set_all(HashMap::from([("Toggle1".to_string(), true)]));
211        assert_eq!(toggles.get(TestToggles::Toggle1 as usize), true);
212        assert_eq!(toggles.get(TestToggles::Toggle2 as usize), false);
213    }
214
215    #[test]
216    fn set_by_name() {
217        let mut toggles: EnumToggles<TestToggles> = EnumToggles::new();
218        assert_eq!(toggles.get(TestToggles::Toggle1 as usize), false);
219        toggles.set_by_name("Toggle1", true);
220        assert_eq!(toggles.get(TestToggles::Toggle1 as usize), true);
221
222        toggles.set_by_name("Undefined_Toggle", true);
223    }
224
225    #[test]
226    fn display() {
227        let toggles: EnumToggles<TestToggles> = EnumToggles::new();
228        assert_eq!(format!("{}", toggles).is_empty(), false);
229    }
230
231    #[test]
232    fn load_from_file() {
233        // Create a temporary file
234        let mut temp_file =
235            tempfile::NamedTempFile::new().expect("Unable to create temporary file");
236
237        // Write some data to the file
238        writeln!(temp_file, "1 Toggle1").expect("Unable to write to temporary file");
239        writeln!(temp_file, "0 Toggle2").expect("Unable to write to temporary file");
240        writeln!(temp_file, "0 VAR1").expect("Unable to write to temporary file");
241        writeln!(temp_file, "TESTTEST").expect("Unable to write to temporary file");
242        writeln!(temp_file, "").expect("Unable to write to temporary file");
243
244        // Get the path of the temporary file
245        let filepath = temp_file.path().to_str().unwrap();
246
247        // Create a Toggles instance and load from the file
248        let mut toggles: EnumToggles<TestToggles> = EnumToggles::new();
249        toggles.load_from_file(filepath);
250
251        // Verify that the toggles were set correctly
252        assert_eq!(toggles.get(TestToggles::Toggle1 as usize), true);
253        assert_eq!(toggles.get(TestToggles::Toggle2 as usize), false);
254    }
255
256    #[derive(AsRefStr, EnumIter, PartialEq)]
257    pub enum DeviantToggles {
258        Toggle1 = 5,
259        Toggle2 = 10,
260    }
261
262    #[test]
263    #[should_panic(
264        expected = "Out-of-bounds access. The provided toggle_id is 5, but the array size is 2. Please use the default enum value."
265    )]
266    fn deviant_toggles() {
267        let mut toggles: EnumToggles<DeviantToggles> = EnumToggles::new();
268        toggles.set(DeviantToggles::Toggle1 as usize, true);
269    }
270}