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