spool/bootstrap/
layout.rs1use anyhow::{Context, Result};
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone)]
10pub struct SpoolLayout {
11 root: PathBuf,
12}
13
14impl SpoolLayout {
15 pub fn resolve() -> Result<Self> {
17 let home = crate::support::home_dir()
18 .context("could not resolve home directory for spool layout")?;
19 Ok(Self::from_root(home.join(".spool")))
20 }
21
22 pub fn from_root(root: PathBuf) -> Self {
24 Self { root }
25 }
26
27 pub fn root(&self) -> &Path {
29 &self.root
30 }
31
32 pub fn bin_dir(&self) -> PathBuf {
34 self.root.join("bin")
35 }
36
37 pub fn data_dir(&self) -> PathBuf {
39 self.root.join("data")
40 }
41
42 pub fn plugins_dir(&self) -> PathBuf {
44 self.root.join("plugins")
45 }
46
47 pub fn version_file(&self) -> PathBuf {
49 self.root.join("version.json")
50 }
51
52 pub fn config_file(&self) -> PathBuf {
54 self.data_dir().join("config.toml")
55 }
56
57 pub fn ledger_file(&self) -> PathBuf {
59 self.data_dir().join("memory-ledger.jsonl")
60 }
61
62 pub fn binary_path(&self, name: &str) -> PathBuf {
64 let exe_name = if cfg!(windows) {
65 format!("{name}.exe")
66 } else {
67 name.to_string()
68 };
69 self.bin_dir().join(exe_name)
70 }
71
72 pub fn ensure_dirs(&self) -> Result<()> {
74 for dir in [
75 self.root(),
76 &self.bin_dir(),
77 &self.data_dir(),
78 &self.plugins_dir(),
79 ] {
80 std::fs::create_dir_all(dir).with_context(|| format!("creating {}", dir.display()))?;
81 }
82 Ok(())
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use tempfile::tempdir;
90
91 #[test]
92 fn layout_should_compute_standard_paths() {
93 let temp = tempdir().unwrap();
94 let layout = SpoolLayout::from_root(temp.path().join(".spool"));
95
96 assert_eq!(layout.bin_dir(), temp.path().join(".spool/bin"));
97 assert_eq!(layout.data_dir(), temp.path().join(".spool/data"));
98 assert_eq!(layout.plugins_dir(), temp.path().join(".spool/plugins"));
99 assert_eq!(
100 layout.version_file(),
101 temp.path().join(".spool/version.json")
102 );
103 assert_eq!(
104 layout.ledger_file(),
105 temp.path().join(".spool/data/memory-ledger.jsonl")
106 );
107 }
108
109 #[test]
110 fn binary_path_should_add_exe_on_windows() {
111 let temp = tempdir().unwrap();
112 let layout = SpoolLayout::from_root(temp.path().to_path_buf());
113 let p = layout.binary_path("spool");
114 if cfg!(windows) {
115 assert!(p.to_string_lossy().ends_with("spool.exe"));
116 } else {
117 assert!(p.to_string_lossy().ends_with("/bin/spool"));
118 }
119 }
120
121 #[test]
122 fn ensure_dirs_should_create_all_directories() {
123 let temp = tempdir().unwrap();
124 let layout = SpoolLayout::from_root(temp.path().join(".spool"));
125 layout.ensure_dirs().unwrap();
126
127 assert!(layout.root().is_dir());
128 assert!(layout.bin_dir().is_dir());
129 assert!(layout.data_dir().is_dir());
130 assert!(layout.plugins_dir().is_dir());
131 }
132
133 #[test]
134 fn ensure_dirs_should_be_idempotent() {
135 let temp = tempdir().unwrap();
136 let layout = SpoolLayout::from_root(temp.path().join(".spool"));
137 layout.ensure_dirs().unwrap();
138 layout.ensure_dirs().unwrap();
139 assert!(layout.bin_dir().is_dir());
140 }
141}