1use std::collections::BTreeMap;
6
7use serde_json::Value;
8
9pub struct SurfaceEntry {
11 pub path: String,
12 pub kind: String,
13 pub signature: String,
14 pub pkg: String,
18 pub summary: String,
20 pub docs: String,
24}
25
26pub fn render(pkg: &str, entries: &[SurfaceEntry], baseline: &Value) -> String {
30 let mut measured: BTreeMap<String, String> = BTreeMap::new();
37 if let Some(items) = baseline["items"].as_object() {
38 for (id, m) in items {
39 let target = m["covers"].as_str().filter(|c| !c.is_empty()).unwrap_or(id);
40 let mut facts = Vec::new();
41 if let Some(i) = m["perfcnt"]["instructions"].as_u64() {
42 facts.push(format!("{i} instructions/iter"));
43 }
44 if let Some(ns) = m["walltime"]["median_ns"].as_f64() {
45 facts.push(format!("{ns:.0}ns median"));
46 }
47 if let Some(a) = m["alloc"]["allocs"].as_u64() {
48 facts.push(format!("{a} allocs/iter"));
49 }
50 if let Some(p) = m["asyncexec"]["polls"].as_u64() {
51 facts.push(format!("{p} polls/iter"));
52 }
53 if !facts.is_empty() {
54 measured.insert(target.to_string(), facts.join(", "));
55 }
56 }
57 }
58
59 let mut groups: BTreeMap<&str, Vec<&SurfaceEntry>> = BTreeMap::new();
61 for e in entries {
62 if e.kind == "module" {
63 continue;
64 }
65 groups.entry(e.pkg.as_str()).or_default().push(e);
66 }
67 let multi_crate = groups.len() > 1;
70 let item_level = if multi_crate { "####" } else { "###" };
71
72 let mut out = format!(
73 "# {pkg}\n\n> Generated by `cargo soothfast report`, derived from the current code and\n> measurements rather than hand-written. Trust it over any prose.\n\n## Public API\n\n"
74 );
75 for (crate_name, items) in &groups {
76 if multi_crate {
77 out.push_str(&format!("### {crate_name}\n\n"));
78 }
79 for e in items {
80 let head = if e.signature.is_empty() {
81 e.path.as_str()
82 } else {
83 e.signature.as_str()
84 };
85 out.push_str(&format!("{item_level} `{head}`\n\n"));
86 let doc = e.docs.trim();
87 if !doc.is_empty() {
88 out.push_str(&nest_headings(doc, item_level.len()));
89 out.push_str("\n\n");
90 }
91 if let Some(facts) = measured.get(&e.path) {
92 out.push_str(&format!("**Measured:** {facts}\n\n"));
93 }
94 }
95 }
96 out
97}
98
99fn nest_headings(text: &str, item_depth: usize) -> String {
105 let mut in_fence = false;
106 text.lines()
107 .map(|line| {
108 if line.trim_start().starts_with("```") {
109 in_fence = !in_fence;
110 return line.to_string();
111 }
112 if in_fence {
113 return line.to_string();
114 }
115 let hashes = line.chars().take_while(|&c| c == '#').count();
116 if hashes > 0 && hashes <= 6 && line.as_bytes().get(hashes) == Some(&b' ') {
117 let depth = (item_depth + hashes).min(6);
118 format!("{}{}", "#".repeat(depth), &line[hashes..])
119 } else {
120 line.to_string()
121 }
122 })
123 .collect::<Vec<_>>()
124 .join("\n")
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use serde_json::json;
131
132 fn entry(pkg: &str, path: &str, docs: &str) -> SurfaceEntry {
133 SurfaceEntry {
134 path: path.to_string(),
135 kind: "function".to_string(),
136 signature: format!("pub fn {path}()"),
137 pkg: pkg.to_string(),
138 summary: docs.lines().next().unwrap_or("").to_string(),
139 docs: docs.to_string(),
140 }
141 }
142
143 #[test]
144 fn nest_headings_shifts_rustdoc_sections_below_the_item_heading() {
145 let doc = "Does a thing.\n\n# Panics\n\nIf you misuse it.";
146 assert_eq!(
147 nest_headings(doc, 4),
148 "Does a thing.\n\n##### Panics\n\nIf you misuse it."
149 );
150 }
151
152 #[test]
153 fn nest_headings_ignores_hashes_inside_fenced_code() {
154 let doc = "Usage:\n\n```ignore\n# not a heading\n#[attr]\nfn f() {}\n```\n\n# Examples";
155 let shifted = nest_headings(doc, 3);
156 assert!(shifted.contains("```ignore\n# not a heading\n#[attr]"));
157 assert!(shifted.contains("#### Examples"));
158 }
159
160 #[test]
161 fn nest_headings_caps_at_depth_six() {
162 assert_eq!(nest_headings("###### already max", 4), "###### already max");
163 }
164
165 #[test]
166 fn render_groups_multiple_crates_under_their_own_heading() {
167 let entries = vec![
168 entry("soothfast", "soothfast::keep", "Optimizer barrier."),
169 entry(
170 "soothfast-macros",
171 "soothfast_macros::bench",
172 "Register a bench.",
173 ),
174 ];
175 let out = render("soothfast, soothfast-macros", &entries, &json!({}));
176 assert!(out.contains("### soothfast\n"));
177 assert!(out.contains("### soothfast-macros\n"));
178 assert!(out.contains("#### `pub fn soothfast::keep()`"));
179 }
180
181 #[test]
182 fn render_single_crate_omits_the_redundant_crate_heading() {
183 let entries = vec![entry("soothfast", "soothfast::keep", "Optimizer barrier.")];
184 let out = render("soothfast", &entries, &json!({}));
185 assert!(!out.contains("### soothfast\n"));
186 assert!(out.contains("### `pub fn soothfast::keep()`"));
187 }
188
189 #[test]
190 fn render_attaches_measured_facts_by_covers_or_id() {
191 let entries = vec![entry("soothfast", "soothfast::keep", "Optimizer barrier.")];
192 let baseline = json!({
193 "items": {
194 "soothfast::keep": {
195 "perfcnt": { "instructions": 42 },
196 "walltime": { "median_ns": 12.0 },
197 }
198 }
199 });
200 let out = render("soothfast", &entries, &baseline);
201 assert!(out.contains("**Measured:** 42 instructions/iter, 12ns median"));
202 }
203
204 #[test]
205 fn render_skips_module_kind_entries() {
206 let mut e = entry("soothfast", "soothfast::registry", "");
207 e.kind = "module".to_string();
208 let out = render("soothfast", &[e], &json!({}));
209 assert!(!out.contains("registry"));
210 }
211}