use std::collections::{BTreeMap, BTreeSet};
const BASELINE: &str = include_str!("../docs/architecture/public-api.txt");
const PUBLIC_MODULES: &[&str] = &[
"macrame",
"macrame::branch",
"macrame::connection",
"macrame::connection::chunk_rows",
"macrame::error",
"macrame::graph",
"macrame::integrity",
"macrame::metrics",
"macrame::prelude",
"macrame::schema",
"macrame::schema::ddl",
"macrame::temporal",
"macrame::util",
"macrame::util::timestamp",
"macrame::vector",
];
fn subject(line: &str) -> Option<&str> {
let start = line.find("macrame::")?;
let rest = &line[start..];
let end = rest
.char_indices()
.find(|(i, c)| {
!(c.is_alphanumeric()
|| *c == '_'
|| (*c == ':' && rest[*i..].starts_with("::"))
|| (*c == ':' && rest[..*i].ends_with(':')))
})
.map(|(i, _)| i)
.unwrap_or(rest.len());
Some(rest[..end].trim_end_matches(':'))
}
fn module_chain(path: &str) -> Vec<&str> {
let seg: Vec<&str> = path.split("::").skip(1).collect();
match seg
.iter()
.position(|s| s.starts_with(|c: char| c.is_ascii_uppercase()))
{
Some(i) => seg[..i].to_vec(),
None => seg[..seg.len().saturating_sub(1)].to_vec(),
}
}
fn is_convenience(path: &str) -> bool {
let chain = module_chain(path);
chain.is_empty() || chain == ["prelude"]
}
fn identity(line: &str, path: &str) -> String {
let seg: Vec<&str> = path.split("::").skip(1).collect();
let tail = match seg
.iter()
.position(|s| s.starts_with(|c: char| c.is_ascii_uppercase()))
{
Some(i) => seg[i..].join("::"),
None => seg.last().copied().unwrap_or_default().to_string(),
};
line.replacen(path, &tail, 1)
}
fn items() -> Vec<(String, String)> {
BASELINE
.lines()
.map(str::trim_end)
.filter(|l| !l.is_empty() && (!l.starts_with('#') || l.starts_with("#[")))
.filter(|l| !l.starts_with("pub mod "))
.filter_map(|l| subject(l).map(|p| (identity(l, p), p.to_string())))
.collect()
}
fn declared_modules() -> BTreeSet<String> {
BASELINE
.lines()
.filter_map(|l| l.strip_prefix("pub mod "))
.map(|p| p.trim().to_string())
.collect()
}
#[test]
fn every_public_item_has_exactly_one_canonical_path() {
let mut by_item: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for (ident, path) in items() {
if !is_convenience(&path) {
by_item.entry(ident).or_default().insert(path);
}
}
let dupes: Vec<_> = by_item.iter().filter(|(_, v)| v.len() > 1).collect();
let surplus: usize = dupes.iter().map(|(_, v)| v.len() - 1).sum();
let mut report = String::new();
for (ident, paths) in dupes.iter().take(40) {
report.push_str(&format!("\n {ident}\n {:?}", paths));
}
assert!(
dupes.is_empty(),
"{} items are reachable at more than one canonical path ({surplus} \
surplus paths). Each surplus path is a module this crate would have to \
keep, and a file it could then never move, for the life of 1.x. The \
fix is to make the inner module `pub(crate)` and let the parent's \
`pub use` be the one path (D-208). The first 40:{report}",
dupes.len()
);
}
#[test]
fn the_public_module_list_is_the_one_recorded_here() {
let declared = declared_modules();
let expected: BTreeSet<String> = PUBLIC_MODULES.iter().map(|s| s.to_string()).collect();
let added: Vec<_> = declared.difference(&expected).collect();
assert!(
added.is_empty(),
"public modules that this file does not account for: {added:?}. A `pub \
mod` is a path the crate supports for the life of 1.x and a file it \
cannot then move. Adding one is fine; adding one silently is what \
produced thirty-nine of them (D-205). Say why here."
);
let gone: Vec<_> = expected.difference(&declared).collect();
assert!(
gone.is_empty(),
"this file expects public modules the surface does not have: {gone:?}"
);
}
#[test]
fn the_convenience_surfaces_carry_names_and_not_namespaces() {
let nested: Vec<_> = declared_modules()
.into_iter()
.filter(|m| m.starts_with("macrame::prelude::"))
.collect();
assert!(
nested.is_empty(),
"the prelude re-exports whole modules: {nested:?}. A prelude exists so \
that one `use` brings in the names a caller needs; re-exporting a \
module brings in a second *namespace*, which is a second canonical \
path to everything inside it and a second name for the module itself. \
`pub use crate::temporal::{{archive, …}}` is how this happens by \
accident: `archive` is both a module and a function there, an explicit \
import binds the name in both namespaces, and re-exporting the \
`pub(crate)` module through a `pub use` is neither an error nor a \
warning — it silently republishes it here (D-208)."
);
}
#[test]
fn the_baseline_is_shaped_the_way_these_tests_assume() {
let all = items();
assert!(
all.len() > 1_000,
"only {} item lines parsed out of the baseline; the format changed and \
these tests are measuring nothing",
all.len()
);
assert!(
declared_modules().len() >= 10,
"only {} modules parsed; see above",
declared_modules().len()
);
assert_eq!(
module_chain("macrame::graph::builder::AttributeMode"),
["graph", "builder"]
);
assert_eq!(module_chain("macrame::graph::astar"), ["graph"]);
assert!(module_chain("macrame::DbError").is_empty());
assert!(is_convenience("macrame::prelude::DbError"));
assert!(!is_convenience("macrame::error::DbError"));
}