use std::path::Path;
use crate::package::{
collect_package_files, extract_api_symbols, push_api_symbol, PackageApiSymbol,
};
struct DocModule {
path: String,
symbols: Vec<PackageApiSymbol>,
}
pub(crate) fn run(path: &Path, output: Option<&Path>) -> i32 {
let modules = match collect_doc_modules(path) {
Ok(modules) => modules,
Err(error) => {
eprintln!("error: {error}");
return 1;
}
};
if modules.is_empty() {
eprintln!(
"error: no public (`pub`) symbols found under {}",
path.display()
);
return 1;
}
let rendered = render_docs(&modules);
match output {
Some(out) => {
if let Err(error) = harn_vm::atomic_io::atomic_write(out, rendered.as_bytes()) {
eprintln!("error: failed to write {}: {error}", out.display());
return 1;
}
let symbols: usize = modules.iter().map(|module| module.symbols.len()).sum();
eprintln!(
"Wrote {symbols} symbol(s) from {} module(s) to {}",
modules.len(),
out.display()
);
}
None => print!("{rendered}"),
}
0
}
fn collect_doc_modules(path: &Path) -> Result<Vec<DocModule>, String> {
let metadata = std::fs::metadata(path)
.map_err(|error| format!("failed to read {}: {error}", path.display()))?;
let mut modules = Vec::new();
if metadata.is_file() {
if let Some(module) = doc_module_for_file(path, &path.to_string_lossy())? {
modules.push(module);
}
return Ok(modules);
}
let files = collect_package_files(path).map_err(|error| error.to_string())?;
for rel in files {
if !rel.ends_with(".harn") {
continue;
}
if let Some(module) = doc_module_for_file(&path.join(&rel), &rel)? {
modules.push(module);
}
}
Ok(modules)
}
fn doc_module_for_file(full: &Path, display: &str) -> Result<Option<DocModule>, String> {
let source = std::fs::read_to_string(full)
.map_err(|error| format!("failed to read {}: {error}", full.display()))?;
let symbols = extract_api_symbols(&source);
if symbols.is_empty() {
return Ok(None);
}
Ok(Some(DocModule {
path: display.replace('\\', "/"),
symbols,
}))
}
fn render_docs(modules: &[DocModule]) -> String {
let mut out = String::from("# API Reference\n\nGenerated by `harn doc`.\n");
for module in modules {
out.push_str(&format!("\n## `{}`\n", module.path));
for symbol in &module.symbols {
push_api_symbol(&mut out, symbol);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
const MATH_SOURCE: &str = "\
/// Adds two integers.
/// @effects none
/// @errors none
pub fn add(a: int, b: int) -> int {
a + b
}
/// Circle constant to a few digits.
pub const PI: float = 3.14159
/// A 2D point in the plane.
pub struct Point {
x: float,
y: float,
}
/// An undocumented-below private helper stays out of the docs.
fn private_helper(n: int) -> int {
n * 2
}
";
#[test]
fn doc_renders_pub_symbols_with_descriptions_and_signatures() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("math.harn"), MATH_SOURCE).unwrap();
let out = tmp.path().join("api.md");
assert_eq!(run(tmp.path(), Some(&out)), 0);
let doc = fs::read_to_string(&out).unwrap();
assert!(doc.contains("### fn `add`"), "{doc}");
assert!(doc.contains("Adds two integers."), "{doc}");
assert!(doc.contains("pub fn add(a: int, b: int) -> int"), "{doc}");
assert!(doc.contains("### const `PI`"), "{doc}");
assert!(doc.contains("Circle constant to a few digits."), "{doc}");
assert!(doc.contains("pub const PI: float"), "{doc}");
assert!(doc.contains("### struct `Point`"), "{doc}");
assert!(doc.contains("A 2D point in the plane."), "{doc}");
assert!(doc.contains("@effects none"), "{doc}");
assert!(doc.contains("@errors none"), "{doc}");
assert!(doc.contains("src/math.harn"), "{doc}");
assert!(!doc.contains("private_helper"), "{doc}");
}
#[test]
fn doc_targets_a_single_file() {
let tmp = tempfile::tempdir().unwrap();
let file = tmp.path().join("lib.harn");
fs::write(
&file,
"/// Greets a name.\npub fn greet(name: string) -> string {\n \"hi\"\n}\n",
)
.unwrap();
let out = tmp.path().join("out.md");
assert_eq!(run(&file, Some(&out)), 0);
let doc = fs::read_to_string(&out).unwrap();
assert!(doc.contains("### fn `greet`"), "{doc}");
assert!(doc.contains("Greets a name."), "{doc}");
assert!(
doc.contains("pub fn greet(name: string) -> string"),
"{doc}"
);
}
#[test]
fn doc_reports_when_no_public_symbols_exist() {
let tmp = tempfile::tempdir().unwrap();
fs::write(tmp.path().join("private.harn"), "fn only_private() {}\n").unwrap();
let out = tmp.path().join("api.md");
assert_eq!(run(tmp.path(), Some(&out)), 1);
assert!(!out.exists());
}
}