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 return;
48 }
49
50 match diff {
51 ResourceDiff::CatalogSchema(d) => render_catalog_schema(out, d),
52 ResourceDiff::CatalogItems(d) => render_catalog_items(out, d),
53 ResourceDiff::ContentBlock(d) => render_content_block(out, d),
54 ResourceDiff::EmailTemplate(d) => render_email_template(out, d),
55 ResourceDiff::CustomAttribute(d) => render_custom_attribute(out, d),
56 }
57}
58
59fn kind_icon(kind: ResourceKind) -> &'static str {
60 match kind {
61 ResourceKind::CatalogSchema => "๐",
62 ResourceKind::CatalogItems => "๐ฆ",
63 ResourceKind::ContentBlock => "๐",
64 ResourceKind::EmailTemplate => "๐ง",
65 ResourceKind::CustomAttribute => "๐ท ",
66 }
67}
68
69fn kind_label(kind: ResourceKind) -> &'static str {
70 match kind {
71 ResourceKind::CatalogSchema => "Catalog Schema",
72 ResourceKind::CatalogItems => "Catalog Items",
73 ResourceKind::ContentBlock => "Content Block",
74 ResourceKind::EmailTemplate => "Email Template",
75 ResourceKind::CustomAttribute => "Custom Attribute",
76 }
77}
78
79fn fmt_field(f: &CatalogField) -> String {
80 format!("{} ({})", f.name, f.field_type.as_str())
81}
82
83fn render_catalog_schema(out: &mut String, d: &CatalogSchemaDiff) {
84 if matches!(d.op, DiffOp::Added(_)) {
85 out.push_str(" + new catalog\n");
86 } else if matches!(d.op, DiffOp::Removed(_)) {
87 out.push_str(" - removed catalog (destructive)\n");
88 }
89 for fd in &d.field_diffs {
90 match fd {
91 DiffOp::Added(f) => {
92 let _ = writeln!(out, " + field: {}", fmt_field(f));
93 }
94 DiffOp::Removed(f) => {
95 let _ = writeln!(out, " - field: {}", fmt_field(f));
96 }
97 DiffOp::Modified { from, to } => {
98 let _ = writeln!(
99 out,
100 " ~ field: {} ({} โ {})",
101 to.name,
102 from.field_type.as_str(),
103 to.field_type.as_str(),
104 );
105 }
106 DiffOp::Unchanged => {}
107 }
108 }
109}
110
111fn render_catalog_items(out: &mut String, d: &CatalogItemsDiff) {
112 let total = d.added_ids.len() + d.modified_ids.len() + d.removed_ids.len() + d.unchanged_count;
113 let _ = writeln!(
114 out,
115 " + {} added, ~ {} modified, - {} removed (in {} total)",
116 d.added_ids.len(),
117 d.modified_ids.len(),
118 d.removed_ids.len(),
119 total,
120 );
121}
122
123fn render_content_block(out: &mut String, d: &ContentBlockDiff) {
124 if d.orphan {
125 out.push_str(" โ orphaned (exists in Braze, not in Git)\n");
126 return;
127 }
128 match &d.op {
129 DiffOp::Added(_) => out.push_str(" + new content block\n"),
130 DiffOp::Removed(_) => out.push_str(" - removed content block\n"),
131 DiffOp::Modified { .. } => {
132 if let Some(td) = &d.text_diff {
133 let _ = writeln!(
134 out,
135 " ~ content changed (+{} -{})",
136 td.additions, td.deletions,
137 );
138 } else {
139 out.push_str(" ~ content changed\n");
140 }
141 }
142 DiffOp::Unchanged => {}
143 }
144}
145
146fn render_email_template(out: &mut String, d: &EmailTemplateDiff) {
147 if d.orphan {
148 out.push_str(" โ orphaned (exists in Braze, not in Git)\n");
149 return;
150 }
151 if matches!(d.op, DiffOp::Added(_)) {
152 out.push_str(" + new email template\n");
153 } else if matches!(d.op, DiffOp::Removed(_)) {
154 out.push_str(" - removed email template\n");
155 }
156 if d.subject_changed {
157 out.push_str(" ~ subject changed\n");
158 }
159 if let Some(td) = &d.body_html_diff {
160 let _ = writeln!(
161 out,
162 " ~ body_html changed (+{} -{})",
163 td.additions, td.deletions
164 );
165 }
166 if let Some(td) = &d.body_plaintext_diff {
167 let _ = writeln!(
168 out,
169 " ~ body_plaintext changed (+{} -{})",
170 td.additions, td.deletions
171 );
172 }
173 if d.metadata_changed {
174 out.push_str(" ~ metadata changed\n");
175 }
176}
177
178fn render_custom_attribute(out: &mut String, d: &CustomAttributeDiff) {
179 match &d.op {
180 CustomAttributeOp::DeprecationToggled { from, to } => {
181 let _ = writeln!(out, " ~ deprecated: {from} โ {to}");
182 }
183 CustomAttributeOp::UnregisteredInGit => {
184 out.push_str(" โ exists in Braze but not in Git registry (run export)\n");
185 }
186 CustomAttributeOp::PresentInGitOnly => {
187 out.push_str(" โ in Git registry but not in Braze (likely a typo)\n");
188 }
189 CustomAttributeOp::MetadataOnly => {
190 out.push_str(" ~ metadata-only change (no API to apply)\n");
191 }
192 CustomAttributeOp::Unchanged => {}
193 }
194}