use std::path::{Path, PathBuf};
use etcetera::base_strategy::{BaseStrategy, Xdg};
use crate::error::{Error, Result};
const APP: &str = "blubat";
const CONFIG_FILE: &str = "config.toml";
const STATE_FILE: &str = "state.toml";
const WATCHES: &str = "watches";
const TUI_LOCK: &str = "tui.lock";
const DAEMON_LOCK: &str = "daemon.lock";
const LOG_FILE: &str = "daemon.log";
const ERROR_LOG_FILE: &str = "daemon.error.log";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Paths {
config_file: PathBuf,
state_dir: PathBuf,
}
impl Paths {
pub fn resolve() -> Result<Self> {
Xdg::new()
.map_err(|error| Error::Path(error.to_string()))
.map(|base| {
Self::based(
&base.config_dir(),
&base.state_dir().unwrap_or_else(|| base.data_dir()),
)
})
}
pub fn rooted(root: &Path) -> Self {
Self {
config_file: root.join(CONFIG_FILE),
state_dir: root.join("state"),
}
}
fn based(config: &Path, state: &Path) -> Self {
Self {
config_file: config.join(APP).join(CONFIG_FILE),
state_dir: state.join(APP),
}
}
pub fn with_config_file(self, config_file: PathBuf) -> Self {
Self {
config_file,
..self
}
}
pub fn with_state_dir(self, state_dir: PathBuf) -> Self {
Self { state_dir, ..self }
}
pub fn state_dir(&self) -> &Path {
&self.state_dir
}
pub fn config_file(&self) -> &Path {
&self.config_file
}
pub fn state_file(&self) -> PathBuf {
self.state_dir.join(STATE_FILE)
}
pub fn watch_dir(&self) -> PathBuf {
self.state_dir.join(WATCHES)
}
pub fn tui_lock(&self) -> PathBuf {
self.state_dir.join(TUI_LOCK)
}
pub fn daemon_lock(&self) -> PathBuf {
self.state_dir.join(DAEMON_LOCK)
}
pub fn log_file(&self) -> PathBuf {
self.state_dir.join(LOG_FILE)
}
pub fn error_log_file(&self) -> PathBuf {
self.state_dir.join(ERROR_LOG_FILE)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn xdg() -> Paths {
Paths::based(
Path::new("/home/blubat/.config"),
Path::new("/home/blubat/.local/state"),
)
}
#[test]
fn each_file_sits_under_its_own_xdg_base() {
let paths = xdg();
assert_eq!(
paths.config_file(),
Path::new("/home/blubat/.config/blubat/config.toml")
);
assert_eq!(
paths.state_file(),
PathBuf::from("/home/blubat/.local/state/blubat/state.toml")
);
assert_eq!(
paths.watch_dir(),
PathBuf::from("/home/blubat/.local/state/blubat/watches")
);
assert_eq!(
paths.tui_lock(),
PathBuf::from("/home/blubat/.local/state/blubat/tui.lock")
);
}
#[test]
fn everything_blubat_writes_about_itself_is_machine_state() {
let paths = xdg();
let state = PathBuf::from("/home/blubat/.local/state/blubat");
for path in [
paths.state_file(),
paths.watch_dir(),
paths.tui_lock(),
paths.daemon_lock(),
paths.log_file(),
paths.error_log_file(),
] {
assert_eq!(path.parent(), Some(state.as_path()), "{path:?}");
}
}
#[test]
fn the_two_locks_and_the_two_logs_are_four_distinct_files() {
let paths = xdg();
let mut named = vec![
paths.tui_lock(),
paths.daemon_lock(),
paths.log_file(),
paths.error_log_file(),
];
let written = named.len();
named.sort();
named.dedup();
assert_eq!(named.len(), written);
}
#[test]
fn intent_and_machine_state_never_share_a_directory() {
for paths in [xdg(), Paths::rooted(Path::new("/tmp/blubat-test-root"))] {
assert_ne!(
paths.config_file().parent(),
Some(paths.state_dir.as_path()),
"{paths:?}"
);
}
}
#[test]
fn a_rooted_set_keeps_every_path_inside_the_root() {
let root = Path::new("/tmp/blubat-test-root");
let paths = Paths::rooted(root);
for path in [
paths.config_file().to_path_buf(),
paths.state_file(),
paths.watch_dir(),
paths.tui_lock(),
paths.daemon_lock(),
paths.log_file(),
paths.error_log_file(),
] {
assert!(path.starts_with(root), "{path:?} escaped the root");
}
}
#[test]
fn an_explicit_config_file_replaces_the_resolved_one_and_nothing_else() {
let paths = Paths::rooted(Path::new("/tmp/blubat-test-root"));
let state = paths.state_file();
let overridden = paths.with_config_file(PathBuf::from("/elsewhere/mine.toml"));
assert_eq!(overridden.config_file(), Path::new("/elsewhere/mine.toml"));
assert_eq!(overridden.state_file(), state);
}
#[test]
fn an_explicit_state_directory_moves_every_file_blubat_writes() {
let paths = xdg().with_state_dir(PathBuf::from("/elsewhere/state"));
assert_eq!(paths.state_dir(), Path::new("/elsewhere/state"));
assert_eq!(
paths.config_file(),
Path::new("/home/blubat/.config/blubat/config.toml"),
"which says nothing about the file the user owns"
);
for path in [
paths.state_file(),
paths.watch_dir(),
paths.tui_lock(),
paths.daemon_lock(),
paths.log_file(),
paths.error_log_file(),
] {
assert!(path.starts_with("/elsewhere/state"), "{path:?}");
}
}
}