Skip to main content

atelier_sdk_diff/
binary.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::model::{Address, Delta, DeltaKind, Diff, Fidelity};
4
5/// The binary rung: diff two file listings by content id alone.
6///
7/// Each listing maps a workspace-relative path to the content id of the bytes
8/// at that path. A path present on one side only is `Added`/`Removed`; a path
9/// on both sides with a different id is `Changed`; an unchanged id yields no
10/// delta. Deltas come back sorted by path, so the same inputs always produce
11/// the same diff. `Moved` is never produced here — it needs the engine's
12/// rename detection, not a content-id comparison.
13#[must_use]
14pub fn diff_listings(before: &BTreeMap<String, String>, after: &BTreeMap<String, String>) -> Diff {
15    let paths: BTreeSet<&String> = before.keys().chain(after.keys()).collect();
16
17    let mut deltas = Vec::new();
18    for path in paths {
19        let delta = match (before.get(path), after.get(path)) {
20            (Some(old), None) => Some(binary_delta(path, DeltaKind::Removed, Some(old), None)),
21            (None, Some(new)) => Some(binary_delta(path, DeltaKind::Added, None, Some(new))),
22            (Some(old), Some(new)) if old != new => {
23                Some(binary_delta(path, DeltaKind::Changed, Some(old), Some(new)))
24            }
25            _ => None,
26        };
27        if let Some(delta) = delta {
28            deltas.push(delta);
29        }
30    }
31
32    Diff { deltas }
33}
34
35fn binary_delta(
36    path: &str,
37    kind: DeltaKind,
38    before: Option<&String>,
39    after: Option<&String>,
40) -> Delta {
41    Delta {
42        address: Address::new(path),
43        kind,
44        fidelity: Fidelity::Binary,
45        before: before.cloned(),
46        after: after.cloned(),
47        lines: Vec::new(),
48        package: None,
49        summary: None,
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    fn listing(entries: &[(&str, &str)]) -> BTreeMap<String, String> {
58        entries
59            .iter()
60            .map(|(path, id)| ((*path).to_owned(), (*id).to_owned()))
61            .collect()
62    }
63
64    #[test]
65    fn added_path_yields_added_delta() {
66        let before = listing(&[]);
67        let after = listing(&[("a.txt", "id1")]);
68
69        let diff = diff_listings(&before, &after);
70
71        assert_eq!(diff.deltas.len(), 1);
72        let delta = &diff.deltas[0];
73        assert_eq!(delta.address, Address::new("a.txt"));
74        assert_eq!(delta.kind, DeltaKind::Added);
75        assert_eq!(delta.fidelity, Fidelity::Binary);
76        assert_eq!(delta.before, None);
77        assert_eq!(delta.after, Some("id1".to_owned()));
78        assert!(delta.lines.is_empty());
79    }
80
81    #[test]
82    fn removed_path_yields_removed_delta() {
83        let before = listing(&[("a.txt", "id1")]);
84        let after = listing(&[]);
85
86        let diff = diff_listings(&before, &after);
87
88        assert_eq!(diff.deltas.len(), 1);
89        let delta = &diff.deltas[0];
90        assert_eq!(delta.kind, DeltaKind::Removed);
91        assert_eq!(delta.fidelity, Fidelity::Binary);
92        assert_eq!(delta.before, Some("id1".to_owned()));
93        assert_eq!(delta.after, None);
94    }
95
96    #[test]
97    fn changed_id_yields_changed_delta() {
98        let before = listing(&[("a.txt", "id1")]);
99        let after = listing(&[("a.txt", "id2")]);
100
101        let diff = diff_listings(&before, &after);
102
103        assert_eq!(diff.deltas.len(), 1);
104        let delta = &diff.deltas[0];
105        assert_eq!(delta.kind, DeltaKind::Changed);
106        assert_eq!(delta.fidelity, Fidelity::Binary);
107        assert_eq!(delta.before, Some("id1".to_owned()));
108        assert_eq!(delta.after, Some("id2".to_owned()));
109    }
110
111    #[test]
112    fn mixed_changes_come_back_sorted_by_path() {
113        let before = listing(&[("gone.txt", "g1"), ("keep.txt", "k1"), ("edit.txt", "e1")]);
114        let after = listing(&[("keep.txt", "k1"), ("edit.txt", "e2"), ("new.txt", "n1")]);
115
116        let diff = diff_listings(&before, &after);
117
118        let observed: Vec<(&str, DeltaKind)> = diff
119            .deltas
120            .iter()
121            .map(|d| (d.address.as_str(), d.kind))
122            .collect();
123        assert_eq!(
124            observed,
125            vec![
126                ("edit.txt", DeltaKind::Changed),
127                ("gone.txt", DeltaKind::Removed),
128                ("new.txt", DeltaKind::Added),
129            ]
130        );
131    }
132
133    #[test]
134    fn identical_listings_yield_no_deltas() {
135        let before = listing(&[("a.txt", "id1"), ("b.txt", "id2")]);
136        let after = listing(&[("a.txt", "id1"), ("b.txt", "id2")]);
137
138        let diff = diff_listings(&before, &after);
139
140        assert!(diff.deltas.is_empty());
141    }
142
143    #[test]
144    fn both_empty_yields_no_deltas() {
145        let diff = diff_listings(&listing(&[]), &listing(&[]));
146
147        assert!(diff.deltas.is_empty());
148    }
149}