use std::env;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use thiserror::Error;
const ENV_HOME: &str = "LERNIE_HOME";
const ENV_XDG_CONFIG: &str = "XDG_CONFIG_HOME";
const ENV_XDG_DATA: &str = "XDG_DATA_HOME";
const SUBDIR: &str = "lernie";
const CONFIG_FALLBACK: &str = ".config";
const DATA_FALLBACK: &str = ".local/share";
const MODELS_FILE: &str = "models.yaml";
#[derive(Debug, Error, PartialEq, Eq)]
pub enum Error {
#[error("LERNIE_HOME is unset and no home directory is available")]
NoHome,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Roots {
pub config: PathBuf,
pub data: PathBuf,
}
pub fn resolve_from(
override_value: Option<&OsStr>,
xdg_config: Option<&OsStr>,
xdg_data: Option<&OsStr>,
home: Option<&Path>,
) -> Result<Roots, Error> {
if let Some(v) = override_value
&& !v.is_empty()
{
let both = PathBuf::from(v);
return Ok(Roots {
config: both.clone(),
data: both,
});
}
Ok(Roots {
config: xdg_root(xdg_config, home, CONFIG_FALLBACK)?,
data: xdg_root(xdg_data, home, DATA_FALLBACK)?,
})
}
fn xdg_root(xdg: Option<&OsStr>, home: Option<&Path>, fallback: &str) -> Result<PathBuf, Error> {
if let Some(v) = xdg
&& !v.is_empty()
{
return Ok(Path::new(v).join(SUBDIR));
}
home.map(|h| h.join(fallback).join(SUBDIR))
.ok_or(Error::NoHome)
}
pub fn resolve() -> Result<Roots, Error> {
let override_value = env::var_os(ENV_HOME);
let xdg_config = env::var_os(ENV_XDG_CONFIG);
let xdg_data = env::var_os(ENV_XDG_DATA);
#[allow(deprecated)] let home = env::home_dir();
resolve_from(
override_value.as_deref(),
xdg_config.as_deref(),
xdg_data.as_deref(),
home.as_deref(),
)
}
pub fn models_path(config_root: &Path) -> PathBuf {
config_root.join(MODELS_FILE)
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsString;
fn os(s: &str) -> OsString {
OsString::from(s)
}
#[test]
fn override_collapses_both_roots() {
let r = resolve_from(
Some(OsStr::new("/opt/lernie")),
Some(OsStr::new("/xc")),
Some(OsStr::new("/xd")),
Some(Path::new("/home/x")),
)
.unwrap();
assert_eq!(r.config, PathBuf::from("/opt/lernie"));
assert_eq!(r.data, PathBuf::from("/opt/lernie"));
}
#[test]
fn empty_override_falls_through_to_xdg() {
let r = resolve_from(
Some(OsStr::new("")),
Some(OsStr::new("/xc")),
Some(OsStr::new("/xd")),
Some(Path::new("/home/x")),
)
.unwrap();
assert_eq!(r.config, PathBuf::from("/xc/lernie"));
assert_eq!(r.data, PathBuf::from("/xd/lernie"));
}
#[test]
fn xdg_vars_name_each_root() {
let r = resolve_from(None, Some(OsStr::new("/xc")), Some(OsStr::new("/xd")), None).unwrap();
assert_eq!(r.config, PathBuf::from("/xc/lernie"));
assert_eq!(r.data, PathBuf::from("/xd/lernie"));
}
#[test]
fn unset_xdg_uses_home_fallbacks() {
let r = resolve_from(None, None, None, Some(Path::new("/home/x"))).unwrap();
assert_eq!(r.config, PathBuf::from("/home/x/.config/lernie"));
assert_eq!(r.data, PathBuf::from("/home/x/.local/share/lernie"));
}
#[test]
fn empty_xdg_var_falls_through_to_home_fallback() {
let r = resolve_from(
None,
Some(OsStr::new("")),
Some(OsStr::new("")),
Some(Path::new("/home/x")),
)
.unwrap();
assert_eq!(r.config, PathBuf::from("/home/x/.config/lernie"));
assert_eq!(r.data, PathBuf::from("/home/x/.local/share/lernie"));
}
#[test]
fn missing_config_root_is_an_error() {
let err = resolve_from(None, None, Some(OsStr::new("/xd")), None).unwrap_err();
assert_eq!(err, Error::NoHome);
}
#[test]
fn missing_data_root_is_an_error() {
let err = resolve_from(None, Some(OsStr::new("/xc")), None, None).unwrap_err();
assert_eq!(err, Error::NoHome);
}
#[test]
fn empty_override_with_no_home_and_no_xdg_is_an_error() {
let err = resolve_from(Some(OsStr::new("")), None, None, None).unwrap_err();
assert_eq!(err, Error::NoHome);
}
#[test]
fn override_value_with_path_separator_is_preserved() {
let v = os("/srv/data/lernie");
let r = resolve_from(Some(v.as_os_str()), None, None, None).unwrap();
assert_eq!(r.config, PathBuf::from("/srv/data/lernie"));
assert_eq!(r.data, PathBuf::from("/srv/data/lernie"));
}
#[test]
fn models_path_appends_filename() {
assert_eq!(
models_path(Path::new("/xc/lernie")),
PathBuf::from("/xc/lernie/models.yaml")
);
}
#[test]
fn live_resolve_returns_some_paths() {
let _ = resolve().expect("either LERNIE_HOME or HOME must be set");
}
}