1use 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
47pub const DEFAULT_PREFIX: &str = "TRAP_SIM";
49
50#[derive(Debug, Clone)]
52pub struct EnvApplyResult {
53 pub applied: usize,
55 pub overridden_fields: Vec<String>,
57 pub errors: Vec<EnvOverrideError>,
59}
60
61impl EnvApplyResult {
62 pub fn has_changes(&self) -> bool {
64 self.applied > 0
65 }
66
67 pub fn has_errors(&self) -> bool {
69 !self.errors.is_empty()
70 }
71
72 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#[derive(Debug, Clone)]
88pub struct EnvOverrideError {
89 pub env_var: String,
91 pub field: String,
93 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
107pub type OverrideFn<T> = Box<dyn Fn(&mut T, &str) -> std::result::Result<(), String> + Send + Sync>;
109
110pub struct EnvRule<T> {
112 pub suffix: String,
114 pub field_path: String,
116 pub description: String,
118 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
132pub struct EnvRuleBuilder<T> {
134 suffix: String,
135 field_path: String,
136 description: String,
137 _phantom: PhantomData<T>,
138}
139
140impl<T> EnvRuleBuilder<T> {
141 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 pub fn field_path(mut self, path: impl Into<String>) -> Self {
154 self.field_path = path.into();
155 self
156 }
157
158 pub fn description(mut self, desc: impl Into<String>) -> Self {
160 self.description = desc.into();
161 self
162 }
163
164 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 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 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 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
233fn 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
242pub struct EnvOverrides<T> {
244 prefix: String,
246 rules: Vec<EnvRule<T>>,
248 ignore_missing: bool,
250 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 pub fn new() -> Self {
268 Self::with_prefix(DEFAULT_PREFIX)
269 }
270
271 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 pub fn full_var_name(&self, suffix: &str) -> String {
283 format!("{}_{}", self.prefix, suffix)
284 }
285
286 pub fn add_rule(mut self, rule: EnvRule<T>) -> Self {
288 self.rules.push(rule);
289 self
290 }
291
292 pub fn ignore_missing(mut self, ignore: bool) -> Self {
294 self.ignore_missing = ignore;
295 self
296 }
297
298 pub fn fail_on_error(mut self, fail: bool) -> Self {
300 self.fail_on_error = fail;
301 self
302 }
303
304 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 }
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 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 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 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#[derive(Debug, Clone)]
388pub struct EnvVarDoc {
389 pub var_name: String,
391 pub field_path: String,
393 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
403pub trait EnvConfigurable: Sized {
405 fn env_overrides() -> EnvOverrides<Self>;
407}
408
409pub fn get_env<T: FromStr>(name: &str) -> Option<T> {
411 env::var(name).ok().and_then(|v| v.parse().ok())
412}
413
414pub fn get_env_or<T: FromStr>(name: &str, default: T) -> T {
416 get_env(name).unwrap_or(default)
417}
418
419pub fn get_env_bool(name: &str) -> Option<bool> {
421 env::var(name).ok().and_then(|v| parse_bool(&v))
422}
423
424pub fn get_env_bool_or(name: &str, default: bool) -> bool {
426 get_env_bool(name).unwrap_or(default)
427}
428
429#[derive(Debug, Default)]
431pub struct EnvSnapshot {
432 vars: HashMap<String, String>,
433}
434
435impl EnvSnapshot {
436 pub fn new() -> Self {
438 Self::default()
439 }
440
441 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 pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
451 self.vars.insert(key.into(), value.into());
452 }
453
454 pub fn apply(&self) {
456 for (key, value) in &self.vars {
457 env::set_var(key, value);
458 }
459 }
460
461 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 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 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}