Skip to main content

krabka_docgen/
lib.rs

1//! Generates Zola markdown reference pages from Krabka's in-process
2//! source-of-truth data structures.
3//!
4//! These structures are the broker config schema, the topic configs, the
5//! protocol API catalog, and the recorded traces of the `KRaft` simulator.
6//! The tool links the broker as a library. It does not spawn the broker
7//! binary. Reference generation does not read Rust source; snippet
8//! synchronization reads the source regions that Markdown pages name.
9//!
10//! `docs/docgen-contract.md` names every entry point the renderers read.
11//! `crates/broker/tests/docgen_contract.rs` holds the broker to that shape.
12
13use std::path::Path;
14
15pub mod broker;
16pub mod emit;
17pub mod scenarios;
18pub mod schema_md;
19pub mod snippets;
20
21/// Rewrite snippet blocks in every `.md` under `content_dir`.
22///
23/// The function pulls the code from source files under `crates_dir`. It returns
24/// the number of files changed.
25///
26/// # Errors
27/// Returns an error if a directory walk, file read/write, or snippet sync fails.
28pub fn sync_snippets(content_dir: &Path, crates_dir: &Path) -> anyhow::Result<usize> {
29    use std::fs;
30    let mut changed = 0;
31    let mut stack = vec![content_dir.to_path_buf()];
32    while let Some(d) = stack.pop() {
33        for entry in fs::read_dir(&d)? {
34            let path = entry?.path();
35            if path.is_dir() {
36                stack.push(path);
37            } else if path.extension().is_some_and(|e| e == "md") {
38                let before = fs::read_to_string(&path)?;
39                let after = snippets::sync_markdown(&before, crates_dir)
40                    .map_err(|e| anyhow::anyhow!("{}: {e}", path.display()))?;
41                if after != before {
42                    fs::write(&path, after)?;
43                    changed += 1;
44                }
45            }
46        }
47    }
48    Ok(changed)
49}
50
51#[cfg(test)]
52mod tests {
53    use std::fs;
54
55    #[test]
56    fn sync_snippets_rewrites_nested_markdown_and_is_idempotent() {
57        let dir = tempfile::tempdir().unwrap();
58        let crates = dir.path().join("crates");
59        let content = dir.path().join("content");
60        fs::create_dir_all(crates.join("c/examples")).unwrap();
61        fs::create_dir_all(content.join("sub")).unwrap();
62        fs::write(
63            crates.join("c/examples/e.rs"),
64            "// docs:begin a\nlet z = 9;\n// docs:end a\n",
65        )
66        .unwrap();
67        let md = content.join("sub/page.md");
68        fs::write(
69            &md,
70            "intro\n<!-- snippet: c/examples/e.rs#a -->\nOLD\n<!-- /snippet -->\nend\n",
71        )
72        .unwrap();
73        // A non-markdown file is left untouched (extension filter).
74        fs::write(content.join("ignore.txt"), "OLD").unwrap();
75
76        let changed = super::sync_snippets(&content, &crates).unwrap();
77        assert2::assert!(changed == 1);
78        let out = fs::read_to_string(&md).unwrap();
79        assert2::assert!(out.contains("```rust\nlet z = 9;\n```"));
80        assert2::assert!(!out.contains("OLD"));
81        assert2::assert!(fs::read_to_string(content.join("ignore.txt")).unwrap() == "OLD");
82
83        // Second run is a no-op: already in sync.
84        assert2::assert!(super::sync_snippets(&content, &crates).unwrap() == 0);
85    }
86}