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