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.
14    pub changed: Vec<(String, bool)>,
15}
16
17impl SurfaceDiff {
18    /// True when the two surfaces are identical: nothing added, removed,
19    /// or changed.
20    pub fn is_empty(&self) -> bool {
21        self.added.is_empty() && self.removed.is_empty() && self.changed.is_empty()
22    }
23}
24
25/// Diff two surfaces: added/removed items by path, changed items by span
26/// fingerprint (modules are exempt — their span is the whole file).
27pub fn compare(old: &Surface, new: &Surface) -> SurfaceDiff {
28    let mut diff = SurfaceDiff::default();
29    for (path, info) in &new.items {
30        match old.items.get(path) {
31            None => diff.added.push(path.clone()),
32            Some(prev) => {
33                // A module's span is its whole file — any edit anywhere would
34                // flag it. Added/removed modules still report.
35                if info.kind != "module" && prev.fingerprint != info.fingerprint {
36                    diff.changed
37                        .push((path.clone(), prev.signature != info.signature));
38                }
39            }
40        }
41    }
42    for path in old.items.keys() {
43        if !new.items.contains_key(path) {
44            diff.removed.push(path.clone());
45        }
46    }
47    diff
48}
49
50/// One `ADDED`/`REMOVED`/`CHANGED` line per item, ready for terminals and
51/// changelog drafts; an empty diff renders as a single "no changes" line.
52pub fn render(diff: &SurfaceDiff) -> String {
53    if diff.is_empty() {
54        return "surface: no public API changes\n".into();
55    }
56    let mut out = String::new();
57    for p in &diff.added {
58        out.push_str(&format!("ADDED    {p}\n"));
59    }
60    for p in &diff.removed {
61        out.push_str(&format!("REMOVED  {p}\n"));
62    }
63    for (p, sig) in &diff.changed {
64        let what = if *sig { "signature" } else { "body" };
65        out.push_str(&format!("CHANGED  {p} ({what})\n"));
66    }
67    out
68}