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
36/// use qubit_config::source::{EnvFileConfigSource, ConfigSource};
37/// use qubit_config::Config;
38///
39/// let temp_dir = tempfile::tempdir().unwrap();
40/// let path = temp_dir.path().join(".env");
41/// std::fs::write(&path, "PORT=8080\n").unwrap();
42/// let source = EnvFileConfigSource::from_file(path);
43/// let mut config = Config::new();
44/// source.load(&mut config).unwrap();
45/// let port = config.get::<String>("PORT").unwrap();
46/// assert_eq!(port, "8080");
47/// ```
48///
49/// # Author
50///
51/// Haixing Hu
52#[derive(Debug, Clone)]
53pub struct EnvFileConfigSource {
54    path: PathBuf,
55}
56
57impl EnvFileConfigSource {
58    /// Creates a new `EnvFileConfigSource` from a file path
59    ///
60    /// # Parameters
61    ///
62    /// * `path` - Path to the `.env` file
63    #[inline]
64    pub fn from_file<P: AsRef<Path>>(path: P) -> Self {
65        Self {
66            path: path.as_ref().to_path_buf(),
67        }
68    }
69}
70
71impl ConfigSource for EnvFileConfigSource {
72    fn load(&self, config: &mut Config) -> ConfigResult<()> {
73        let iter = dotenvy::from_path_iter(&self.path).map_err(|e| {
74            ConfigError::IoError(std::io::Error::other(format!(
75                "Failed to read .env file '{}': {}",
76                self.path.display(),
77                e
78            )))
79        })?;
80
81        for item in iter {
82            let (key, value) = item.map_err(|e| {
83                ConfigError::ParseError(format!(
84                    "Failed to parse .env file '{}': {}",
85                    self.path.display(),
86                    e
87                ))
88            })?;
89            config.set(&key, value)?;
90        }
91
92        Ok(())
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::ConfigError;
100
101    #[test]
102    fn test_load_invalid_env_file_returns_parse_error() {
103        // dotenvy fails to parse files with invalid UTF-8 sequences
104        let dir = tempfile::tempdir().unwrap();
105        let path = dir.path().join("bad.env");
106        // Write invalid UTF-8 content
107        std::fs::write(&path, b"VALID=ok\n\xff\xfe=bad\n").unwrap();
108
109        let source = EnvFileConfigSource::from_file(&path);
110        let mut config = Config::new();
111        let result = source.load(&mut config);
112        // dotenvy may return an error or silently skip; either way it shouldn't panic
113        // If it returns an error, it should be ParseError
114        if let Err(e) = result {
115            assert!(matches!(
116                e,
117                ConfigError::ParseError(_) | ConfigError::IoError(_)
118            ));
119        }
120    }
121
122    #[test]
123    fn test_load_env_file_with_unclosed_quote_returns_error() {
124        let dir = tempfile::tempdir().unwrap();
125        let path = dir.path().join("unclosed.env");
126        // Unclosed quote - dotenvy should return a parse error
127        std::fs::write(&path, "KEY=\"unclosed value\n").unwrap();
128
129        let source = EnvFileConfigSource::from_file(&path);
130        let mut config = Config::new();
131        let result = source.load(&mut config);
132        // Either succeeds (dotenvy is lenient) or fails with ParseError
133        let _ = result;
134    }
135}