braze_sync/format/
table.rs1use crate::diff::catalog::{CatalogItemsDiff, CatalogSchemaDiff};
8use crate::diff::content_block::ContentBlockDiff;
9use crate::diff::custom_attribute::{CustomAttributeDiff, CustomAttributeOp};
10use crate::diff::email_template::EmailTemplateDiff;
11use crate::diff::{DiffOp, DiffSummary, ResourceDiff};
12use crate::resource::{CatalogField, ResourceKind};
13use std::fmt::Write as _;
14
15pub fn render(summary: &DiffSummary) -> String {
16 let mut out = String::new();
17
18 for diff in &summary.diffs {
19 render_one(&mut out, diff);
20 out.push('\n');
21 }
22
23 let _ = writeln!(
24 out,
25 "Summary: {} changed, {} in sync, {} orphan, {} destructive",
26 summary.changed_count(),
27 summary.in_sync_count(),
28 summary.orphan_count(),
29 summary.destructive_count(),
30 );
31
32 out
33}
34
35fn render_one(out: &mut String, diff: &ResourceDiff) {
36 let unchanged = !diff.has_changes();
37 let icon = if unchanged {
38 "โ
"
39 } else {
40 kind_icon(diff.kind())
41 };
42 let label = kind_label(diff.kind());
43 let _ = writeln!(out, "{icon} {label}: {}", diff.name());
44
45 if unchanged {
46 out.push_str(" no drift\n");
47 if let ResourceDiff::CustomAttribute(d) = diff {
50 render_custom_attribute(out, d);
51 }
52 return;
53 }
54
55 match diff {
56 ResourceDiff::CatalogSchema(d) => render_catalog_schema(out, d),
57 ResourceDiff::CatalogItems(d) => render_catalog_items(out, d),
58 ResourceDiff::ContentBlock(d) => render_content_block(out, d),
59 ResourceDiff::EmailTemplate(d) => render_email_template(out, d),
60 ResourceDiff::CustomAttribute(d) => render_custom_attribute(out, d),
61 }
62}
63
64fn kind_icon(kind: ResourceKind) -> &'static str {
65 match kind {
66 ResourceKind::CatalogSchema => "๐",
67 ResourceKind::CatalogItems => "๐ฆ",
68 ResourceKind::ContentBlock => "๐",
69 ResourceKind::EmailTemplate => "๐ง",
70 ResourceKind::CustomAttribute => "๐ท ",
71 }
72}
73
74fn kind_label(kind: ResourceKind) -> &'static str {
75 match kind {
76 ResourceKind::CatalogSchema => "Catalog Schema",
77 ResourceKind::CatalogItems => "Catalog Items",
78 ResourceKind::ContentBlock => "Content Block",
79 ResourceKind::EmailTemplate => "Email Template",
80 ResourceKind::CustomAttribute => "Custom Attribute",
81 }
82}
83
84fn fmt_field(f: &CatalogField) -> String {
85 format!("{} ({})", f.name, f.field_type.as_str())
86}
87
88fn render_catalog_schema(out: &mut String, d: &CatalogSchemaDiff) {
89 if matches!(d.op, DiffOp::Added(_)) {
90 out.push_str(" + new catalog\n");
91 } else if matches!(d.op, DiffOp::Removed(_)) {
92 out.push_str(" - removed catalog (destructive)\n");
93 }
94 for fd in &d.field_diffs {
95 match fd {
96 DiffOp::Added(f) => {
97 let _ = writeln!(out, " + field: {}", fmt_field(f));
98 }
99 DiffOp::Removed(f) => {
100 let _ = writeln!(out, " - field: {}", fmt_field(f));
101 }
102 DiffOp::Modified { from, to } => {
103 let _ = writeln!(
104 out,
105 " ~ field: {} ({} โ {})",
106 to.name,
107 from.field_type.as_str(),
108 to.field_type.as_str(),
109 );
110 }
111 DiffOp::Unchanged => {}
112 }
113 }
114}
115
116fn render_catalog_items(out: &mut String, d: &CatalogItemsDiff) {
117 let total = d.added_ids.len() + d.modified_ids.len() + d.removed_ids.len() + d.unchanged_count;
118 let _ = writeln!(
119 out,
120 " + {} added, ~ {} modified, - {} removed (in {} total)",
121 d.added_ids.len(),
122 d.modified_ids.len(),
123 d.removed_ids.len(),
124 total,
125 );
126}
127
128fn render_content_block(out: &mut String, d: &ContentBlockDiff) {
129 if d.orphan {
130 out.push_str(" โ orphaned (exists in Braze, not in Git)\n");
131 return;
132 }
133 match &d.op {
134 DiffOp::Added(_) => out.push_str(" + new content block\n"),
135 DiffOp::Removed(_) => out.push_str(" - removed content block\n"),
136 DiffOp::Modified { .. } => {
137 if let Some(td) = &d.text_diff {
138 let _ = writeln!(
139 out,
140 " ~ content changed (+{} -{})",
141 td.additions, td.deletions,
142 );
143 } else {
144 out.push_str(" ~ content changed\n");
145 }
146 }
147 DiffOp::Unchanged => {}
148 }
149}
150
151fn render_email_template(out: &mut String, d: &EmailTemplateDiff) {
152 if d.orphan {
153 out.push_str(" โ orphaned (exists in Braze, not in Git)\n");
154 return;
155 }
156 if matches!(d.op, DiffOp::Added(_)) {
157 out.push_str(" + new email template\n");
158 } else if matches!(d.op, DiffOp::Removed(_)) {
159 out.push_str(" - removed email template\n");
160 }
161 if d.subject_changed {
162 out.push_str(" ~ subject changed\n");
163 }
164 if let Some(td) = &d.body_html_diff {
165 let _ = writeln!(
166 out,
167 " ~ body_html changed (+{} -{})",
168 td.additions, td.deletions
169 );
170 }
171 if let Some(td) = &d.body_plaintext_diff {
172 let _ = writeln!(
173 out,
174 " ~ body_plaintext changed (+{} -{})",
175 td.additions, td.deletions
176 );
177 }
178 if d.metadata_changed {
179 out.push_str(" ~ metadata changed\n");
180 }
181}
182
183fn render_custom_attribute(out: &mut String, d: &CustomAttributeDiff) {
184 match &d.op {
185 CustomAttributeOp::DeprecationToggled { from, to } => {
186 let _ = writeln!(out, " ~ deprecated: {from} โ {to}");
187 }
188 CustomAttributeOp::UnregisteredInGit => {
189 out.push_str(" โ exists in Braze but not in Git registry (run export)\n");
190 }
191 CustomAttributeOp::PresentInGitOnly => {
192 out.push_str(" โ in Git registry but not in Braze (likely a typo)\n");
193 }
194 CustomAttributeOp::MetadataOnly => {
195 out.push_str(" ~ metadata-only change (no API to apply)\n");
196 }
197 CustomAttributeOp::Unchanged => {}
198 }
199 for hint in &d.hints {
200 let _ = writeln!(out, " โน {hint}");
201 }
202}