use std::path::{Path, PathBuf};
use serde::de::DeserializeOwned;
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum LoadError {
#[error("no config file named {0:?} found")]
NotFound(String),
#[error("could not read {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("could not parse {path}: {message}")]
Parse {
path: PathBuf,
message: String,
},
}
#[non_exhaustive]
#[derive(Debug)]
pub struct LoadedConfig<T> {
pub source: PathBuf,
pub value: T,
}
#[must_use]
pub fn find_ancestor(start: &Path, file_name: &str) -> Option<PathBuf> {
let mut current: Option<&Path> = Some(start);
while let Some(dir) = current {
let candidate = dir.join(file_name);
if candidate.is_file() {
return Some(candidate);
}
current = dir.parent();
}
None
}
pub fn load_from_ancestor<T>(start: &Path, file_name: &str) -> Result<LoadedConfig<T>, LoadError>
where
T: DeserializeOwned,
{
let path =
find_ancestor(start, file_name).ok_or_else(|| LoadError::NotFound(file_name.to_owned()))?;
load_from_path(&path)
}
pub fn load_from_path<T>(path: &Path) -> Result<LoadedConfig<T>, LoadError>
where
T: DeserializeOwned,
{
let text = std::fs::read_to_string(path).map_err(|source| LoadError::Io {
path: path.to_path_buf(),
source,
})?;
let value: T = toml::from_str(&text).map_err(|err| LoadError::Parse {
path: path.to_path_buf(),
message: err.to_string(),
})?;
Ok(LoadedConfig {
source: path.to_path_buf(),
value,
})
}