Skip to main content

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}