1use padlock_core::ir::{optimal_order, StructLayout};
4use similar::{ChangeTag, TextDiff};
5
6pub fn render_diff(layout: &StructLayout) -> String {
8 let original = fields_to_text(layout.fields.iter().map(|f| f.name.as_str()));
9 let optimal = optimal_order(layout);
10 let optimized = fields_to_text(optimal.iter().map(|f| f.name.as_str()));
11 text_diff(&original, &optimized)
12}
13
14pub fn text_diff(original: &str, updated: &str) -> String {
16 if original == updated {
17 return String::from("(no changes)\n");
18 }
19
20 let diff = TextDiff::from_lines(original, updated);
21 let mut out = String::new();
22
23 for change in diff.iter_all_changes() {
24 let prefix = match change.tag() {
25 ChangeTag::Delete => "-",
26 ChangeTag::Insert => "+",
27 ChangeTag::Equal => " ",
28 };
29 out.push_str(&format!("{prefix} {}", change.value()));
30 if !change.value().ends_with('\n') {
31 out.push('\n');
32 }
33 }
34 out
35}
36
37fn fields_to_text<'a>(names: impl Iterator<Item = &'a str>) -> String {
38 names.map(|n| format!("{n}\n")).collect()
39}
40
41#[cfg(test)]
44mod tests {
45 use super::*;
46 use padlock_core::ir::test_fixtures::{connection_layout, packed_layout};
47
48 #[test]
49 fn diff_misaligned_is_nonempty() {
50 let out = render_diff(&connection_layout());
51 assert_ne!(out, "(no changes)\n");
53 assert!(out.contains("timeout") || out.contains("port"));
54 }
55
56 #[test]
57 fn diff_already_optimal_is_no_changes() {
58 let out = render_diff(&packed_layout());
60 assert_eq!(out, "(no changes)\n");
61 }
62
63 #[test]
64 fn text_diff_shows_plus_minus() {
65 let out = text_diff("a\nb\n", "a\nc\n");
66 assert!(out.contains("- b") || out.contains("-b"));
67 assert!(out.contains("+ c") || out.contains("+c"));
68 }
69
70 #[test]
71 fn text_diff_identical_is_no_changes() {
72 assert_eq!(text_diff("x\n", "x\n"), "(no changes)\n");
73 }
74}