Skip to main content

speccoder_runtime/
ladder.rs

1//! Where work came from, written once: `<project state>/ladder/origins.json`,
2//! one record per subject (a unit, or `<unit>/<requirement>`). Only
3//! [`record_origin`] writes it; a second write for a subject is refused.
4
5use std::collections::BTreeMap;
6use std::path::{Path, PathBuf};
7
8use speccoder_core::ladder::Origin;
9
10/// The store's file.
11#[must_use]
12pub fn file(project_state: &Path) -> PathBuf {
13    project_state.join("ladder").join("origins.json")
14}
15
16/// Every recorded subject's origins.
17#[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/// The origins of `subject`.
26#[must_use]
27pub fn of(project_state: &Path, subject: &str) -> Vec<Origin> {
28    origins(project_state).remove(subject).unwrap_or_default()
29}
30
31/// Records `subject`'s origins, once.
32///
33/// # Errors
34///
35/// It was recorded before, or the store cannot be written.
36pub 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}