requiem/storage/
directory.rs1use std::{ffi::OsStr, path::PathBuf};
7
8use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
9use walkdir::WalkDir;
10
11use crate::{
12 Requirement,
13 domain::{
14 Config, Hrid,
15 requirement::{LoadError, Parent},
16 },
17};
18
19pub use crate::storage::Tree;
20
21#[derive(Debug, PartialEq)]
22pub struct Loaded(Tree);
23
24#[derive(Debug, PartialEq, Eq)]
25pub struct Unloaded;
26
27pub struct Directory<S> {
29 root: PathBuf,
31 state: S,
32}
33
34impl<S> Directory<S> {
35 pub fn link_requirement(&self, child: String, parent: String) {
37 let mut child = self.load_requirement(child).unwrap().unwrap();
38 let parent = self.load_requirement(parent).unwrap().unwrap();
39
40 child.add_parent(
41 parent.uuid(),
42 Parent {
43 hrid: parent.hrid().clone(),
44 fingerprint: parent.fingerprint(),
45 },
46 );
47
48 child.save(&self.root).unwrap();
49 }
50
51 fn load_requirement(&self, hrid: String) -> Option<Result<Requirement, LoadError>> {
52 match Requirement::load(&self.root, hrid) {
53 Ok(requirement) => Some(Ok(requirement)),
54 Err(LoadError::NotFound) => None,
55 Err(e) => Some(Err(e)),
56 }
57 }
58}
59
60impl Directory<Unloaded> {
61 #[must_use]
63 pub const fn new(root: PathBuf) -> Self {
64 Self {
65 root,
66 state: Unloaded,
67 }
68 }
69
70 pub fn load_all(self) -> Directory<Loaded> {
72 let paths: Vec<_> = WalkDir::new(&self.root)
73 .into_iter()
74 .filter_map(Result::ok)
75 .filter(|entry| entry.path().extension() == Some(OsStr::new("md")))
76 .map(walkdir::DirEntry::into_path)
77 .collect();
78
79 let requirements: Vec<Requirement> = paths
80 .par_iter()
81 .map(|path| {
82 let hrid = path.file_stem().unwrap().to_string_lossy().to_string();
83 let directory = path.parent().unwrap().to_path_buf();
84 Requirement::load(&directory, hrid).unwrap() })
86 .collect();
87
88 let mut tree = Tree::with_capacity(requirements.len());
89
90 for req in requirements {
91 tree.insert(req);
92 }
93
94 Directory {
95 root: self.root,
96 state: Loaded(tree),
97 }
98 }
99}
100
101impl Directory<Loaded> {
102 pub fn add_requirement(&mut self, kind: String) -> Requirement {
104 let config_path = self.root.join("config.toml");
105
106 let tree = &mut self.state.0;
107
108 let _config = Config::load(&config_path).unwrap_or_else(|e| {
109 tracing::debug!("Failed to load config: {e}");
110 Config::default()
111 });
112
113 let id = tree.next_index(&kind);
114
115 let requirement = Requirement::new(Hrid { kind, id }, String::new());
116
117 requirement.save(&self.root).unwrap();
118 tree.insert(requirement.clone());
119
120 tracing::info!("Added requirement: {}", requirement.hrid());
121
122 requirement
123 }
124
125 pub fn update_hrids(&mut self) {
129 let tree = &mut self.state.0;
130 let updated: Vec<_> = tree.update_hrids().collect();
131
132 for id in updated {
133 let requirement = tree
134 .requirement(id)
135 .expect("this just got updated, so we know it exists");
136 requirement.save(&self.root).unwrap();
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::Requirement;
145 use tempfile::TempDir;
146
147 fn setup_temp_directory() -> (TempDir, Directory<Loaded>) {
148 let tmp = TempDir::new().expect("failed to create temp dir");
149 let path = tmp.path().to_path_buf();
150 (tmp, Directory::new(path).load_all())
151 }
152
153 #[test]
154 fn can_add_requirement() {
155 let (_tmp, mut dir) = setup_temp_directory();
156 let r1 = dir.add_requirement("REQ".to_string());
157
158 assert_eq!(r1.hrid().to_string(), "REQ-001");
159
160 let loaded = Requirement::load(&dir.root, r1.hrid().to_string())
161 .expect("should load saved requirement");
162 assert_eq!(loaded.uuid(), r1.uuid());
163 }
164
165 #[test]
166 fn can_add_multiple_requirements_with_incrementing_id() {
167 let (_tmp, mut dir) = setup_temp_directory();
168 let r1 = dir.add_requirement("REQ".to_string());
169 let r2 = dir.add_requirement("REQ".to_string());
170
171 assert_eq!(r1.hrid().to_string(), "REQ-001");
172 assert_eq!(r2.hrid().to_string(), "REQ-002");
173 }
174
175 #[test]
176 fn can_link_two_requirements() {
177 let (_tmp, mut dir) = setup_temp_directory();
178 let parent = dir.add_requirement("SYS".to_string());
179 let child = dir.add_requirement("USR".to_string());
180
181 Directory::new(dir.root.clone())
182 .link_requirement(child.hrid().to_string(), parent.hrid().to_string());
183
184 let updated =
185 Requirement::load(&dir.root, child.hrid().to_string()).expect("should load child");
186
187 let parents: Vec<_> = updated.parents().collect();
188 assert_eq!(parents.len(), 1);
189 assert_eq!(parents[0].0, parent.uuid());
190 assert_eq!(&parents[0].1.hrid, parent.hrid());
191 }
192
193 #[test]
194 fn update_hrids_corrects_outdated_parent_hrids() {
195 let (_tmp, mut dir) = setup_temp_directory();
196 let parent = dir.add_requirement("P".to_string());
197 let mut child = dir.add_requirement("C".to_string());
198
199 child.add_parent(
201 parent.uuid(),
202 Parent {
203 hrid: Hrid::try_from("WRONG-999").unwrap(),
204 fingerprint: parent.fingerprint(),
205 },
206 );
207 child.save(&dir.root).unwrap();
208
209 let mut loaded_dir = Directory::new(dir.root.clone()).load_all();
210 loaded_dir.update_hrids();
211
212 let updated = Requirement::load(&loaded_dir.root, child.hrid().to_string())
213 .expect("should load updated child");
214 let (_, parent_ref) = updated.parents().next().unwrap();
215
216 assert_eq!(&parent_ref.hrid, parent.hrid());
217 }
218
219 #[test]
220 fn load_all_reads_all_saved_requirements() {
221 let (_tmp, mut dir) = setup_temp_directory();
222 let r1 = dir.add_requirement("X".to_string());
223 let r2 = dir.add_requirement("X".to_string());
224
225 let loaded = Directory::new(dir.root.clone()).load_all();
226
227 let mut found = 0;
228 for i in 1..=2 {
229 let hrid = format!("X-00{i}");
230 dbg!(&hrid);
231 let req = Requirement::load(&loaded.root, hrid).unwrap();
232 if req.uuid() == r1.uuid() || req.uuid() == r2.uuid() {
233 found += 1;
234 }
235 }
236
237 assert_eq!(found, 2);
238 }
239}