use std::fs;
use std::path::Path;
use anyhow::Context;
use serde::Deserialize;
use crate::entity;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub(crate) struct Meta {
pub(crate) id: u32,
pub(crate) slug: String,
pub(crate) title: String,
pub(crate) status: String,
}
pub(crate) fn read_meta(tree_root: &Path, stem: &str, id: u32) -> anyhow::Result<Meta> {
let name = format!("{id:03}");
let path = tree_root.join(&name).join(format!("{stem}-{name}.toml"));
let text = fs::read_to_string(&path)
.with_context(|| format!("{stem} {name} not found at {}", path.display()))?;
toml::from_str(&text).with_context(|| format!("Failed to parse {}", path.display()))
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub(crate) struct IdOnly {
pub(crate) id: u32,
}
pub(crate) fn read_id(tree_root: &Path, stem: &str, id: u32) -> anyhow::Result<u32> {
let name = format!("{id:03}");
let path = tree_root.join(&name).join(format!("{stem}-{name}.toml"));
let text = fs::read_to_string(&path)
.with_context(|| format!("{stem} {name} not found at {}", path.display()))?;
let parsed: IdOnly =
toml::from_str(&text).with_context(|| format!("Failed to parse {}", path.display()))?;
Ok(parsed.id)
}
pub(crate) fn read_metas(tree_root: &Path, stem: &str) -> anyhow::Result<Vec<Meta>> {
let mut metas = Vec::new();
for id in entity::scan_ids(tree_root)? {
metas.push(read_meta(tree_root, stem, id)?);
}
Ok(metas)
}
#[cfg(test)]
mod tests {
use super::*;
fn meta(id: u32, status: &str, slug: &str, title: &str) -> Meta {
Meta {
id,
slug: slug.to_string(),
title: title.to_string(),
status: status.to_string(),
}
}
fn write_meta_toml(tree_root: &Path, stem: &str, id: u32, status: &str, slug: &str) {
let name = format!("{id:03}");
let dir = tree_root.join(&name);
fs::create_dir_all(&dir).unwrap();
let body = format!(
"id = {id}\nslug = \"{slug}\"\ntitle = \"Title {id}\"\nstatus = \"{status}\"\n"
);
fs::write(dir.join(format!("{stem}-{name}.toml")), body).unwrap();
}
#[test]
fn read_meta_reads_the_stem_toml() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
write_meta_toml(root, "slice", 1, "proposed", "my-slug");
let m = read_meta(root, "slice", 1).unwrap();
assert_eq!(m, meta(1, "proposed", "my-slug", "Title 1"));
}
#[test]
fn read_meta_is_parameterised_by_stem() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
write_meta_toml(root, "adr", 7, "accepted", "use-rust");
let m = read_meta(root, "adr", 7).unwrap();
assert_eq!(m.status, "accepted");
assert!(read_meta(root, "slice", 7).is_err());
}
fn write_statusless_toml(tree_root: &Path, stem: &str, id: u32) {
let name = format!("{id:03}");
let dir = tree_root.join(&name);
fs::create_dir_all(&dir).unwrap();
let body = format!("id = {id}\nslug = \"sl\"\ntitle = \"T {id}\"\n");
fs::write(dir.join(format!("{stem}-{name}.toml")), body).unwrap();
}
#[test]
fn read_id_scans_a_statusless_toml() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
write_statusless_toml(root, "review", 7);
assert_eq!(read_id(root, "review", 7).unwrap(), 7);
}
#[test]
fn read_meta_still_hard_fails_on_a_missing_status() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
write_statusless_toml(root, "slice", 7);
let err = read_meta(root, "slice", 7).unwrap_err();
assert!(
err.to_string().contains("Failed to parse"),
"missing status must be a hard parse error: {err}"
);
}
#[test]
fn read_metas_collects_every_numeric_and_skips_the_rest() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
write_meta_toml(root, "slice", 2, "proposed", "two");
write_meta_toml(root, "slice", 1, "done", "one");
std::os::unix::fs::symlink("001", root.join("001-one")).unwrap();
fs::create_dir_all(root.join("notes")).unwrap();
let mut ids: Vec<u32> = read_metas(root, "slice")
.unwrap()
.iter()
.map(|m| m.id)
.collect();
ids.sort_unstable();
assert_eq!(ids, vec![1, 2]);
}
}