rullst_core/feature/
toml.rs1use async_trait::async_trait;
2use std::collections::HashMap;
3use std::sync::{PoisonError, RwLock};
4
5use super::driver::FeatureDriver;
6use super::resolvers::parse_feature_string_value;
7
8#[non_exhaustive]
25pub struct TomlFeatureDriver {
26 pub(crate) config: RwLock<HashMap<String, String>>,
27 config_path: std::path::PathBuf,
28}
29
30impl TomlFeatureDriver {
31 pub fn new() -> Self {
33 let config_path = std::env::current_dir()
34 .unwrap_or_else(|_| std::path::PathBuf::from("."))
35 .join("Rullst.toml");
36 let driver = Self {
37 config: RwLock::new(HashMap::new()),
38 config_path,
39 };
40
41 if let Ok(content) = std::fs::read_to_string(&driver.config_path) {
42 driver.load_from_str(&content);
43 }
44
45 driver
46 }
47
48 #[cfg_attr(mutants, mutants::skip)]
50 pub async fn reload(&self) -> Result<(), Box<dyn std::error::Error>> {
51 let content = tokio::fs::read_to_string(&self.config_path).await?;
52 self.load_from_str(&content);
53 Ok(())
54 }
55
56 pub(crate) fn load_from_str(&self, content: &str) {
57 let parsed = parse_features(content);
58 *self.config.write().unwrap_or_else(PoisonError::into_inner) = parsed;
59 }
60
61 pub(crate) fn value(&self, flag: &str) -> Option<String> {
62 self.config
63 .read()
64 .unwrap_or_else(PoisonError::into_inner)
65 .get(flag)
66 .cloned()
67 }
68
69 fn evaluate(&self, value: &str, flag: &str, identifier: Option<&str>) -> Option<String> {
70 parse_feature_string_value(value, flag, identifier)
71 }
72}
73
74fn parse_features(content: &str) -> HashMap<String, String> {
76 let mut config = HashMap::new();
77 let mut in_features = false;
78 for line in content.lines() {
79 let trimmed = line.trim();
80 #[cfg_attr(mutants, mutants::skip)]
81 if trimmed.is_empty() || trimmed.starts_with('#') {
82 continue;
83 }
84 if trimmed.starts_with('[') {
85 let header = trimmed.split('#').next().unwrap_or(trimmed).trim();
87 in_features = header == "[features]";
88 continue;
89 }
90
91 if in_features {
92 let mut parts = trimmed.splitn(2, '=');
93 if let (Some(key), Some(val)) = (parts.next(), parts.next()) {
94 let k = key.trim().to_string();
95 let clean_val = val.split('#').next().unwrap_or(val).trim();
96 let v = clean_val.trim_matches('"').trim_matches('\'').to_string();
97 config.insert(k, v);
98 }
99 }
100 }
101 config
102}
103
104impl Default for TomlFeatureDriver {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110#[async_trait]
111impl FeatureDriver for TomlFeatureDriver {
112 #[cfg_attr(mutants, mutants::skip)]
113 async fn enabled(&self, flag: &str) -> Option<bool> {
114 let val = self.value(flag)?;
115 let evaluated = self.evaluate(&val, flag, None)?;
116 Some(evaluated == "enabled")
117 }
118
119 #[cfg_attr(mutants, mutants::skip)]
120 async fn enabled_for(&self, flag: &str, identifier: &str) -> Option<bool> {
121 let val = self.value(flag)?;
122 let evaluated = self.evaluate(&val, flag, Some(identifier))?;
123 Some(evaluated == "enabled")
124 }
125
126 #[cfg_attr(mutants, mutants::skip)]
127 async fn variant(&self, flag: &str, identifier: &str) -> Option<String> {
128 let val = self.value(flag)?;
129 self.evaluate(&val, flag, Some(identifier))
130 }
131}