1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::model::{Address, Delta, DeltaKind, Diff, Fidelity};
4
5#[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}