qubit_config/source/env_file_config_source.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2025 - 2026.
4 * Haixing Hu, Qubit Co. Ltd.
5 *
6 * All rights reserved.
7 *
8 ******************************************************************************/
9//! # `.env` File Configuration Source
10//!
11//! Loads configuration from `.env` format files (as used by dotenv tools).
12//!
13//! # Format
14//!
15//! The `.env` format supports:
16//! - `KEY=VALUE` assignments
17//! - `# comment` lines
18//! - Quoted values: `KEY="value with spaces"` or `KEY='value'`
19//! - Export prefix: `export KEY=VALUE` (the `export` keyword is ignored)
20//!
21//! # Author
22//!
23//! Haixing Hu
24
25use std::path::{Path, PathBuf};
26
27use crate::{Config, ConfigError, ConfigResult};
28
29use super::ConfigSource;
30
31/// Configuration source that loads from `.env` format files
32///
33/// # Examples
34///
35/// ```rust,ignore
36/// use qubit_config::source::{EnvFileConfigSource, ConfigSource};
37/// use qubit_config::Config;
38///
39/// let source = EnvFileConfigSource::from_file(".env");
40/// let mut config = Config::new();
41/// source.load(&mut config).unwrap();
42/// ```
43///
44/// # Author
45///
46/// Haixing Hu
47#[derive(Debug, Clone)]
48pub struct EnvFileConfigSource {
49 path: PathBuf,
50}
51
52impl EnvFileConfigSource {
53 /// Creates a new `EnvFileConfigSource` from a file path
54 ///
55 /// # Parameters
56 ///
57 /// * `path` - Path to the `.env` file
58 #[inline]
59 pub fn from_file<P: AsRef<Path>>(path: P) -> Self {
60 Self {
61 path: path.as_ref().to_path_buf(),
62 }
63 }
64}
65
66impl ConfigSource for EnvFileConfigSource {
67 fn load(&self, config: &mut Config) -> ConfigResult<()> {
68 let iter = dotenvy::from_path_iter(&self.path).map_err(|e| {
69 ConfigError::IoError(std::io::Error::new(
70 std::io::ErrorKind::Other,
71 format!("Failed to read .env file '{}': {}", self.path.display(), e),
72 ))
73 })?;
74
75 for item in iter {
76 let (key, value) = item.map_err(|e| {
77 ConfigError::ParseError(format!(
78 "Failed to parse .env file '{}': {}",
79 self.path.display(),
80 e
81 ))
82 })?;
83 config.set(&key, value)?;
84 }
85
86 Ok(())
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::ConfigError;
94
95 #[test]
96 fn test_load_invalid_env_file_returns_parse_error() {
97 // dotenvy fails to parse files with invalid UTF-8 sequences
98 let dir = tempfile::tempdir().unwrap();
99 let path = dir.path().join("bad.env");
100 // Write invalid UTF-8 content
101 std::fs::write(&path, b"VALID=ok\n\xff\xfe=bad\n").unwrap();
102
103 let source = EnvFileConfigSource::from_file(&path);
104 let mut config = Config::new();
105 let result = source.load(&mut config);
106 // dotenvy may return an error or silently skip; either way it shouldn't panic
107 // If it returns an error, it should be ParseError
108 if let Err(e) = result {
109 assert!(matches!(
110 e,
111 ConfigError::ParseError(_) | ConfigError::IoError(_)
112 ));
113 }
114 }
115
116 #[test]
117 fn test_load_env_file_with_unclosed_quote_returns_error() {
118 let dir = tempfile::tempdir().unwrap();
119 let path = dir.path().join("unclosed.env");
120 // Unclosed quote - dotenvy should return a parse error
121 std::fs::write(&path, "KEY=\"unclosed value\n").unwrap();
122
123 let source = EnvFileConfigSource::from_file(&path);
124 let mut config = Config::new();
125 let result = source.load(&mut config);
126 // Either succeeds (dotenvy is lenient) or fails with ParseError
127 let _ = result;
128 }
129}