soothfast_docs/diff.rs
1//! Public-surface diff between two builds: what a reviewer needs to see
2//! about API-relevant change, and the raw material for changelog drafting.
3
4use crate::surface::Surface;
5
6/// Public-API delta between two [`Surface`] snapshots, by item path.
7#[derive(Debug, Default)]
8pub struct SurfaceDiff {
9 /// Item paths present only in the new surface.
10 pub added: Vec<String>,
11 /// Item paths present only in the old surface.
12 pub removed: Vec<String>,
13 /// (path, signature_changed) — false means body-only change. An enum's
14 /// variants and a struct's or union's `pub` fields count as signature,
15 /// so one added, removed, or retyped is never "body".
16 pub changed: Vec<(String, bool)>,
17}
18
19impl SurfaceDiff {
20 /// True when the two surfaces are identical: nothing added, removed,
21 /// or changed.
22 pub fn is_empty(&self) -> bool {
23 self.added.is_empty() && self.removed.is_empty() && self.changed.is_empty()
24 }
25}
26
27/// Diff two surfaces: added/removed items by path, changed items by span
28/// fingerprint (modules are exempt — their span is the whole file).
29pub fn compare(old: &Surface, new: &Surface) -> SurfaceDiff {
30 let mut diff = SurfaceDiff::default();
31 for (path, info) in &new.items {
32 match old.items.get(path) {
33 None => diff.added.push(path.clone()),
34 Some(prev) => {
35 // A module's span is its whole file — any edit anywhere would
36 // flag it. Added/removed modules still report.
37 if info.kind != "module" && prev.fingerprint != info.fingerprint {
38 diff.changed
39 .push((path.clone(), prev.signature != info.signature));
40 }
41 }
42 }
43 }
44 for path in old.items.keys() {
45 if !new.items.contains_key(path) {
46 diff.removed.push(path.clone());
47 }
48 }
49 diff
50}
51
52/// One `ADDED`/`REMOVED`/`CHANGED` line per item, ready for terminals and
53/// changelog drafts; an empty diff renders as a single "no changes" line.
54pub fn render(diff: &SurfaceDiff) -> String {
55 if diff.is_empty() {
56 return "surface: no public API changes\n".into();
57 }
58 let mut out = String::new();
59 for p in &diff.added {
60 out.push_str(&format!("ADDED {p}\n"));
61 }
62 for p in &diff.removed {
63 out.push_str(&format!("REMOVED {p}\n"));
64 }
65 for (p, sig) in &diff.changed {
66 let what = if *sig { "signature" } else { "body" };
67 out.push_str(&format!("CHANGED {p} ({what})\n"));
68 }
69 out
70}