braze_sync/format/
table.rs1use crate::diff::catalog::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::ContentBlock(d) => render_content_block(out, d),
58 ResourceDiff::EmailTemplate(d) => render_email_template(out, d),
59 ResourceDiff::CustomAttribute(d) => render_custom_attribute(out, d),
60 ResourceDiff::Tag(d) => render_tag(out, d),
61 }
62}
63
64fn kind_icon(kind: ResourceKind) -> &'static str {
65 match kind {
66 ResourceKind::CatalogSchema => "๐",
67 ResourceKind::ContentBlock => "๐",
68 ResourceKind::EmailTemplate => "๐ง",
69 ResourceKind::CustomAttribute => "๐ท ",
70 ResourceKind::Tag => "๐",
71 }
72}
73
74fn kind_label(kind: ResourceKind) -> &'static str {
75 match kind {
76 ResourceKind::CatalogSchema => "Catalog Schema",
77 ResourceKind::ContentBlock => "Content Block",
78 ResourceKind::EmailTemplate => "Email Template",
79 ResourceKind::CustomAttribute => "Custom Attribute",
80 ResourceKind::Tag => "Tag",
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_content_block(out: &mut String, d: &ContentBlockDiff) {
117 if d.orphan {
118 out.push_str(" โ orphaned (exists in Braze, not in Git)\n");
119 return;
120 }
121 match &d.op {
122 DiffOp::Added(_) => out.push_str(" + new content block\n"),
123 DiffOp::Removed(_) => out.push_str(" - removed content block\n"),
124 DiffOp::Modified { .. } => {
125 if let Some(td) = &d.text_diff {
126 let _ = writeln!(
127 out,
128 " ~ content changed (+{} -{})",
129 td.additions, td.deletions,
130 );
131 } else {
132 out.push_str(" ~ content changed\n");
133 }
134 }
135 DiffOp::Unchanged => {}
136 }
137}
138
139fn render_email_template(out: &mut String, d: &EmailTemplateDiff) {
140 if d.orphan {
141 out.push_str(" โ orphaned (exists in Braze, not in Git)\n");
142 return;
143 }
144 if matches!(d.op, DiffOp::Added(_)) {
145 out.push_str(" + new email template\n");
146 } else if matches!(d.op, DiffOp::Removed(_)) {
147 out.push_str(" - removed email template\n");
148 }
149 if d.subject_changed {
150 out.push_str(" ~ subject changed\n");
151 }
152 if let Some(td) = &d.body_html_diff {
153 let _ = writeln!(
154 out,
155 " ~ body_html changed (+{} -{})",
156 td.additions, td.deletions
157 );
158 }
159 if let Some(td) = &d.body_plaintext_diff {
160 let _ = writeln!(
161 out,
162 " ~ body_plaintext changed (+{} -{})",
163 td.additions, td.deletions
164 );
165 }
166 if d.metadata_changed {
167 out.push_str(" ~ metadata changed\n");
168 }
169}
170
171fn render_custom_attribute(out: &mut String, d: &CustomAttributeDiff) {
172 match &d.op {
173 CustomAttributeOp::DeprecationToggled { from, to } => {
174 let _ = writeln!(out, " ~ deprecated: {from} โ {to}");
175 }
176 CustomAttributeOp::UnregisteredInGit => {
177 out.push_str(" โ exists in Braze but not in Git registry (run export)\n");
178 }
179 CustomAttributeOp::PresentInGitOnly => {
180 out.push_str(" โ in Git registry but not in Braze (likely a typo)\n");
181 }
182 CustomAttributeOp::MetadataOnly => {
183 out.push_str(" ~ metadata-only change (no API to apply)\n");
184 }
185 CustomAttributeOp::Unchanged => {}
186 }
187 for hint in &d.hints {
188 let _ = writeln!(out, " โน {hint}");
189 }
190}
191
192fn render_tag(out: &mut String, d: &crate::diff::tag::TagDiff) {
193 use crate::diff::tag::TagOp;
194 match &d.op {
195 TagOp::ReferencedButUnregistered => {
196 out.push_str(
197 " โ referenced by a resource but not in tags/registry.yaml \
198 (apply will fail until added + created in Braze dashboard)\n",
199 );
200 }
201 TagOp::RegisteredButUnreferenced => {
202 out.push_str(" โน in tags/registry.yaml but no local resource references it\n");
203 }
204 TagOp::Unchanged => {}
205 }
206 for hint in &d.hints {
207 let _ = writeln!(out, " โน {hint}");
208 }
209}