1use std::collections::BTreeSet;
4
5use super::ChangeKind;
6use super::fields::{FieldChange, FieldValueSnapshot, classify_severity};
7
8#[must_use]
22pub(crate) fn diff_relation_field(
23 field_name: &str,
24 old: &[String],
25 new: &[String],
26) -> Vec<FieldChange> {
27 let mut changes = Vec::new();
28
29 if is_ordered_field(field_name) {
30 diff_ordered(field_name, old, new, &mut changes);
31 } else {
32 diff_unordered(field_name, old, new, &mut changes);
33 }
34
35 changes
36}
37
38fn diff_unordered(
40 field_name: &str,
41 old: &[String],
42 new: &[String],
43 changes: &mut Vec<FieldChange>,
44) {
45 let old_set: BTreeSet<&str> = old.iter().map(String::as_str).collect();
46 let new_set: BTreeSet<&str> = new.iter().map(String::as_str).collect();
47
48 for elem in old_set.difference(&new_set) {
50 let severity = classify_severity(field_name, ChangeKind::Removed);
51 changes.push(FieldChange {
52 field: field_name.to_owned(),
53 kind: ChangeKind::Removed,
54 severity,
55 old_value: Some(FieldValueSnapshot::Scalar((*elem).to_owned())),
56 new_value: None,
57 });
58 }
59
60 for elem in new_set.difference(&old_set) {
62 let severity = classify_severity(field_name, ChangeKind::Added);
63 changes.push(FieldChange {
64 field: field_name.to_owned(),
65 kind: ChangeKind::Added,
66 severity,
67 old_value: None,
68 new_value: Some(FieldValueSnapshot::Scalar((*elem).to_owned())),
69 });
70 }
71}
72
73fn diff_ordered(field_name: &str, old: &[String], new: &[String], changes: &mut Vec<FieldChange>) {
78 let old_set: BTreeSet<&str> = old.iter().map(String::as_str).collect();
79 let new_set: BTreeSet<&str> = new.iter().map(String::as_str).collect();
80
81 for elem in old_set.difference(&new_set) {
83 let severity = classify_severity(field_name, ChangeKind::Removed);
84 changes.push(FieldChange {
85 field: field_name.to_owned(),
86 kind: ChangeKind::Removed,
87 severity,
88 old_value: Some(FieldValueSnapshot::Scalar((*elem).to_owned())),
89 new_value: None,
90 });
91 }
92
93 for elem in new_set.difference(&old_set) {
95 let severity = classify_severity(field_name, ChangeKind::Added);
96 changes.push(FieldChange {
97 field: field_name.to_owned(),
98 kind: ChangeKind::Added,
99 severity,
100 old_value: None,
101 new_value: Some(FieldValueSnapshot::Scalar((*elem).to_owned())),
102 });
103 }
104
105 if old_set == new_set && old != new {
107 changes.push(FieldChange {
108 field: field_name.to_owned(),
109 kind: ChangeKind::Modified,
110 severity: super::ChangeSeverity::Minor,
111 old_value: Some(FieldValueSnapshot::List(old.to_vec())),
112 new_value: Some(FieldValueSnapshot::List(new.to_vec())),
113 });
114 }
115}
116
117fn is_ordered_field(field_name: &str) -> bool {
120 matches!(field_name, "steps" | "depends")
121}
122
123#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::diff::ChangeSeverity;
131
132 fn strs(v: &[&str]) -> Vec<String> {
133 v.iter().map(|s| (*s).to_owned()).collect()
134 }
135
136 #[test]
137 fn test_diff_relation_depends_element_removed_returns_breaking() {
138 let old = strs(&["a", "b"]);
139 let new = strs(&["a"]);
140 let changes = diff_relation_field("depends", &old, &new);
141 assert_eq!(changes.len(), 1);
142 assert_eq!(changes[0].kind, ChangeKind::Removed);
143 assert_eq!(changes[0].severity, ChangeSeverity::Breaking);
144 }
145
146 #[test]
147 fn test_diff_relation_depends_element_added_returns_minor() {
148 let old = strs(&["a"]);
149 let new = strs(&["a", "b"]);
150 let changes = diff_relation_field("depends", &old, &new);
151 assert_eq!(changes.len(), 1);
152 assert_eq!(changes[0].kind, ChangeKind::Added);
153 assert_eq!(changes[0].severity, ChangeSeverity::Minor);
154 }
155
156 #[test]
157 fn test_diff_relation_related_to_element_added_returns_info() {
158 let old = strs(&["a"]);
159 let new = strs(&["a", "b"]);
160 let changes = diff_relation_field("related_to", &old, &new);
161 assert_eq!(changes.len(), 1);
162 assert_eq!(changes[0].kind, ChangeKind::Added);
163 assert_eq!(changes[0].severity, ChangeSeverity::Info);
164 }
165
166 #[test]
167 fn test_diff_relation_related_to_element_removed_returns_info() {
168 let old = strs(&["a", "b"]);
169 let new = strs(&["a"]);
170 let changes = diff_relation_field("related_to", &old, &new);
171 assert_eq!(changes.len(), 1);
172 assert_eq!(changes[0].kind, ChangeKind::Removed);
173 assert_eq!(changes[0].severity, ChangeSeverity::Info);
174 }
175
176 #[test]
177 fn test_diff_relation_see_also_reordered_returns_empty() {
178 let old = strs(&["a", "b", "c"]);
179 let new = strs(&["c", "a", "b"]);
180 let changes = diff_relation_field("see_also", &old, &new);
181 assert!(changes.is_empty());
182 }
183
184 #[test]
185 fn test_diff_relation_steps_element_removed_returns_breaking() {
186 let old = strs(&["step1", "step2", "step3"]);
187 let new = strs(&["step1", "step3"]);
188 let changes = diff_relation_field("steps", &old, &new);
189 let removed: Vec<_> = changes
191 .iter()
192 .filter(|c| c.kind == ChangeKind::Removed)
193 .collect();
194 assert!(!removed.is_empty());
195 assert_eq!(removed[0].severity, ChangeSeverity::Breaking);
196 }
197
198 #[test]
199 fn test_diff_relation_tags_reordered_returns_empty() {
200 let old = strs(&["alpha", "beta"]);
201 let new = strs(&["beta", "alpha"]);
202 let changes = diff_relation_field("tags", &old, &new);
203 assert!(changes.is_empty());
204 }
205
206 #[test]
207 fn test_diff_relation_empty_to_empty_returns_empty() {
208 let changes = diff_relation_field("items", &[], &[]);
209 assert!(changes.is_empty());
210 }
211
212 #[test]
213 fn test_diff_relation_empty_to_some_returns_added() {
214 let old: Vec<String> = vec![];
215 let new = strs(&["x", "y"]);
216 let changes = diff_relation_field("items", &old, &new);
217 assert_eq!(changes.len(), 2);
218 assert!(changes.iter().all(|c| c.kind == ChangeKind::Added));
219 }
220
221 #[test]
222 fn test_diff_relation_some_to_empty_returns_removed() {
223 let old = strs(&["x", "y"]);
224 let new: Vec<String> = vec![];
225 let changes = diff_relation_field("items", &old, &new);
226 assert_eq!(changes.len(), 2);
227 assert!(changes.iter().all(|c| c.kind == ChangeKind::Removed));
228 }
229}