Skip to main content

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}