Skip to main content

mabi_core/config/
env.rs

1//! Environment variable override system.
2//!
3//! This module provides hierarchical environment variable support for configuration
4//! with type-safe parsing and customizable prefix.
5//!
6//! # Naming Convention
7//!
8//! Environment variables follow a hierarchical pattern:
9//! `{PREFIX}_{SECTION}_{FIELD}` where:
10//! - `PREFIX`: Application prefix (default: `TRAP_SIM`)
11//! - `SECTION`: Configuration section (e.g., `ENGINE`, `MODBUS`)
12//! - `FIELD`: Specific field name in SCREAMING_SNAKE_CASE
13//!
14//! # Examples
15//!
16//! ```text
17//! TRAP_SIM_ENGINE_MAX_DEVICES=50000
18//! TRAP_SIM_ENGINE_TICK_INTERVAL_MS=50
19//! TRAP_SIM_MODBUS_TCP_BIND_ADDRESS=0.0.0.0:5502
20//! TRAP_SIM_LOG_LEVEL=debug
21//! ```
22//!
23//! # Usage
24//!
25//! ```rust,ignore
26//! use mabi_core::config::env::EnvOverrides;
27//!
28//! let overrides = EnvOverrides::with_prefix("TRAP_SIM")
29//!     .add_rule("ENGINE_MAX_DEVICES", |c, v| {
30//!         c.max_devices = v.parse().ok()?;
31//!         Some(())
32//!     })
33//!     .build();
34//!
35//! overrides.apply(&mut config)?;
36//! ```
37
38use std::collections::HashMap;
39use std::env;
40use std::fmt;
41use std::marker::PhantomData;
42use std::str::FromStr;
43
44use crate::error::{Error, ValidationErrors};
45use crate::Result;
46
47/// Default environment variable prefix.
48pub const DEFAULT_PREFIX: &str = "TRAP_SIM";
49
50/// Environment variable override result.
51#[derive(Debug, Clone)]
52pub struct EnvApplyResult {
53    /// Number of overrides successfully applied.
54    pub applied: usize,
55    /// Fields that were overridden.
56    pub overridden_fields: Vec<String>,
57    /// Errors encountered during application.
58    pub errors: Vec<EnvOverrideError>,
59}
60
61impl EnvApplyResult {
62    /// Check if any overrides were applied.
63    pub fn has_changes(&self) -> bool {
64        self.applied > 0
65    }
66
67    /// Check if there were any errors.
68    pub fn has_errors(&self) -> bool {
69        !self.errors.is_empty()
70    }
71
72    /// Convert to Result, failing if there were errors.
73    pub fn into_result(self) -> Result<Self> {
74        if self.errors.is_empty() {
75            Ok(self)
76        } else {
77            let mut validation = ValidationErrors::new();
78            for err in &self.errors {
79                validation.add(&err.env_var, &err.message);
80            }
81            Err(Error::validation(validation))
82        }
83    }
84}
85
86/// Error that occurred while applying an environment override.
87#[derive(Debug, Clone)]
88pub struct EnvOverrideError {
89    /// Environment variable name.
90    pub env_var: String,
91    /// Field path being overridden.
92    pub field: String,
93    /// Error message.
94    pub message: String,
95}
96
97impl fmt::Display for EnvOverrideError {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        write!(
100            f,
101            "Failed to apply {} to {}: {}",
102            self.env_var, self.field, self.message
103        )
104    }
105}
106
107/// Type for override application functions.
108pub type OverrideFn<T> = Box<dyn Fn(&mut T, &str) -> std::result::Result<(), String> + Send + Sync>;
109
110/// Rule for applying an environment variable override.
111pub struct EnvRule<T> {
112    /// Environment variable suffix (without prefix).
113    pub suffix: String,
114    /// Target field path for documentation.
115    pub field_path: String,
116    /// Description of what this override does.
117    pub description: String,
118    /// Function to apply the override.
119    pub apply: OverrideFn<T>,
120}
121
122impl<T> fmt::Debug for EnvRule<T> {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        f.debug_struct("EnvRule")
125            .field("suffix", &self.suffix)
126            .field("field_path", &self.field_path)
127            .field("description", &self.description)
128            .finish()
129    }
130}
131
132/// Builder for creating environment override rules.
133pub struct EnvRuleBuilder<T> {
134    suffix: String,
135    field_path: String,
136    description: String,
137    _phantom: PhantomData<T>,
138}
139
140impl<T> EnvRuleBuilder<T> {
141    /// Create a new rule builder.
142    pub fn new(suffix: impl Into<String>) -> Self {
143        let suffix = suffix.into();
144        Self {
145            field_path: suffix.to_lowercase().replace('_', "."),
146            suffix,
147            description: String::new(),
148            _phantom: PhantomData,
149        }
150    }
151
152    /// Set the field path for documentation.
153    pub fn field_path(mut self, path: impl Into<String>) -> Self {
154        self.field_path = path.into();
155        self
156    }
157
158    /// Set the description.
159    pub fn description(mut self, desc: impl Into<String>) -> Self {
160        self.description = desc.into();
161        self
162    }
163
164    /// Build with a parsing function for types that implement FromStr.
165    pub fn parse_into<F, V>(self, setter: F) -> EnvRule<T>
166    where
167        F: Fn(&mut T, V) + Send + Sync + 'static,
168        V: FromStr,
169        V::Err: fmt::Display,
170    {
171        EnvRule {
172            suffix: self.suffix,
173            field_path: self.field_path,
174            description: self.description,
175            apply: Box::new(move |config, value| {
176                let parsed = value
177                    .parse::<V>()
178                    .map_err(|e| format!("Failed to parse: {}", e))?;
179                setter(config, parsed);
180                Ok(())
181            }),
182        }
183    }
184
185    /// Build with a custom application function.
186    pub fn apply_with<F>(self, f: F) -> EnvRule<T>
187    where
188        F: Fn(&mut T, &str) -> std::result::Result<(), String> + Send + Sync + 'static,
189    {
190        EnvRule {
191            suffix: self.suffix,
192            field_path: self.field_path,
193            description: self.description,
194            apply: Box::new(f),
195        }
196    }
197
198    /// Build for string fields (no parsing needed).
199    pub fn as_string<F>(self, setter: F) -> EnvRule<T>
200    where
201        F: Fn(&mut T, String) + Send + Sync + 'static,
202    {
203        EnvRule {
204            suffix: self.suffix,
205            field_path: self.field_path,
206            description: self.description,
207            apply: Box::new(move |config, value| {
208                setter(config, value.to_string());
209                Ok(())
210            }),
211        }
212    }
213
214    /// Build for boolean fields with flexible parsing.
215    pub fn as_bool<F>(self, setter: F) -> EnvRule<T>
216    where
217        F: Fn(&mut T, bool) + Send + Sync + 'static,
218    {
219        EnvRule {
220            suffix: self.suffix,
221            field_path: self.field_path,
222            description: self.description,
223            apply: Box::new(move |config, value| {
224                let parsed = parse_bool(value)
225                    .ok_or_else(|| format!("Invalid boolean value: {}", value))?;
226                setter(config, parsed);
227                Ok(())
228            }),
229        }
230    }
231}
232
233/// Parse a boolean from various string representations.
234fn parse_bool(s: &str) -> Option<bool> {
235    match s.to_lowercase().as_str() {
236        "true" | "1" | "yes" | "on" | "enabled" => Some(true),
237        "false" | "0" | "no" | "off" | "disabled" => Some(false),
238        _ => None,
239    }
240}
241
242/// Environment variable overrides configuration.
243pub struct EnvOverrides<T> {
244    /// Prefix for all environment variables.
245    prefix: String,
246    /// Override rules.
247    rules: Vec<EnvRule<T>>,
248    /// Whether to ignore missing environment variables.
249    ignore_missing: bool,
250    /// Whether to fail on parse errors.
251    fail_on_error: bool,
252}
253
254impl<T> fmt::Debug for EnvOverrides<T> {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        f.debug_struct("EnvOverrides")
257            .field("prefix", &self.prefix)
258            .field("rules_count", &self.rules.len())
259            .field("ignore_missing", &self.ignore_missing)
260            .field("fail_on_error", &self.fail_on_error)
261            .finish()
262    }
263}
264
265impl<T> EnvOverrides<T> {
266    /// Create a new environment overrides handler with default prefix.
267    pub fn new() -> Self {
268        Self::with_prefix(DEFAULT_PREFIX)
269    }
270
271    /// Create with a custom prefix.
272    pub fn with_prefix(prefix: impl Into<String>) -> Self {
273        Self {
274            prefix: prefix.into(),
275            rules: Vec::new(),
276            ignore_missing: true,
277            fail_on_error: false,
278        }
279    }
280
281    /// Get the full environment variable name for a suffix.
282    pub fn full_var_name(&self, suffix: &str) -> String {
283        format!("{}_{}", self.prefix, suffix)
284    }
285
286    /// Add a rule to the overrides.
287    pub fn add_rule(mut self, rule: EnvRule<T>) -> Self {
288        self.rules.push(rule);
289        self
290    }
291
292    /// Set whether to ignore missing environment variables.
293    pub fn ignore_missing(mut self, ignore: bool) -> Self {
294        self.ignore_missing = ignore;
295        self
296    }
297
298    /// Set whether to fail on parse errors.
299    pub fn fail_on_error(mut self, fail: bool) -> Self {
300        self.fail_on_error = fail;
301        self
302    }
303
304    /// Apply all environment overrides to the configuration.
305    pub fn apply(&self, config: &mut T) -> EnvApplyResult {
306        let mut result = EnvApplyResult {
307            applied: 0,
308            overridden_fields: Vec::new(),
309            errors: Vec::new(),
310        };
311
312        for rule in &self.rules {
313            let var_name = self.full_var_name(&rule.suffix);
314
315            match env::var(&var_name) {
316                Ok(value) => {
317                    match (rule.apply)(config, &value) {
318                        Ok(()) => {
319                            result.applied += 1;
320                            result.overridden_fields.push(rule.field_path.clone());
321                            tracing::debug!(
322                                env_var = %var_name,
323                                field = %rule.field_path,
324                                "Applied environment override"
325                            );
326                        }
327                        Err(msg) => {
328                            result.errors.push(EnvOverrideError {
329                                env_var: var_name,
330                                field: rule.field_path.clone(),
331                                message: msg,
332                            });
333                        }
334                    }
335                }
336                Err(env::VarError::NotPresent) => {
337                    // Variable not set, skip
338                }
339                Err(env::VarError::NotUnicode(_)) => {
340                    result.errors.push(EnvOverrideError {
341                        env_var: var_name,
342                        field: rule.field_path.clone(),
343                        message: "Value is not valid UTF-8".to_string(),
344                    });
345                }
346            }
347        }
348
349        result
350    }
351
352    /// Apply and return Result based on fail_on_error setting.
353    pub fn apply_checked(&self, config: &mut T) -> Result<EnvApplyResult> {
354        let result = self.apply(config);
355        if self.fail_on_error && result.has_errors() {
356            result.into_result()
357        } else {
358            Ok(result)
359        }
360    }
361
362    /// Get documentation for all registered rules.
363    pub fn documentation(&self) -> Vec<EnvVarDoc> {
364        self.rules
365            .iter()
366            .map(|rule| EnvVarDoc {
367                var_name: self.full_var_name(&rule.suffix),
368                field_path: rule.field_path.clone(),
369                description: rule.description.clone(),
370            })
371            .collect()
372    }
373
374    /// Get the number of registered rules.
375    pub fn rule_count(&self) -> usize {
376        self.rules.len()
377    }
378}
379
380impl<T> Default for EnvOverrides<T> {
381    fn default() -> Self {
382        Self::new()
383    }
384}
385
386/// Documentation entry for an environment variable.
387#[derive(Debug, Clone)]
388pub struct EnvVarDoc {
389    /// Full environment variable name.
390    pub var_name: String,
391    /// Configuration field path.
392    pub field_path: String,
393    /// Description.
394    pub description: String,
395}
396
397impl fmt::Display for EnvVarDoc {
398    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399        write!(f, "{}: {} - {}", self.var_name, self.field_path, self.description)
400    }
401}
402
403/// Helper trait for creating typed environment rules.
404pub trait EnvConfigurable: Sized {
405    /// Create environment overrides for this type.
406    fn env_overrides() -> EnvOverrides<Self>;
407}
408
409/// Convenience function to read and parse an environment variable.
410pub fn get_env<T: FromStr>(name: &str) -> Option<T> {
411    env::var(name).ok().and_then(|v| v.parse().ok())
412}
413
414/// Convenience function to read an environment variable with a default.
415pub fn get_env_or<T: FromStr>(name: &str, default: T) -> T {
416    get_env(name).unwrap_or(default)
417}
418
419/// Read an environment variable as a boolean with flexible parsing.
420pub fn get_env_bool(name: &str) -> Option<bool> {
421    env::var(name).ok().and_then(|v| parse_bool(&v))
422}
423
424/// Read an environment variable as a boolean with a default.
425pub fn get_env_bool_or(name: &str, default: bool) -> bool {
426    get_env_bool(name).unwrap_or(default)
427}
428
429/// A collection of environment variable values for testing.
430#[derive(Debug, Default)]
431pub struct EnvSnapshot {
432    vars: HashMap<String, String>,
433}
434
435impl EnvSnapshot {
436    /// Create a new empty snapshot.
437    pub fn new() -> Self {
438        Self::default()
439    }
440
441    /// Capture current environment variables with a specific prefix.
442    pub fn capture(prefix: &str) -> Self {
443        let vars = env::vars()
444            .filter(|(k, _)| k.starts_with(prefix))
445            .collect();
446        Self { vars }
447    }
448
449    /// Set a variable in this snapshot.
450    pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
451        self.vars.insert(key.into(), value.into());
452    }
453
454    /// Apply this snapshot to the environment.
455    pub fn apply(&self) {
456        for (key, value) in &self.vars {
457            env::set_var(key, value);
458        }
459    }
460
461    /// Clear variables from environment (for testing).
462    pub fn clear_from_env(&self) {
463        for key in self.vars.keys() {
464            env::remove_var(key);
465        }
466    }
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472
473    #[derive(Debug, Default)]
474    struct TestConfig {
475        max_devices: usize,
476        name: String,
477        enabled: bool,
478    }
479
480    fn test_overrides() -> EnvOverrides<TestConfig> {
481        EnvOverrides::with_prefix("TEST")
482            .add_rule(
483                EnvRuleBuilder::new("MAX_DEVICES")
484                    .field_path("max_devices")
485                    .description("Maximum device count")
486                    .parse_into(|c: &mut TestConfig, v: usize| c.max_devices = v),
487            )
488            .add_rule(
489                EnvRuleBuilder::new("NAME")
490                    .field_path("name")
491                    .description("Config name")
492                    .as_string(|c: &mut TestConfig, v| c.name = v),
493            )
494            .add_rule(
495                EnvRuleBuilder::new("ENABLED")
496                    .field_path("enabled")
497                    .description("Enable flag")
498                    .as_bool(|c: &mut TestConfig, v| c.enabled = v),
499            )
500    }
501
502    #[test]
503    fn test_env_var_name() {
504        let overrides: EnvOverrides<TestConfig> = EnvOverrides::with_prefix("TRAP_SIM");
505        assert_eq!(
506            overrides.full_var_name("ENGINE_MAX_DEVICES"),
507            "TRAP_SIM_ENGINE_MAX_DEVICES"
508        );
509    }
510
511    #[test]
512    fn test_parse_bool() {
513        assert_eq!(parse_bool("true"), Some(true));
514        assert_eq!(parse_bool("True"), Some(true));
515        assert_eq!(parse_bool("1"), Some(true));
516        assert_eq!(parse_bool("yes"), Some(true));
517        assert_eq!(parse_bool("on"), Some(true));
518        assert_eq!(parse_bool("enabled"), Some(true));
519
520        assert_eq!(parse_bool("false"), Some(false));
521        assert_eq!(parse_bool("False"), Some(false));
522        assert_eq!(parse_bool("0"), Some(false));
523        assert_eq!(parse_bool("no"), Some(false));
524        assert_eq!(parse_bool("off"), Some(false));
525        assert_eq!(parse_bool("disabled"), Some(false));
526
527        assert_eq!(parse_bool("invalid"), None);
528    }
529
530    #[test]
531    fn test_apply_overrides() {
532        // Set test environment variables
533        env::set_var("TEST_MAX_DEVICES", "5000");
534        env::set_var("TEST_NAME", "test-config");
535        env::set_var("TEST_ENABLED", "true");
536
537        let overrides = test_overrides();
538        let mut config = TestConfig::default();
539
540        let result = overrides.apply(&mut config);
541
542        assert_eq!(result.applied, 3);
543        assert_eq!(config.max_devices, 5000);
544        assert_eq!(config.name, "test-config");
545        assert!(config.enabled);
546
547        // Cleanup
548        env::remove_var("TEST_MAX_DEVICES");
549        env::remove_var("TEST_NAME");
550        env::remove_var("TEST_ENABLED");
551    }
552
553    #[test]
554    fn test_parse_error() {
555        env::set_var("TEST_MAX_DEVICES", "not_a_number");
556
557        let overrides = test_overrides();
558        let mut config = TestConfig::default();
559
560        let result = overrides.apply(&mut config);
561
562        assert!(result.has_errors());
563        assert_eq!(result.errors.len(), 1);
564        assert!(result.errors[0].message.contains("Failed to parse"));
565
566        env::remove_var("TEST_MAX_DEVICES");
567    }
568
569    #[test]
570    fn test_documentation() {
571        let overrides = test_overrides();
572        let docs = overrides.documentation();
573
574        assert_eq!(docs.len(), 3);
575        assert_eq!(docs[0].var_name, "TEST_MAX_DEVICES");
576        assert_eq!(docs[0].field_path, "max_devices");
577    }
578
579    #[test]
580    fn test_env_snapshot() {
581        let mut snapshot = EnvSnapshot::new();
582        snapshot.set("TEST_VAR", "value");
583        snapshot.apply();
584
585        assert_eq!(env::var("TEST_VAR").ok(), Some("value".to_string()));
586
587        snapshot.clear_from_env();
588        assert!(env::var("TEST_VAR").is_err());
589    }
590}