use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use directories::ProjectDirs;
#[derive(Debug, Clone)]
pub struct ShokaPaths {
config_file: PathBuf,
config_dir: PathBuf,
state_dir: PathBuf,
cache_dir: PathBuf,
}
impl ShokaPaths {
pub fn resolve(config_override: Option<&Path>) -> Result<Self> {
let project = ProjectDirs::from("", "yukimemi", "shoka")
.context("could not determine OS data directories for shoka")?;
let default_config_dir = project.config_dir().to_path_buf();
let (config_file, config_dir) = match config_override {
Some(p) => {
let abs = std::path::absolute(p)
.with_context(|| format!("absolutising config path {}", p.display()))?;
let dir = abs
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
(abs, dir)
}
None => (default_config_dir.join("config.toml"), default_config_dir),
};
let state_dir =
env_override("SHOKA_STATE_DIR").unwrap_or_else(|| project.data_dir().to_path_buf());
let cache_dir =
env_override("SHOKA_CACHE_DIR").unwrap_or_else(|| project.cache_dir().to_path_buf());
Ok(Self {
config_file,
config_dir,
state_dir,
cache_dir,
})
}
pub fn config_dir(&self) -> &Path {
&self.config_dir
}
pub fn config_file(&self) -> &Path {
&self.config_file
}
pub fn state_dir(&self) -> &Path {
&self.state_dir
}
pub fn state_file(&self) -> PathBuf {
self.state_dir.join("state.toml")
}
pub fn cache_dir(&self) -> &Path {
&self.cache_dir
}
pub fn cache_file(&self) -> PathBuf {
self.cache_dir.join("cache.toml")
}
}
fn env_override(var: &str) -> Option<PathBuf> {
let raw = std::env::var_os(var)?;
if raw.is_empty() {
return None;
}
let p = PathBuf::from(raw);
std::path::absolute(&p).ok().or(Some(p))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_default_paths_contain_shoka() {
let p = ShokaPaths::resolve(None).expect("paths resolve");
assert!(
p.config_dir()
.to_string_lossy()
.to_lowercase()
.contains("shoka"),
"config_dir should contain `shoka`: {:?}",
p.config_dir()
);
assert!(
p.state_dir()
.to_string_lossy()
.to_lowercase()
.contains("shoka"),
"state_dir should contain `shoka`: {:?}",
p.state_dir()
);
assert_eq!(
p.state_file().file_name().and_then(|s| s.to_str()),
Some("state.toml")
);
assert_eq!(
p.cache_file().file_name().and_then(|s| s.to_str()),
Some("cache.toml")
);
}
#[test]
fn resolve_with_absolute_override_uses_explicit_path() {
let dir = std::env::temp_dir().join("shoka-paths-test-abs");
let file = dir.join("custom.toml");
let p = ShokaPaths::resolve(Some(&file)).expect("paths resolve");
assert_eq!(p.config_file(), file);
assert_eq!(p.config_dir(), dir);
}
#[test]
fn resolve_with_relative_override_becomes_absolute_via_cwd() {
let p = ShokaPaths::resolve(Some(Path::new("rel/config.toml"))).expect("paths resolve");
assert!(p.config_file().is_absolute(), "{:?}", p.config_file());
assert!(p.config_dir().is_absolute(), "{:?}", p.config_dir());
}
#[test]
fn config_file_default_is_config_toml_in_config_dir() {
let p = ShokaPaths::resolve(None).expect("paths resolve");
assert_eq!(p.config_file(), p.config_dir().join("config.toml"));
}
}