use std::path::Path;
fn heading_rpc(line: &str) -> Option<String> {
let line = line.trim_start().strip_prefix("///")?.trim();
let rest = if let Some(r) = line.strip_prefix("**`Admin.") {
r
} else {
let at = line.find("** (`Admin.")?;
if !line.starts_with("**") {
return None;
}
&line[at + "** (`Admin.".len()..]
};
let name: String = rest
.chars()
.take_while(|c| c.is_ascii_alphanumeric())
.collect();
(!name.is_empty()).then_some(name)
}
fn snake(camel: &str) -> String {
let mut out = String::new();
for (i, c) in camel.chars().enumerate() {
if c.is_ascii_uppercase() {
if i > 0 {
out.push('_');
}
out.push(c.to_ascii_lowercase());
} else {
out.push(c);
}
}
out
}
fn fn_name(line: &str) -> Option<String> {
let at = line.find("fn ")?;
let before = line[..at].trim();
if !before
.split_whitespace()
.all(|w| matches!(w, "pub" | "pub(crate)" | "async" | "const" | "unsafe"))
{
return None;
}
let name: String = line[at + 3..]
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
(!name.is_empty()).then_some(name)
}
fn pairs(src: &str) -> (Vec<(String, String)>, Vec<String>) {
let lines: Vec<&str> = src.lines().collect();
let (mut out, mut doubled) = (Vec::new(), Vec::new());
let mut i = 0;
while i < lines.len() {
if !lines[i].trim_start().starts_with("///") {
i += 1;
continue;
}
let start = i;
while i < lines.len() && lines[i].trim_start().starts_with("///") {
i += 1;
}
let headings: Vec<String> = lines[start..i]
.iter()
.filter_map(|l| heading_rpc(l))
.collect();
if headings.len() > 1 {
doubled.push(format!("line {}: {headings:?}", start + 1));
}
let Some(first) = heading_rpc(lines[start]) else {
continue;
};
let mut j = i;
while j < lines.len() && lines[j].trim_start().starts_with("#[") {
j += 1;
}
let headed = lines
.get(j)
.and_then(|l| fn_name(l))
.unwrap_or_else(|| "(not a fn)".to_owned());
out.push((first, headed));
}
(out, doubled)
}
fn check(rel: &str, must_have: &[&str]) {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(rel);
let src =
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let (pairs, doubled) = pairs(&src);
assert!(
doubled.is_empty(),
"{rel}: a doc block names more than one Admin RPC: {doubled:?}"
);
let wrong: Vec<_> = pairs.iter().filter(|(rpc, f)| snake(rpc) != *f).collect();
assert!(
wrong.is_empty(),
"{rel}: an `Admin.X` doc heads the wrong fn (rpc, fn): {wrong:?}"
);
for rpc in must_have {
assert!(
pairs.iter().any(|(r, f)| r == rpc && *f == snake(rpc)),
"{rel}: `{}` has no doc naming `Admin.{rpc}`",
snake(rpc)
);
}
}
#[test]
fn the_grpc_service_docs_head_their_rpcs() {
check(
"../gunnar-grpc/src/service/admin.rs",
&["AuditTrail", "GetSetting", "SetSetting", "Topology"],
);
}
#[test]
fn the_control_plane_docs_head_their_rpcs() {
check("src/control_plane.rs", &["AuditTrail", "IssuerInfo"]);
}
#[test]
fn the_reader_sees_both_heading_forms() {
assert_eq!(
heading_rpc(" /// **`Admin.AuditTrail`** (lane T11): x").as_deref(),
Some("AuditTrail")
);
assert_eq!(
heading_rpc(" /// **The chain** (`Admin.AuditTrail`, lane T11).").as_deref(),
Some("AuditTrail")
);
assert_eq!(
heading_rpc(" /// **What replication is doing**, for `Admin.Stats`."),
None
);
assert_eq!(snake("GetSetting"), "get_setting");
assert_eq!(
fn_name(" async fn audit_trail(").as_deref(),
Some("audit_trail")
);
assert_eq!(
fn_name(" pub(crate) fn setting(&self"),
Some("setting".to_owned())
);
let (p, d) = pairs(
"/// **`Admin.AuditTrail`** a\n/// **`Admin.GetSetting`** b\nasync fn get_setting(\n",
);
assert_eq!(p, vec![("AuditTrail".to_owned(), "get_setting".to_owned())]);
assert_eq!(d.len(), 1);
}