use std::{
env, fs,
path::{Path, PathBuf},
};
use toml::Table;
use crate::SettingsError;
pub struct ConfigPathOptions<'a> {
pub app: &'a str,
pub path: Option<&'a str>,
pub default_file: Option<&'a str>,
}
pub fn resolve_config_file(options: &ConfigPathOptions) -> Result<Option<PathBuf>, SettingsError> {
if let Ok(env_var) = env::var(format!("{}_CONFIG", options.app.to_uppercase())) {
return Ok(Some(PathBuf::from(env_var)));
}
if let Some(path) = options.path {
return Ok(Some(Path::new(path).to_path_buf()));
}
let Some(default_file) = options.default_file else {
return Ok(None);
};
let Some(project_dirs) = directories::ProjectDirs::from("", "", options.app) else {
return Err(SettingsError::NoConfigDir);
};
Ok(Some(project_dirs.config_dir().join(default_file)))
}
pub fn load_toml_file(path: Option<&Path>) -> Result<Option<toml::Table>, SettingsError> {
match path {
Some(file) => {
let raw_content = match fs::read_to_string(file) {
Ok(content) => content,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => {
return Err(SettingsError::ConfigRead {
path: file.to_path_buf(),
source: e,
});
}
};
let parsed_content =
raw_content
.parse::<Table>()
.map_err(|e| SettingsError::ConfigParse {
path: file.to_path_buf(),
source: e,
})?;
Ok(Some(parsed_content))
}
None => Ok(None),
}
}
#[cfg(test)]
mod test {
use std::io::Write;
use std::assert_matches;
use toml::Value;
use super::*;
#[test]
fn test_resolve_config_file_from_configured_path() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let tmp_path = tmp.into_temp_path();
let tmp_path_str = tmp_path.to_str().unwrap().to_string();
let options = ConfigPathOptions {
app: "test_app",
path: Some(&tmp_path_str),
default_file: None,
};
let config_file = resolve_config_file(&options).unwrap().unwrap();
assert_eq!(config_file, PathBuf::from(tmp_path_str))
}
#[test]
fn test_resolve_config_file_from_env_var() {
let config_path = "~/.config/test_app/test.toml";
let options = ConfigPathOptions {
app: "test_app",
path: None,
default_file: None,
};
temp_env::with_var("TEST_APP_CONFIG", Some(config_path), || {
let config_file = resolve_config_file(&options);
assert!(config_file.is_ok());
let config_file = config_file.unwrap().unwrap();
assert_eq!(config_file.to_str().unwrap(), config_path)
});
}
#[test]
fn test_resolve_config_file_returns_none_without_path_or_default() {
let options = ConfigPathOptions {
app: "test_app_without_path_or_default",
path: None,
default_file: None,
};
let config_file = resolve_config_file(&options).unwrap();
assert!(config_file.is_none())
}
#[test]
fn test_load_toml_file_returns_table() {
let content = "foo = 'bar'";
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(content.as_bytes()).unwrap();
let parsed = load_toml_file(Some(tmp.path())).unwrap().unwrap();
assert_eq!(parsed["foo"], Value::from("bar"))
}
#[test]
fn test_load_toml_file_returns_none_for_missing_file() {
let parsed = load_toml_file(Some(Path::new("not/real/path"))).unwrap();
assert!(parsed.is_none())
}
#[test]
fn test_load_toml_file_returns_none_when_given_none() {
let parsed = load_toml_file(None).unwrap();
assert!(parsed.is_none())
}
#[test]
fn test_load_toml_file_returns_settingserror_for_invalid_toml() {
let content = "aaabbbccc";
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(content.as_bytes()).unwrap();
let err = load_toml_file(Some(tmp.path())).unwrap_err();
assert_matches!(err, SettingsError::ConfigParse { .. })
}
}