1use std::path::{Path, PathBuf};
7
8pub struct PathResolver<'a> {
12 vault_root: &'a Path,
13}
14
15impl<'a> PathResolver<'a> {
16 pub fn new(vault_root: &'a Path) -> Self {
17 Self { vault_root }
18 }
19
20 pub fn inbox_task(&self, id: &str) -> PathBuf {
24 self.vault_root.join(format!("Inbox/{id}.md"))
25 }
26
27 pub fn project_task(&self, project: &str, id: &str) -> PathBuf {
29 self.vault_root.join(format!("Projects/{project}/Tasks/{id}.md"))
30 }
31
32 pub fn project_dir(&self, project: &str) -> PathBuf {
34 self.vault_root.join(format!("Projects/{project}"))
35 }
36
37 pub fn project_note(&self, project: &str) -> PathBuf {
39 self.vault_root.join(format!("Projects/{project}/{project}.md"))
40 }
41
42 pub fn archive_project_note(&self, project: &str) -> PathBuf {
44 self.vault_root.join(format!("Projects/_archive/{project}/{project}.md"))
45 }
46
47 pub fn daily_note(&self, date: &str) -> PathBuf {
49 let year = &date[..4];
50 self.vault_root.join(format!("Journal/{year}/Daily/{date}.md"))
51 }
52
53 pub fn weekly_note(&self, week: &str) -> PathBuf {
55 let year = &week[..4];
56 self.vault_root.join(format!("Journal/{year}/Weekly/{week}.md"))
57 }
58
59 pub fn meeting_note(&self, date: &str, id: &str) -> PathBuf {
61 let year = &date[..4];
62 self.vault_root.join(format!("Meetings/{year}/{id}.md"))
63 }
64
65 pub fn zettel(&self, slug: &str) -> PathBuf {
67 self.vault_root.join(format!("zettels/{slug}.md"))
68 }
69
70 pub fn custom_type(&self, type_name: &str, slug: &str) -> PathBuf {
72 self.vault_root.join(format!("{type_name}s/{slug}.md"))
73 }
74
75 pub fn meetings_dir(&self, year: &str) -> PathBuf {
79 self.vault_root.join(format!("Meetings/{year}"))
80 }
81
82 pub fn index_db(&self) -> PathBuf {
86 self.vault_root.join(".mdvault/index.db")
87 }
88
89 pub fn state_dir(&self) -> PathBuf {
91 self.vault_root.join(".mdvault/state")
92 }
93
94 pub fn state_file(&self) -> PathBuf {
96 self.vault_root.join(".mdvault/state/context.toml")
97 }
98
99 pub fn activity_log(&self) -> PathBuf {
101 self.vault_root.join(".mdvault/activity.jsonl")
102 }
103
104 pub fn activity_archive_dir(&self) -> PathBuf {
106 self.vault_root.join(".mdvault/activity_archive")
107 }
108
109 pub fn is_project_task(task_path: &str, project_folder: &str) -> bool {
116 task_path.contains(&format!("Projects/{project_folder}/"))
117 || task_path.contains(&format!("Projects/_archive/{project_folder}/"))
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use std::path::Path;
125
126 fn resolver() -> PathResolver<'static> {
127 PathResolver::new(Path::new("/vault"))
128 }
129
130 #[test]
131 fn inbox_task_path() {
132 assert_eq!(
133 resolver().inbox_task("INB-001"),
134 Path::new("/vault/Inbox/INB-001.md")
135 );
136 }
137
138 #[test]
139 fn project_task_path() {
140 assert_eq!(
141 resolver().project_task("my-proj", "MP-001"),
142 Path::new("/vault/Projects/my-proj/Tasks/MP-001.md")
143 );
144 }
145
146 #[test]
147 fn project_dir_path() {
148 assert_eq!(
149 resolver().project_dir("my-proj"),
150 Path::new("/vault/Projects/my-proj")
151 );
152 }
153
154 #[test]
155 fn project_note_path() {
156 assert_eq!(
157 resolver().project_note("my-proj"),
158 Path::new("/vault/Projects/my-proj/my-proj.md")
159 );
160 }
161
162 #[test]
163 fn archive_project_note_path() {
164 assert_eq!(
165 resolver().archive_project_note("old-proj"),
166 Path::new("/vault/Projects/_archive/old-proj/old-proj.md")
167 );
168 }
169
170 #[test]
171 fn daily_note_path() {
172 assert_eq!(
173 resolver().daily_note("2026-03-15"),
174 Path::new("/vault/Journal/2026/Daily/2026-03-15.md")
175 );
176 }
177
178 #[test]
179 fn weekly_note_path() {
180 assert_eq!(
181 resolver().weekly_note("2026-W13"),
182 Path::new("/vault/Journal/2026/Weekly/2026-W13.md")
183 );
184 }
185
186 #[test]
187 fn meeting_note_path() {
188 assert_eq!(
189 resolver().meeting_note("2026-01-15", "MTG-2026-01-15-001"),
190 Path::new("/vault/Meetings/2026/MTG-2026-01-15-001.md")
191 );
192 }
193
194 #[test]
195 fn zettel_path() {
196 assert_eq!(
197 resolver().zettel("my-knowledge-note"),
198 Path::new("/vault/zettels/my-knowledge-note.md")
199 );
200 }
201
202 #[test]
203 fn custom_type_path() {
204 assert_eq!(
205 resolver().custom_type("contact", "john-doe"),
206 Path::new("/vault/contacts/john-doe.md")
207 );
208 }
209
210 #[test]
211 fn index_db_path() {
212 assert_eq!(resolver().index_db(), Path::new("/vault/.mdvault/index.db"));
213 }
214
215 #[test]
216 fn state_paths() {
217 assert_eq!(resolver().state_dir(), Path::new("/vault/.mdvault/state"));
218 assert_eq!(
219 resolver().state_file(),
220 Path::new("/vault/.mdvault/state/context.toml")
221 );
222 }
223
224 #[test]
225 fn activity_paths() {
226 assert_eq!(
227 resolver().activity_log(),
228 Path::new("/vault/.mdvault/activity.jsonl")
229 );
230 assert_eq!(
231 resolver().activity_archive_dir(),
232 Path::new("/vault/.mdvault/activity_archive")
233 );
234 }
235
236 #[test]
237 fn is_project_task_active() {
238 assert!(PathResolver::is_project_task(
239 "Projects/my-proj/Tasks/MP-001.md",
240 "my-proj"
241 ));
242 }
243
244 #[test]
245 fn is_project_task_archived() {
246 assert!(PathResolver::is_project_task(
247 "Projects/_archive/my-proj/Tasks/MP-001.md",
248 "my-proj"
249 ));
250 }
251
252 #[test]
253 fn is_project_task_wrong_project() {
254 assert!(!PathResolver::is_project_task(
255 "Projects/other/Tasks/MP-001.md",
256 "my-proj"
257 ));
258 }
259
260 #[test]
261 fn is_project_task_inbox() {
262 assert!(!PathResolver::is_project_task("Inbox/INB-001.md", "my-proj"));
263 }
264
265 #[test]
266 fn meetings_dir_path() {
267 assert_eq!(resolver().meetings_dir("2026"), Path::new("/vault/Meetings/2026"));
268 }
269
270 #[test]
271 fn is_project_task_not_confused_by_substring() {
272 assert!(!PathResolver::is_project_task(
274 "Projects/my-proj/Tasks/MP-001.md",
275 "proj"
276 ));
277 }
278}