use std::path::{Path, PathBuf};
use crate::{
discovery::search_dirs,
error::{ConfigError, Result},
format::Format,
parsers,
value::ConfigMap,
};
#[derive(Debug)]
pub struct ConfigGet {
stem: String,
config_dir: String,
data: ConfigMap,
loaded_from: Option<PathBuf>,
}
#[must_use]
pub struct ConfigGetBuilder {
stem: String,
config_dir: String,
explicit_path: Option<PathBuf>,
auto_load: bool,
create: bool,
}
impl ConfigGetBuilder {
fn new(stem: impl Into<String>) -> Self {
let stem = stem.into();
let config_dir = stem.clone();
Self {
stem,
config_dir,
explicit_path: None,
auto_load: true,
create: false,
}
}
pub fn config_dir(mut self, dir: impl Into<String>) -> Self {
self.config_dir = dir.into();
self
}
pub fn path(mut self, p: impl Into<PathBuf>) -> Self {
self.explicit_path = Some(p.into());
self
}
pub fn auto_load(mut self, v: bool) -> Self {
self.auto_load = v;
self
}
pub fn create(mut self, v: bool) -> Self {
self.create = v;
self
}
pub fn build(self) -> Result<ConfigGet> {
let mut cfg = ConfigGet {
stem: self.stem.clone(),
config_dir: self.config_dir.clone(),
data: ConfigMap::new(),
loaded_from: None,
};
if self.auto_load {
if let Some(p) = self.explicit_path {
cfg.load_from(&p)?;
} else {
match cfg.find() {
Some(p) => cfg.load_from(&p)?,
None if self.create => {
let created = cfg.create_default()?;
log::info!("Created empty config at {}", created.display());
cfg.loaded_from = Some(created);
}
None => {
return Err(ConfigError::NotFound(self.stem));
}
}
}
}
Ok(cfg)
}
}
impl ConfigGet {
pub fn builder(stem: impl Into<String>) -> ConfigGetBuilder {
ConfigGetBuilder::new(stem)
}
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let stem = path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
let mut cfg = Self {
stem: stem.clone(),
config_dir: stem,
data: ConfigMap::new(),
loaded_from: None,
};
cfg.load_from(path)?;
Ok(cfg)
}
pub fn from_env(stem: impl Into<String>, config_dir: impl Into<String>) -> Result<Self> {
Self::builder(stem).config_dir(config_dir).build()
}
pub fn from_ini(stem: impl Into<String>, config_dir: impl Into<String>) -> Result<Self> {
find_and_load_format(&stem.into(), &config_dir.into(), Format::Ini)
}
pub fn from_toml(stem: impl Into<String>, config_dir: impl Into<String>) -> Result<Self> {
find_and_load_format(&stem.into(), &config_dir.into(), Format::Toml)
}
pub fn from_json(stem: impl Into<String>, config_dir: impl Into<String>) -> Result<Self> {
find_and_load_format(&stem.into(), &config_dir.into(), Format::Json)
}
pub fn from_yaml(stem: impl Into<String>, config_dir: impl Into<String>) -> Result<Self> {
find_and_load_format(&stem.into(), &config_dir.into(), Format::Yaml)
}
#[must_use]
pub fn search_paths(stem: &str, config_dir: &str) -> Vec<PathBuf> {
let candidates = Format::candidates(stem);
let dirs = search_dirs(config_dir);
dirs.into_iter()
.flat_map(|dir| {
candidates
.iter()
.map(move |c| dir.join(c))
.collect::<Vec<_>>()
})
.collect()
}
#[must_use]
pub fn find(&self) -> Option<PathBuf> {
Self::search_paths(&self.stem, &self.config_dir)
.into_iter()
.find(|p| p.is_file())
}
pub fn reload(&mut self, path: Option<&Path>) -> Result<()> {
let p = match path {
Some(p) => p.to_path_buf(),
None => self
.find()
.ok_or_else(|| ConfigError::NotFound(self.stem.clone()))?,
};
self.load_from(&p)
}
fn load_from(&mut self, path: &Path) -> Result<()> {
log::debug!("Loading config from {}", path.display());
let format = Format::from_path(path).ok_or_else(|| ConfigError::Parse {
path: path.display().to_string(),
message: "unrecognised file extension".to_string(),
})?;
self.data = match format {
Format::Env => parsers::parse_env(path)?,
Format::Ini => parsers::parse_ini(path)?,
Format::Toml => parsers::parse_toml(path)?,
Format::Json => parsers::parse_json(path)?,
Format::Yaml => parsers::parse_yaml(path)?,
};
self.loaded_from = Some(path.to_path_buf());
Ok(())
}
fn create_default(&self) -> Result<PathBuf> {
let dirs = search_dirs(&self.config_dir);
let dir = dirs.first().ok_or_else(|| {
ConfigError::Other("could not determine a directory to create config in".into())
})?;
std::fs::create_dir_all(dir).map_err(|e| ConfigError::Io {
path: dir.display().to_string(),
source: e,
})?;
let path = dir.join(".env");
std::fs::write(&path, "# config-get generated empty config\n").map_err(|e| {
ConfigError::Io {
path: path.display().to_string(),
source: e,
}
})?;
Ok(path)
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&str> {
self.data.get_flat(key)
}
#[must_use]
pub fn get_or<'a>(&'a self, key: &str, fallback: &'a str) -> &'a str {
self.get(key).unwrap_or(fallback)
}
pub fn require(&self, key: &str) -> Result<&str> {
self.get(key)
.ok_or_else(|| ConfigError::KeyNotFound(key.to_string()))
}
#[must_use]
pub fn get_in(&self, section: &str, key: &str) -> Option<&str> {
self.data.get_in_section(section, key)
}
#[must_use]
pub fn get_in_or<'a>(&'a self, section: &str, key: &str, fallback: &'a str) -> &'a str {
self.get_in(section, key).unwrap_or(fallback)
}
pub fn require_in(&self, section: &str, key: &str) -> Result<&str> {
self.get_in(section, key)
.ok_or_else(|| ConfigError::KeyNotFound(format!("{section}.{key}")))
}
pub fn get_section(&self, section: &str) -> Result<indexmap::IndexMap<String, String>> {
self.data
.get_section(section)
.cloned()
.ok_or_else(|| ConfigError::SectionNotFound(section.to_string()))
}
#[must_use]
pub fn all(&self) -> ConfigMap {
self.data.clone()
}
#[must_use]
pub fn loaded_from(&self) -> Option<&Path> {
self.loaded_from.as_deref()
}
pub fn parse<T>(&self, key: &str) -> Result<T>
where
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
let v = self.require(key)?;
v.parse::<T>().map_err(|e| ConfigError::Parse {
path: self
.loaded_from
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_default(),
message: format!("key '{key}': {e}"),
})
}
pub fn parse_in<T>(&self, section: &str, key: &str) -> Result<T>
where
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
let v = self.require_in(section, key)?;
v.parse::<T>().map_err(|e| ConfigError::Parse {
path: self
.loaded_from
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_default(),
message: format!("key '{section}.{key}': {e}"),
})
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.data.flat_iter()
}
pub fn sections(&self) -> impl Iterator<Item = &str> {
self.data.sections().map(|(s, _)| s)
}
#[must_use]
pub fn len(&self) -> usize {
self.data.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
#[must_use]
pub fn contains_key(&self, key: &str) -> bool {
self.data.contains_key(key)
}
}
impl std::ops::Index<&str> for ConfigGet {
type Output = str;
fn index(&self, key: &str) -> &Self::Output {
self.get(key)
.unwrap_or_else(|| panic!("config key not found: '{key}'"))
}
}
fn find_and_load_format(stem: &str, config_dir: &str, fmt: Format) -> Result<ConfigGet> {
let ext = match fmt {
Format::Env => ".env".to_string(),
Format::Ini => format!("{stem}.ini"),
Format::Toml => format!("{stem}.toml"),
Format::Json => format!("{stem}.json"),
Format::Yaml => format!("{stem}.yml"),
};
let path = search_dirs(config_dir)
.into_iter()
.map(|d| d.join(&ext))
.find(|p| p.is_file())
.ok_or_else(|| ConfigError::NotFound(format!("{stem} ({fmt})")))?;
ConfigGet::from_file(path)
}