use std::collections::HashMap;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
pub trait ConfigEnv: Send + Sync {
fn read_file(&self, path: &Path) -> io::Result<String>;
fn file_exists(&self, path: &Path) -> bool;
fn is_directory(&self, path: &Path) -> bool;
fn get_env(&self, name: &str) -> Option<String>;
fn env_vars_with_prefix(&self, prefix: &str) -> Vec<(String, String)>;
fn all_env_vars(&self) -> Vec<(String, String)>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RealEnv;
impl RealEnv {
pub fn new() -> Self {
Self
}
}
impl ConfigEnv for RealEnv {
fn read_file(&self, path: &Path) -> io::Result<String> {
std::fs::read_to_string(path)
}
fn file_exists(&self, path: &Path) -> bool {
path.is_file()
}
fn is_directory(&self, path: &Path) -> bool {
path.is_dir()
}
fn get_env(&self, name: &str) -> Option<String> {
std::env::var(name).ok()
}
fn env_vars_with_prefix(&self, prefix: &str) -> Vec<(String, String)> {
std::env::vars()
.filter(|(k, _)| k.starts_with(prefix))
.collect()
}
fn all_env_vars(&self) -> Vec<(String, String)> {
std::env::vars().collect()
}
}
#[derive(Debug, Clone)]
enum MockFile {
Content(String),
NotFound,
PermissionDenied,
}
#[derive(Debug, Default)]
pub struct MockEnv {
files: RwLock<HashMap<PathBuf, MockFile>>,
env_vars: RwLock<HashMap<String, String>>,
directories: RwLock<Vec<PathBuf>>,
}
impl MockEnv {
pub fn new() -> Self {
Self::default()
}
pub fn with_file(self, path: impl Into<PathBuf>, content: impl Into<String>) -> Self {
self.files
.write()
.unwrap()
.insert(path.into(), MockFile::Content(content.into()));
self
}
pub fn with_missing_file(self, path: impl Into<PathBuf>) -> Self {
self.files
.write()
.unwrap()
.insert(path.into(), MockFile::NotFound);
self
}
pub fn with_unreadable_file(self, path: impl Into<PathBuf>) -> Self {
self.files
.write()
.unwrap()
.insert(path.into(), MockFile::PermissionDenied);
self
}
pub fn with_directory(self, path: impl Into<PathBuf>) -> Self {
self.directories.write().unwrap().push(path.into());
self
}
pub fn with_env(self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.env_vars
.write()
.unwrap()
.insert(name.into(), value.into());
self
}
pub fn with_envs<I, K, V>(self, vars: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
let mut env_vars = self.env_vars.write().unwrap();
for (k, v) in vars {
env_vars.insert(k.into(), v.into());
}
drop(env_vars);
self
}
pub fn set_file(&self, path: impl Into<PathBuf>, content: impl Into<String>) {
self.files
.write()
.unwrap()
.insert(path.into(), MockFile::Content(content.into()));
}
pub fn remove_file(&self, path: impl AsRef<Path>) {
self.files.write().unwrap().remove(path.as_ref());
}
pub fn set_env(&self, name: impl Into<String>, value: impl Into<String>) {
self.env_vars
.write()
.unwrap()
.insert(name.into(), value.into());
}
pub fn remove_env(&self, name: &str) {
self.env_vars.write().unwrap().remove(name);
}
}
impl ConfigEnv for MockEnv {
fn read_file(&self, path: &Path) -> io::Result<String> {
let files = self.files.read().unwrap();
match files.get(path) {
Some(MockFile::Content(content)) => Ok(content.clone()),
Some(MockFile::NotFound) | None => Err(io::Error::new(
io::ErrorKind::NotFound,
format!("mock file not found: {}", path.display()),
)),
Some(MockFile::PermissionDenied) => Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("mock permission denied: {}", path.display()),
)),
}
}
fn file_exists(&self, path: &Path) -> bool {
let files = self.files.read().unwrap();
matches!(files.get(path), Some(MockFile::Content(_)))
}
fn is_directory(&self, path: &Path) -> bool {
self.directories
.read()
.unwrap()
.contains(&path.to_path_buf())
}
fn get_env(&self, name: &str) -> Option<String> {
self.env_vars.read().unwrap().get(name).cloned()
}
fn env_vars_with_prefix(&self, prefix: &str) -> Vec<(String, String)> {
self.env_vars
.read()
.unwrap()
.iter()
.filter(|(k, _)| k.starts_with(prefix))
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
fn all_env_vars(&self) -> Vec<(String, String)> {
self.env_vars
.read()
.unwrap()
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_real_env_file_exists() {
let env = RealEnv::new();
assert!(env.file_exists(Path::new("Cargo.toml")));
assert!(!env.file_exists(Path::new("nonexistent.toml")));
}
#[test]
fn test_mock_env_files() {
let env = MockEnv::new()
.with_file("config.toml", "host = \"localhost\"")
.with_file("other.toml", "port = 8080");
assert!(env.file_exists(Path::new("config.toml")));
assert!(env.file_exists(Path::new("other.toml")));
assert!(!env.file_exists(Path::new("missing.toml")));
let content = env.read_file(Path::new("config.toml")).unwrap();
assert_eq!(content, "host = \"localhost\"");
}
#[test]
fn test_mock_env_missing_file() {
let env = MockEnv::new();
let result = env.read_file(Path::new("missing.toml"));
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
}
#[test]
fn test_mock_env_permission_denied() {
let env = MockEnv::new().with_unreadable_file("secret.toml");
let result = env.read_file(Path::new("secret.toml"));
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::PermissionDenied);
}
#[test]
fn test_mock_env_vars() {
let env = MockEnv::new()
.with_env("APP_HOST", "localhost")
.with_env("APP_PORT", "8080")
.with_env("OTHER_VAR", "value");
assert_eq!(env.get_env("APP_HOST"), Some("localhost".to_string()));
assert_eq!(env.get_env("APP_PORT"), Some("8080".to_string()));
assert_eq!(env.get_env("MISSING"), None);
let app_vars = env.env_vars_with_prefix("APP_");
assert_eq!(app_vars.len(), 2);
let all_vars = env.all_env_vars();
assert_eq!(all_vars.len(), 3);
}
#[test]
fn test_mock_env_mutations() {
let env = MockEnv::new()
.with_file("config.toml", "original")
.with_env("VAR", "original");
env.set_file("config.toml", "modified");
assert_eq!(env.read_file(Path::new("config.toml")).unwrap(), "modified");
env.set_env("VAR", "modified");
assert_eq!(env.get_env("VAR"), Some("modified".to_string()));
env.remove_file("config.toml");
assert!(!env.file_exists(Path::new("config.toml")));
env.remove_env("VAR");
assert_eq!(env.get_env("VAR"), None);
}
#[test]
fn test_mock_env_directories() {
let env = MockEnv::new()
.with_directory("/etc/myapp")
.with_file("/etc/myapp/config.toml", "content");
assert!(env.is_directory(Path::new("/etc/myapp")));
assert!(!env.is_directory(Path::new("/etc/myapp/config.toml")));
assert!(!env.is_directory(Path::new("/other")));
}
}