use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct SpoolLayout {
root: PathBuf,
}
impl SpoolLayout {
pub fn resolve() -> Result<Self> {
let home = crate::support::home_dir()
.context("could not resolve home directory for spool layout")?;
Ok(Self::from_root(home.join(".spool")))
}
pub fn from_root(root: PathBuf) -> Self {
Self { root }
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn bin_dir(&self) -> PathBuf {
self.root.join("bin")
}
pub fn data_dir(&self) -> PathBuf {
self.root.join("data")
}
pub fn plugins_dir(&self) -> PathBuf {
self.root.join("plugins")
}
pub fn version_file(&self) -> PathBuf {
self.root.join("version.json")
}
pub fn config_file(&self) -> PathBuf {
self.data_dir().join("config.toml")
}
pub fn ledger_file(&self) -> PathBuf {
self.data_dir().join("memory-ledger.jsonl")
}
pub fn binary_path(&self, name: &str) -> PathBuf {
let exe_name = if cfg!(windows) {
format!("{name}.exe")
} else {
name.to_string()
};
self.bin_dir().join(exe_name)
}
pub fn ensure_dirs(&self) -> Result<()> {
for dir in [
self.root(),
&self.bin_dir(),
&self.data_dir(),
&self.plugins_dir(),
] {
std::fs::create_dir_all(dir).with_context(|| format!("creating {}", dir.display()))?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn layout_should_compute_standard_paths() {
let temp = tempdir().unwrap();
let layout = SpoolLayout::from_root(temp.path().join(".spool"));
assert_eq!(layout.bin_dir(), temp.path().join(".spool/bin"));
assert_eq!(layout.data_dir(), temp.path().join(".spool/data"));
assert_eq!(layout.plugins_dir(), temp.path().join(".spool/plugins"));
assert_eq!(
layout.version_file(),
temp.path().join(".spool/version.json")
);
assert_eq!(
layout.ledger_file(),
temp.path().join(".spool/data/memory-ledger.jsonl")
);
}
#[test]
fn binary_path_should_add_exe_on_windows() {
let temp = tempdir().unwrap();
let layout = SpoolLayout::from_root(temp.path().to_path_buf());
let p = layout.binary_path("spool");
if cfg!(windows) {
assert!(p.to_string_lossy().ends_with("spool.exe"));
} else {
assert!(p.to_string_lossy().ends_with("/bin/spool"));
}
}
#[test]
fn ensure_dirs_should_create_all_directories() {
let temp = tempdir().unwrap();
let layout = SpoolLayout::from_root(temp.path().join(".spool"));
layout.ensure_dirs().unwrap();
assert!(layout.root().is_dir());
assert!(layout.bin_dir().is_dir());
assert!(layout.data_dir().is_dir());
assert!(layout.plugins_dir().is_dir());
}
#[test]
fn ensure_dirs_should_be_idempotent() {
let temp = tempdir().unwrap();
let layout = SpoolLayout::from_root(temp.path().join(".spool"));
layout.ensure_dirs().unwrap();
layout.ensure_dirs().unwrap();
assert!(layout.bin_dir().is_dir());
}
}