use std::path::Path;
use anyhow::Context;
use super::{KINDS, KindRef};
use crate::fsutil;
pub(crate) fn kind_by_prefix(prefix: &str) -> Option<&'static KindRef> {
KINDS.iter().find(|k| k.kind.prefix == prefix)
}
pub(crate) fn ensure_ref_resolves(root: &Path, reference: &str) -> anyhow::Result<()> {
parse_resolvable_ref(root, reference)?;
Ok(())
}
pub(crate) fn canonical_id(prefix: &str, id: u32) -> String {
format!("{prefix}-{id:03}")
}
pub(crate) fn parse_canonical_ref(reference: &str) -> anyhow::Result<(&'static KindRef, u32)> {
let (prefix, num) = reference
.rsplit_once('-')
.with_context(|| format!("`{reference}` is not a canonical ref (expected e.g. SL-031)"))?;
let kind = kind_by_prefix(prefix)
.with_context(|| format!("unknown kind prefix `{prefix}` in `{reference}`"))?;
let id = num
.parse::<u32>()
.with_context(|| format!("`{num}` is not a numeric id in `{reference}`"))?;
Ok((kind, id))
}
pub(crate) fn parse_resolvable_ref(
root: &Path,
reference: &str,
) -> anyhow::Result<(&'static KindRef, u32)> {
if reference.contains('-') {
let (kref, id) = parse_canonical_ref(reference)?;
let name = format!("{id:03}");
let dir = root.join(kref.kind.dir).join(&name);
anyhow::ensure!(
fsutil::is_real_dir(&dir),
"`{reference}` does not resolve to an entity (no {} at {})",
canonical_id(kref.kind.prefix, id),
dir.display()
);
return Ok((kref, id));
}
let id: u32 = reference.parse().with_context(|| {
format!("`{reference}` is not a valid entity reference (expected e.g. SL-031 or 31)")
})?;
let name = format!("{id:03}");
let mut matches: Vec<&'static KindRef> = Vec::new();
for kref in KINDS {
let dir = root.join(kref.kind.dir).join(&name);
if fsutil::is_real_dir(&dir) {
matches.push(kref);
}
}
match matches.as_slice() {
[] => anyhow::bail!(
"no entity found for bare id `{reference}` — try the prefixed form (e.g. SL-{name})"
),
[single] => Ok((single, id)),
many => {
let ids: Vec<String> = many
.iter()
.map(|k| canonical_id(k.kind.prefix, id))
.collect();
anyhow::bail!(
"bare id `{reference}` is ambiguous — matches {}; use the prefixed form",
ids.join(", ")
)
}
}
}
pub(crate) fn scanned_kinds() -> String {
KINDS
.iter()
.map(|k| k.kind.prefix)
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ensure_ref_resolves_guards_the_forward_edge() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let dir = root.join(".doctrine/slice/024");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("slice-024.toml"), "id = 24\n").unwrap();
assert!(ensure_ref_resolves(root, "SL-024").is_ok());
let dangling = ensure_ref_resolves(root, "SL-099").unwrap_err();
assert!(
dangling.to_string().contains("does not resolve"),
"{dangling}"
);
let unknown = ensure_ref_resolves(root, "ZZ-001").unwrap_err();
assert!(
unknown.to_string().contains("unknown kind prefix"),
"{unknown}"
);
}
#[test]
fn canonical_id_round_trips_through_parse_canonical_ref() {
assert_eq!(canonical_id("ADR", 1), "ADR-001");
let (kind, id) = parse_canonical_ref(&canonical_id("SL", 31)).expect("round-trip");
assert_eq!(kind.kind.prefix, "SL");
assert_eq!(id, 31);
}
#[test]
fn parse_canonical_ref_resolves_kind_and_id() {
let (kind, id) = parse_canonical_ref("SL-031").expect("valid ref");
assert_eq!(kind.kind.prefix, "SL");
assert_eq!(id, 31);
assert!(
parse_canonical_ref("031").is_err(),
"bare id is not canonical"
);
assert!(
parse_canonical_ref("ZZ-001").is_err(),
"unknown prefix rejected"
);
assert!(
parse_canonical_ref("SL-x").is_err(),
"non-numeric id rejected"
);
}
}