scirs2_core/
config.rs

1//! Configuration system for ``SciRS2``
2//!
3//! This module provides a centralized configuration system for ``SciRS2``, allowing
4//! users to customize the behavior of various algorithms and functions.
5
6use std::collections::HashMap;
7use std::env;
8use std::fmt;
9use std::sync::{Arc, Mutex, RwLock};
10
11use crate::error::{CoreError, CoreResult, ErrorContext, ErrorLocation};
12
13/// Default precision for floating-point comparisons
14pub const DEFAULT_FLOAT_EPS: f64 = 1e-10;
15
16/// Default number of threads to use for parallel operations
17pub const DEFAULT_NUM_THREADS: usize = 4;
18
19/// Default maximum iterations for iterative algorithms
20pub const DEFAULT_MAX_ITERATIONS: usize = 1000;
21
22/// Default memory limit for operations (in bytes)
23pub const DEFAULT_MEMORY_LIMIT: usize = 1_073_741_824; // 1 GB
24
25/// Global configuration for ``SciRS2``
26///
27/// This struct is not intended to be instantiated directly.
28/// Instead, use the `get_config` and `set_config` functions to access and modify the configuration.
29#[derive(Debug, Clone)]
30pub struct Config {
31    /// Configuration values
32    values: HashMap<String, ConfigValue>,
33    /// Environment prefix for ``SciRS2`` environment variables
34    env_prefix: String,
35}
36
37/// Configuration value types
38#[derive(Debug, Clone)]
39pub enum ConfigValue {
40    /// Boolean value
41    Bool(bool),
42    /// Integer value
43    Int(i64),
44    /// Unsigned integer value
45    UInt(u64),
46    /// Floating-point value
47    Float(f64),
48    /// String value
49    String(String),
50}
51
52impl fmt::Display for ConfigValue {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            ConfigValue::Bool(b) => write!(f, "{b}"),
56            ConfigValue::Int(i) => write!(f, "{i}"),
57            ConfigValue::UInt(u) => write!(f, "{u}"),
58            ConfigValue::Float(fl) => write!(f, "{fl}"),
59            ConfigValue::String(s) => write!(f, "{s}"),
60        }
61    }
62}
63
64impl Default for Config {
65    fn default() -> Self {
66        let mut config = Self {
67            values: HashMap::new(),
68            env_prefix: "SCIRS_".to_string(),
69        };
70
71        // Set default values
72        config.set_default("float_eps", ConfigValue::Float(DEFAULT_FLOAT_EPS));
73        config.set_default("num_threads", ConfigValue::UInt(DEFAULT_NUM_THREADS as u64));
74        config.set_default(
75            "max_iterations",
76            ConfigValue::UInt(DEFAULT_MAX_ITERATIONS as u64),
77        );
78        config.set_default(
79            "memory_limit",
80            ConfigValue::UInt(DEFAULT_MEMORY_LIMIT as u64),
81        );
82        config.set_default("parallel_enabled", ConfigValue::Bool(true));
83        config.set_default("debug_mode", ConfigValue::Bool(false));
84        config.set_default("suppress_warnings", ConfigValue::Bool(false));
85
86        // Load values from environment variables
87        config.load_from_env();
88
89        config
90    }
91}
92
93impl Config {
94    /// Create a new configuration with default values
95    #[must_use]
96    pub fn new() -> Self {
97        Self::default()
98    }
99
100    /// Get a configuration value
101    #[must_use]
102    pub fn get(&self, key: &str) -> Option<&ConfigValue> {
103        self.values.get(key)
104    }
105
106    /// Set a configuration value
107    pub fn set(&mut self, key: &str, value: ConfigValue) {
108        self.values.insert(key.to_string(), value);
109    }
110
111    /// Set a default configuration value (only if the key doesn't exist)
112    fn set_default(&mut self, key: &str, value: ConfigValue) {
113        self.values.entry(key.to_string()).or_insert(value);
114    }
115
116    /// Get a boolean configuration value
117    ///
118    /// # Errors
119    ///
120    /// Returns `CoreError::ConfigError` if the key is not found or if the value is not a boolean.
121    pub fn get_bool(&self, key: &str) -> CoreResult<bool> {
122        match self.get(key) {
123            Some(ConfigValue::Bool(b)) => Ok(*b),
124            Some(value) => Err(CoreError::ConfigError(
125                ErrorContext::new(format!(
126                    "Expected boolean value for key '{key}', got: {value}"
127                ))
128                .with_location(ErrorLocation::new(file!(), line!())),
129            )),
130            None => Err(CoreError::ConfigError(
131                ErrorContext::new(format!("Configuration key '{key}' not found"))
132                    .with_location(ErrorLocation::new(file!(), line!())),
133            )),
134        }
135    }
136
137    /// Get an integer configuration value
138    ///
139    /// # Errors
140    ///
141    /// Returns `CoreError::ConfigError` if the key is not found or if the value is not an integer.
142    pub fn get_int(&self, key: &str) -> CoreResult<i64> {
143        match self.get(key) {
144            Some(ConfigValue::Int(i)) => Ok(*i),
145            Some(ConfigValue::UInt(u)) if *u <= i64::MAX as u64 => Ok(*u as i64),
146            Some(value) => Err(CoreError::ConfigError(
147                ErrorContext::new(format!(
148                    "Expected integer value for key '{key}', got: {value}"
149                ))
150                .with_location(ErrorLocation::new(file!(), line!())),
151            )),
152            None => Err(CoreError::ConfigError(
153                ErrorContext::new(format!("Configuration key '{key}' not found"))
154                    .with_location(ErrorLocation::new(file!(), line!())),
155            )),
156        }
157    }
158
159    /// Get an unsigned integer configuration value
160    ///
161    /// # Errors
162    ///
163    /// Returns `CoreError::ConfigError` if the key is not found or if the value is not an unsigned integer.
164    pub fn get_uint(&self, key: &str) -> CoreResult<u64> {
165        match self.get(key) {
166            Some(ConfigValue::UInt(u)) => Ok(*u),
167            Some(ConfigValue::Int(i)) if *i >= 0 => Ok(*i as u64),
168            Some(value) => Err(CoreError::ConfigError(
169                ErrorContext::new(format!(
170                    "Expected unsigned integer value for key '{key}', got: {value}"
171                ))
172                .with_location(ErrorLocation::new(file!(), line!())),
173            )),
174            None => Err(CoreError::ConfigError(
175                ErrorContext::new(format!("Configuration key '{key}' not found"))
176                    .with_location(ErrorLocation::new(file!(), line!())),
177            )),
178        }
179    }
180
181    /// Get a floating-point configuration value
182    ///
183    /// # Errors
184    ///
185    /// Returns `CoreError::ConfigError` if the key is not found or if the value cannot be converted to a float.
186    pub fn get_float(&self, key: &str) -> CoreResult<f64> {
187        match self.get(key) {
188            Some(ConfigValue::Float(f)) => Ok(*f),
189            Some(ConfigValue::Int(i)) => Ok(*i as f64),
190            Some(ConfigValue::UInt(u)) => Ok(*u as f64),
191            Some(value) => Err(CoreError::ConfigError(
192                ErrorContext::new(format!(
193                    "Expected float value for key '{key}', got: {value}"
194                ))
195                .with_location(ErrorLocation::new(file!(), line!())),
196            )),
197            None => Err(CoreError::ConfigError(
198                ErrorContext::new(format!("Configuration key '{key}' not found"))
199                    .with_location(ErrorLocation::new(file!(), line!())),
200            )),
201        }
202    }
203
204    /// Get a string configuration value
205    ///
206    /// # Errors
207    ///
208    /// Returns `CoreError::ConfigError` if the key is not found.
209    pub fn get_string(&self, key: &str) -> CoreResult<String> {
210        match self.get(key) {
211            Some(ConfigValue::String(s)) => Ok(s.clone()),
212            Some(value) => Ok(value.to_string()),
213            None => Err(CoreError::ConfigError(
214                ErrorContext::new(format!("Configuration key '{key}' not found"))
215                    .with_location(ErrorLocation::new(file!(), line!())),
216            )),
217        }
218    }
219
220    /// Load configuration values from environment variables
221    fn load_from_env(&mut self) {
222        // Find all environment variables with the configured prefix
223        for (key, value) in env::vars() {
224            if key.starts_with(&self.env_prefix) {
225                let config_key = key[self.env_prefix.len()..].to_lowercase();
226
227                // Try to parse the value as different types
228                if let Ok(bool_val) = value.parse::<bool>() {
229                    self.set(&config_key, ConfigValue::Bool(bool_val));
230                } else if let Ok(int_val) = value.parse::<i64>() {
231                    self.set(&config_key, ConfigValue::Int(int_val));
232                } else if let Ok(uint_val) = value.parse::<u64>() {
233                    self.set(&config_key, ConfigValue::UInt(uint_val));
234                } else if let Ok(float_val) = value.parse::<f64>() {
235                    self.set(&config_key, ConfigValue::Float(float_val));
236                } else {
237                    self.set(&config_key, ConfigValue::String(value));
238                }
239            }
240        }
241    }
242}
243
244static GLOBAL_CONFIG: std::sync::LazyLock<RwLock<Config>> =
245    std::sync::LazyLock::new(|| RwLock::new(Config::default()));
246
247thread_local! {
248    static THREAD_LOCAL_CONFIG: Arc<Mutex<Option<Config>>> = Arc::new(Mutex::new(None));
249}
250
251/// Get the current configuration
252///
253/// This function first checks for a thread-local configuration, and falls back to the global configuration.
254#[must_use]
255#[allow(dead_code)]
256pub fn get_config() -> Config {
257    // Try to get thread-local config first
258    let thread_local = THREAD_LOCAL_CONFIG.with(|config| {
259        let config_lock = config.lock().unwrap();
260        config_lock.clone()
261    });
262
263    // If thread-local config exists, use it, otherwise use global config
264    match thread_local {
265        Some(config) => config,
266        None => GLOBAL_CONFIG.read().unwrap().clone(),
267    }
268}
269
270/// Set the global configuration
271#[allow(dead_code)]
272pub fn set_global_config(config: Config) {
273    let mut global_config = GLOBAL_CONFIG.write().unwrap();
274    *global_config = config;
275}
276
277/// Set a thread-local configuration
278#[allow(dead_code)]
279pub fn set_thread_local_config(config: Config) {
280    THREAD_LOCAL_CONFIG.with(|thread_config| {
281        let mut config_lock = thread_config.lock().unwrap();
282        *config_lock = Some(config);
283    });
284}
285
286/// Clear the thread-local configuration
287#[allow(dead_code)]
288pub fn clear_thread_local_config() {
289    THREAD_LOCAL_CONFIG.with(|thread_config| {
290        let mut config_lock = thread_config.lock().unwrap();
291        *config_lock = None;
292    });
293}
294
295/// Set a global configuration value
296#[allow(dead_code)]
297pub fn set_config_value(key: &str, value: ConfigValue) {
298    let mut global_config = GLOBAL_CONFIG.write().unwrap();
299    global_config.set(key, value);
300}
301
302/// Get a global configuration value
303#[must_use]
304#[allow(dead_code)]
305pub fn get_config_value(key: &str) -> Option<ConfigValue> {
306    let config = get_config();
307    config.get(key).cloned()
308}
309
310/// Set a thread-local configuration value
311#[allow(dead_code)]
312pub fn set_thread_local_config_value(key: &str, value: ConfigValue) {
313    THREAD_LOCAL_CONFIG.with(|thread_config| {
314        let mut config_lock = thread_config.lock().unwrap();
315
316        // Create a new config from global if it doesn't exist
317        if config_lock.is_none() {
318            let global_config = GLOBAL_CONFIG.read().unwrap().clone();
319            *config_lock = Some(global_config);
320        }
321
322        // Now set the value
323        if let Some(config) = config_lock.as_mut() {
324            config.set(key, value);
325        }
326    });
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn test_default_config() {
335        let config = Config::default();
336
337        assert!(matches!(
338            config.get("float_eps"),
339            Some(ConfigValue::Float(_))
340        ));
341        assert!(matches!(
342            config.get("num_threads"),
343            Some(ConfigValue::UInt(_))
344        ));
345        assert!(matches!(
346            config.get("max_iterations"),
347            Some(ConfigValue::UInt(_))
348        ));
349        assert!(matches!(
350            config.get("memory_limit"),
351            Some(ConfigValue::UInt(_))
352        ));
353    }
354
355    #[test]
356    fn test_config_get_methods() {
357        let mut config = Config::default();
358
359        config.set("test_bool", ConfigValue::Bool(true));
360        config.set("test_int", ConfigValue::Int(42));
361        config.set("test_uint", ConfigValue::UInt(100));
362        config.set("test_float", ConfigValue::Float(3.5));
363        config.set("test_string", ConfigValue::String("hello".to_string()));
364
365        assert!(config.get_bool("test_bool").unwrap());
366        assert_eq!(config.get_int("test_int").unwrap(), 42);
367        assert_eq!(config.get_uint("test_uint").unwrap(), 100);
368        assert_eq!(config.get_float("test_float").unwrap(), 3.5);
369        assert_eq!(config.get_string("test_string").unwrap(), "hello");
370
371        // Test type conversions
372        assert_eq!(config.get_float("test_int").unwrap(), 42.0);
373        assert_eq!(config.get_int("test_uint").unwrap(), 100);
374
375        // Test error cases
376        assert!(config.get_bool("nonexistent").is_err());
377        assert!(config.get_bool("test_int").is_err());
378    }
379
380    #[test]
381    fn test_global_config() {
382        // Use a unique test key to avoid conflicts with other tests
383        let process_id = std::process::id();
384        let test_key = format!("{process_id}");
385
386        // Store original value if it exists
387        let original_value = GLOBAL_CONFIG.read().unwrap().values.get(&test_key).cloned();
388
389        // Set test value using a scoped lock to minimize interference
390        {
391            let mut global_config = GLOBAL_CONFIG.write().unwrap();
392            global_config.set(&test_key, ConfigValue::String("test_value".to_string()));
393        }
394
395        // Verify the value was set correctly
396        {
397            let config = get_config();
398            assert_eq!(config.get_string(&test_key).unwrap(), "test_value");
399        }
400
401        // Clean up by restoring original state
402        {
403            let mut global_config = GLOBAL_CONFIG.write().unwrap();
404            if let Some(original) = original_value {
405                global_config.set(&test_key, original);
406            } else {
407                global_config.values.remove(&test_key);
408            }
409        }
410    }
411
412    #[test]
413    fn test_thread_local_config() {
414        let test_key = "test_thread_key";
415        let original = get_config();
416
417        {
418            // First set a global value
419            let mut global_config = GLOBAL_CONFIG.write().unwrap();
420            global_config.set(test_key, ConfigValue::String("global".to_string()));
421        }
422
423        // Set a thread-local value
424        let mut thread_config = Config::default();
425        thread_config.set(test_key, ConfigValue::String("thread-local".to_string()));
426        set_thread_local_config(thread_config);
427
428        // Thread-local should take precedence
429        let config = get_config();
430        assert_eq!(config.get_string(test_key).unwrap(), "thread-local");
431
432        // Clear thread-local config
433        clear_thread_local_config();
434
435        // Need to verify thread-local is gone
436        let thread_result = THREAD_LOCAL_CONFIG.with(|config| {
437            let locked = config.lock().unwrap();
438            locked.is_none()
439        });
440        assert!(thread_result, "Thread-local config should be cleared");
441
442        // Restore original config
443        set_global_config(original);
444
445        // Clean up by removing the test key
446        let mut final_config = GLOBAL_CONFIG.write().unwrap();
447        final_config.values.remove(test_key);
448    }
449}
450
451/// Production-level configuration management with validation, hot reloading, and feature flags
452pub mod production;