///|
struct ReadmeApiVerifyBlock {
interface_path : String
signatures : Array[String]
} derive(Eq, Debug)
///|
pub impl Show for ReadmeApiVerifyBlock with fn output(self, logger) {
logger.write_string(@debug.to_string(self))
}
///|
fn ReadmeApiVerifyBlock::new(
interface_path : String,
signatures : Array[String],
) -> ReadmeApiVerifyBlock {
{ interface_path, signatures, }
}
///|
fn ras_has_prefix(text : String, prefix : String) -> Bool {
if text.length() < prefix.length() {
return false
}
let head = text.sub(end=prefix.length()).to_owned()
head == prefix
}
///|
fn ras_has_suffix(text : String, suffix : String) -> Bool {
if text.length() < suffix.length() {
return false
}
let start = text.length() - suffix.length()
let tail = text.sub(start~).to_owned()
tail == suffix
}
///|
fn ras_extract_marker_path(line : String) -> String? {
let marker = line.trim().to_owned()
let prefix = ""
if !ras_has_prefix(marker, prefix) {
return None
}
if !ras_has_suffix(marker, suffix) {
return None
}
let start = prefix.length()
let end = marker.length() - suffix.length()
if end <= start {
return None
}
let path = marker.sub(start~, end~).to_owned()
let normalized = path.trim().to_owned()
if normalized.length() == 0 {
None
} else {
Some(normalized)
}
}
///|
/// Join collected error lines into one newline-separated diagnostic block.
fn ras_join_lines_impl(lines : Array[String]) -> String {
let out = StringBuilder::new()
let mut i = 0
while i < lines.length() {
if i > 0 {
out.write_string("\n")
}
out.write_string(lines[i])
i += 1
}
out.to_string()
}
///|
/// Parse all ````README_API_VERIFY`` blocks in one pass, then validate block shape:
/// marker -> optional blank lines -> opening fence -> at least one signature -> closing fence.
/// This keeps failure reasons actionable and local to the declaration layout.
/// Marker syntax is strict for reproducible checks:
/// `` only, with optional whitespace and optional
/// blank lines before the following ````mbti` block.
fn parse_readme_api_verify_blocks(
readme_text : String,
) -> Result[Array[ReadmeApiVerifyBlock], String] {
let lines : Array[String] = []
for line in readme_text.split("\n") {
lines.push(line.to_owned())
}
let blocks : Array[ReadmeApiVerifyBlock] = []
let mut i = 0
while i < lines.length() {
let line = lines[i]
match ras_extract_marker_path(line) {
Some(interface_path) => {
i += 1
while i < lines.length() && lines[i].trim().to_owned().length() == 0 {
i += 1
}
if i >= lines.length() || lines[i].trim().to_owned() != "```mbti" {
return Err(
"README_API_VERIFY marker for \{interface_path} must be followed by a ```mbti code block",
)
}
i += 1
let signatures : Array[String] = []
while i < lines.length() {
let sig = lines[i].trim().to_owned()
if sig == "```" {
break
}
if sig.length() > 0 {
signatures.push(sig)
}
i += 1
}
if i >= lines.length() {
return Err(
"README_API_VERIFY marker for \{interface_path} has an unterminated code block",
)
}
if signatures.length() == 0 {
return Err(
"README_API_VERIFY marker for \{interface_path} has no signatures",
)
}
blocks.push(ReadmeApiVerifyBlock::new(interface_path, signatures))
}
None => ()
}
i += 1
}
if blocks.length() == 0 {
return Err("README contains no README_API_VERIFY blocks")
}
Ok(blocks)
}
///|
fn ras_find_interface_text(
interface_text_by_path : Array[(String, String)],
path : String,
) -> String? {
for pair in interface_text_by_path {
if pair.0 == path {
return Some(pair.1)
}
}
None
}
///|
fn ras_has_block_for_path(
blocks : Array[ReadmeApiVerifyBlock],
path : String,
) -> Bool {
for block in blocks {
if block.interface_path == path {
return true
}
}
false
}
///|
/// Resolve required blocks and validate that every listed signature appears in the
/// corresponding interface text, while collecting all diagnostics and reporting one
/// aggregated error message when inconsistencies exist.
pub fn verify_readme_api_signatures_with_required_blocks(
readme_text : String,
interface_text_by_path : Array[(String, String)],
required_block_paths : Array[String],
) -> Result[Unit, String] {
let blocks = match parse_readme_api_verify_blocks(readme_text) {
Ok(v) => v
Err(e) => return Err(e)
}
let diagnostics : Array[String] = []
for raw_path in required_block_paths {
let path = raw_path.trim().to_owned()
if path.length() == 0 {
continue
}
if !ras_has_block_for_path(blocks, path) {
diagnostics.push("missing required README_API_VERIFY block: \{path}")
}
}
for block in blocks {
let interface_text = match
ras_find_interface_text(interface_text_by_path, block.interface_path) {
Some(v) => v
None => {
diagnostics.push(
"missing interface file for README_API_VERIFY block: \{block.interface_path}",
)
continue
}
}
for signature in block.signatures {
if !interface_text.contains(signature) {
diagnostics.push(
"missing signature in \{block.interface_path}: \{signature}",
)
}
}
}
if diagnostics.length() == 0 {
Ok(())
} else {
Err(ras_join_lines_impl(diagnostics))
}
}
///|
pub fn verify_readme_api_signatures(
readme_text : String,
interface_text_by_path : Array[(String, String)],
) -> Result[Unit, String] {
verify_readme_api_signatures_with_required_blocks(
readme_text,
interface_text_by_path,
[],
)
}