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 std::path::PathBuf;
100
101    #[test]
102    fn test_load_invalid_env_file_returns_parse_error() {
103        let dir = tempfile::tempdir().unwrap();
104        let source = EnvFileConfigSource::from_file(dir.path());
105        let mut config = Config::new();
106        let result = source.load(&mut config);
107        result.expect_err("loading a directory as an .env file should fail");
108    }
109
110    #[test]
111    fn test_load_env_file_with_unclosed_quote_returns_error() {
112        let dir = tempfile::tempdir().unwrap();
113        let path = dir.path().join("unclosed.env");
114        // Unclosed quote - dotenvy should return a parse error
115        std::fs::write(&path, "KEY=\"unclosed value\n").unwrap();
116
117        let source = EnvFileConfigSource::from_file(&path);
118        let mut config = Config::new();
119        let result = source.load(&mut config);
120        // Either succeeds (dotenvy is lenient) or fails with ParseError
121        let _ = result;
122    }
123
124    #[test]
125    fn test_from_file_stores_path() {
126        let path = PathBuf::from("config.env");
127        let source = EnvFileConfigSource::from_file(&path);
128        let cloned = source.clone();
129        assert_eq!(source.path, path);
130        assert_eq!(cloned.path, PathBuf::from("config.env"));
131    }
132}