use crate::{ConfigSource, CoreError};
use camino::{Utf8Path, Utf8PathBuf};
const APP_NAME: &str = "cook";
pub(crate) const LOCAL_CONFIG_DIR: &str = "config";
const AUTO_AISLE: &str = "aisle.conf";
pub(crate) const AUTO_PANTRY: &str = "pantry.conf";
#[derive(Debug, Clone)]
pub struct Context {
base_path: Utf8PathBuf,
aisle: ConfigSource,
pantry: ConfigSource,
}
impl Context {
pub fn new(base_path: Utf8PathBuf) -> Self {
Self {
base_path,
aisle: ConfigSource::None,
pantry: ConfigSource::None,
}
}
pub fn discover(base_path: Utf8PathBuf) -> Self {
let aisle = Self::discover_one(&base_path, AUTO_AISLE);
let pantry = Self::discover_one(&base_path, AUTO_PANTRY);
Self {
base_path,
aisle,
pantry,
}
}
fn discover_one(base_path: &Utf8Path, name: &str) -> ConfigSource {
Self::search(base_path, name, global_config_path(name).ok().as_deref())
}
fn search(base_path: &Utf8Path, name: &str, global: Option<&Utf8Path>) -> ConfigSource {
let local = base_path.join(LOCAL_CONFIG_DIR).join(name);
tracing::trace!("checking local config file: {local}");
if local.is_file() {
return ConfigSource::Path(local);
}
match global {
Some(global) => {
tracing::trace!("checking global config file: {global}");
if global.is_file() {
ConfigSource::Path(global.to_owned())
} else {
ConfigSource::None
}
}
None => ConfigSource::None,
}
}
pub fn with_aisle(mut self, source: ConfigSource) -> Self {
self.aisle = source;
self
}
pub fn with_pantry(mut self, source: ConfigSource) -> Self {
self.pantry = source;
self
}
pub fn base_path(&self) -> &Utf8Path {
&self.base_path
}
pub fn aisle(&self) -> &ConfigSource {
&self.aisle
}
pub fn pantry(&self) -> &ConfigSource {
&self.pantry
}
}
pub fn global_config_path(name: &str) -> Result<Utf8PathBuf, CoreError> {
let dirs =
directories::ProjectDirs::from("", "", APP_NAME).ok_or_else(|| CoreError::Config {
path: None,
message: format!("could not determine the home directory to locate {name}"),
})?;
let config = Utf8Path::from_path(dirs.config_dir()).ok_or_else(|| CoreError::Config {
path: None,
message: format!(
"the configuration directory holding {name} is not valid utf-8, \
and cook only supports utf-8 paths"
),
})?;
Ok(config.join(name))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ConfigSource;
#[test]
fn new_touches_nothing() {
let dir = tempfile::TempDir::new().unwrap();
let base = utf8(&dir);
write(&base.join("config").join("aisle.conf"), "[produce]\nleek");
write(
&base.join("config").join("pantry.conf"),
"[freezer]\npeas = \"1kg\"",
);
let ctx = Context::new(base.clone());
assert!(ctx.aisle().is_unset(), "new must not discover local config");
assert!(
ctx.pantry().is_unset(),
"new must not discover local config"
);
assert_eq!(ctx.base_path(), base);
}
#[test]
fn with_aisle_overrides() {
let ctx = Context::new(Utf8PathBuf::from("/tmp"))
.with_aisle(ConfigSource::Inline("[produce]\nleek".to_string()));
assert_eq!(
ctx.aisle().read().unwrap().as_deref(),
Some("[produce]\nleek")
);
assert!(
ctx.pantry().is_unset(),
"with_aisle must not set the pantry"
);
}
#[test]
fn with_pantry_overrides() {
let ctx = Context::new(Utf8PathBuf::from("/tmp")).with_pantry(ConfigSource::Inline(
"[freezer]\npeas = \"1kg\"".to_string(),
));
assert_eq!(
ctx.pantry().read().unwrap().as_deref(),
Some("[freezer]\npeas = \"1kg\"")
);
assert!(ctx.aisle().is_unset(), "with_pantry must not set the aisle");
}
#[test]
fn discover_finds_local_config() {
let dir = tempfile::TempDir::new().unwrap();
let base = utf8(&dir);
write(&base.join("config").join("aisle.conf"), "[produce]\nleek");
write(
&base.join("config").join("pantry.conf"),
"[freezer]\npeas = \"1kg\"",
);
let ctx = Context::discover(base.clone());
assert_eq!(
ctx.aisle().path(),
Some(base.join("config").join("aisle.conf").as_path())
);
assert_eq!(
ctx.pantry().path(),
Some(base.join("config").join("pantry.conf").as_path())
);
}
#[test]
fn global_config_path_joins_the_app_name() {
let path = global_config_path("aisle.conf").expect("a home directory");
assert_eq!(
path.file_name(),
Some("aisle.conf"),
"the name asked for must be the last component: {path}"
);
assert!(
path.components().any(|c| c.as_str() == APP_NAME),
"expected a `{APP_NAME}` component in {path}"
);
assert!(
path.is_absolute(),
"the platform config directory is absolute: {path}"
);
}
fn write(path: &Utf8Path, text: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, text).unwrap();
}
fn utf8(dir: &tempfile::TempDir) -> Utf8PathBuf {
Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).unwrap()
}
#[test]
fn local_config_wins_over_global() {
let dir = tempfile::TempDir::new().unwrap();
let base = utf8(&dir);
let local = base.join("config").join("aisle.conf");
let global = base.join("global").join("aisle.conf");
write(&local, "[produce]\nleek");
write(&global, "[dairy]\nmilk");
let found = Context::search(&base, "aisle.conf", Some(&global));
assert_eq!(found, ConfigSource::Path(local));
}
#[test]
fn global_config_is_used_when_there_is_no_local_one() {
let dir = tempfile::TempDir::new().unwrap();
let base = utf8(&dir);
let global = base.join("global").join("pantry.conf");
write(&global, "[freezer]\npeas = \"1kg\"");
let found = Context::search(&base, "pantry.conf", Some(&global));
assert_eq!(found, ConfigSource::Path(global));
}
#[test]
fn absent_everywhere_is_unset() {
let dir = tempfile::TempDir::new().unwrap();
let base = utf8(&dir);
let global = base.join("global").join("pantry.conf");
assert!(Context::search(&base, "pantry.conf", Some(&global)).is_unset());
assert!(Context::search(&base, "pantry.conf", None).is_unset());
}
}