speccoder_runtime/
ladder.rs1use std::collections::BTreeMap;
6use std::path::{Path, PathBuf};
7
8use speccoder_core::ladder::Origin;
9
10#[must_use]
12pub fn file(project_state: &Path) -> PathBuf {
13 project_state.join("ladder").join("origins.json")
14}
15
16#[must_use]
18pub fn origins(project_state: &Path) -> BTreeMap<String, Vec<Origin>> {
19 std::fs::read_to_string(file(project_state))
20 .ok()
21 .and_then(|t| serde_json::from_str(&t).ok())
22 .unwrap_or_default()
23}
24
25#[must_use]
27pub fn of(project_state: &Path, subject: &str) -> Vec<Origin> {
28 origins(project_state).remove(subject).unwrap_or_default()
29}
30
31pub fn record_origin(
37 project_state: &Path,
38 subject: &str,
39 origin: Vec<Origin>,
40) -> Result<(), String> {
41 let mut all = origins(project_state);
42 if all.contains_key(subject) {
43 return Err(format!(
44 "the origin of {subject} is recorded already; it is written once"
45 ));
46 }
47 all.insert(subject.to_owned(), origin);
48 let f = file(project_state);
49 if let Some(parent) = f.parent() {
50 std::fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?;
51 }
52 let text = serde_json::to_string_pretty(&all).map_err(|e| e.to_string())?;
53 let tmp = f.with_extension("json.tmp");
54 std::fs::write(&tmp, text).map_err(|e| format!("{}: {e}", tmp.display()))?;
55 std::fs::rename(&tmp, &f).map_err(|e| format!("{}: {e}", f.display()))
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
63 fn an_origin_is_written_once() {
64 let state = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
65 let o = vec![Origin::Issue {
66 number: 7,
67 repo: None,
68 context_sha256: None,
69 }];
70 assert!(record_origin(state.path(), "007-bugfixes/FR-001", o.clone()).is_ok());
71 assert!(record_origin(state.path(), "007-bugfixes/FR-001", o.clone()).is_err());
72 assert!(record_origin(state.path(), "007-bugfixes/FR-002", o.clone()).is_ok());
73 assert_eq!(of(state.path(), "007-bugfixes/FR-001"), o);
74 }
75}