Skip to main content

planarian_gym/
catalog.rs

1//! The fault catalog on disk: a directory of TOML files (`00-format.toml`,
2//! then one file per family), read in filename order and concatenated into
3//! one TOML document, or a single file. Ids are unique across the files.
4
5use std::path::Path;
6use toml::Value as T;
7
8/// The Phase A catalog, relative to the repository root.
9pub const DIR: &str = "crates/gym/catalog/phase-a";
10
11/// The catalog's text: a file's, or a directory's `*.toml` files in
12/// filename order, concatenated.
13pub fn read_text(path: &Path) -> Result<String, String> {
14    if !path.is_dir() {
15        return std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()));
16    }
17    let mut files: Vec<_> = std::fs::read_dir(path)
18        .map_err(|e| format!("{}: {e}", path.display()))?
19        .flatten()
20        .map(|e| e.path())
21        .filter(|p| p.extension().is_some_and(|x| x == "toml"))
22        .collect();
23    files.sort();
24    if files.is_empty() {
25        return Err(format!("{}: no .toml files", path.display()));
26    }
27    let mut text = String::new();
28    for f in files {
29        text.push_str(&std::fs::read_to_string(&f).map_err(|e| format!("{}: {e}", f.display()))?);
30        text.push('\n');
31    }
32    Ok(text)
33}
34
35/// The entries, with every id present and unique.
36pub fn entries(path: &Path) -> Result<Vec<T>, String> {
37    let text = read_text(path)?;
38    let doc: T = toml::from_str(&text).map_err(|e| format!("{}: {e}", path.display()))?;
39    let faults = doc.get("fault").and_then(T::as_array).cloned().unwrap_or_default();
40    let mut seen = std::collections::BTreeSet::new();
41    for e in &faults {
42        let id = e.get("id").and_then(T::as_str).ok_or_else(|| format!("{}: an entry has no id", path.display()))?;
43        if !seen.insert(id.to_string()) {
44            return Err(format!("{}: id {id} is used twice", path.display()));
45        }
46    }
47    Ok(faults)
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    /// The shipped catalog loads as a directory, its ids are unique, and a
55    /// duplicate id across two files is refused.
56    #[test]
57    fn the_catalog_is_a_directory_of_unique_ids() {
58        let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("catalog/phase-a");
59        let all = entries(&dir).unwrap();
60        assert!(all.len() > 100);
61        let tmp = std::env::temp_dir().join(format!("planarian-catalog-test-{}", std::process::id()));
62        std::fs::create_dir_all(&tmp).unwrap();
63        std::fs::write(tmp.join("a.toml"), "[[fault]]\nid = \"x\"\n").unwrap();
64        std::fs::write(tmp.join("b.toml"), "[[fault]]\nid = \"x\"\n").unwrap();
65        assert!(entries(&tmp).unwrap_err().contains("used twice"));
66        std::fs::write(tmp.join("b.toml"), "[[fault]]\nid = \"y\"\n").unwrap();
67        let ids: Vec<_> = entries(&tmp).unwrap().iter().map(|e| e["id"].as_str().unwrap().to_string()).collect();
68        let _ = std::fs::remove_dir_all(&tmp);
69        assert_eq!(ids, ["x", "y"], "filename order");
70    }
71}