1use crate::diff::catalog::CatalogSchemaDiff;
15use crate::diff::content_block::ContentBlockDiff;
16use crate::diff::custom_attribute::{CustomAttributeDiff, CustomAttributeOp};
17use crate::diff::email_template::EmailTemplateDiff;
18use crate::diff::tag::{TagDiff, TagOp};
19use crate::diff::TextDiffSummary;
20use crate::diff::{DiffOp, DiffSummary, ResourceDiff};
21use crate::resource::CatalogField;
22use serde::Serialize;
23
24pub fn render(summary: &DiffSummary) -> String {
25 let root = JsonRoot::from(summary);
26 let mut s = serde_json::to_string_pretty(&root).expect("internal wire types serialize cleanly");
27 s.push('\n');
32 s
33}
34
35#[derive(Serialize)]
40struct JsonRoot {
41 version: u32,
42 summary: JsonSummary,
43 diffs: Vec<JsonDiffEntry>,
44}
45
46#[derive(Serialize)]
47struct JsonSummary {
48 changed: usize,
49 in_sync: usize,
50 destructive: usize,
51 orphan: usize,
52}
53
54#[derive(Serialize)]
55#[serde(tag = "kind", rename_all = "snake_case")]
56enum JsonDiffEntry {
57 CatalogSchema {
58 name: String,
59 op: JsonOp,
60 field_diffs: Vec<JsonFieldDiff>,
61 },
62 ContentBlock {
63 name: String,
64 op: JsonOp,
65 orphan: bool,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 text_diff: Option<JsonTextDiff>,
68 },
69 EmailTemplate {
70 name: String,
71 op: JsonOp,
72 subject_changed: bool,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 body_html_diff: Option<JsonTextDiff>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 body_plaintext_diff: Option<JsonTextDiff>,
77 metadata_changed: bool,
78 orphan: bool,
79 },
80 CustomAttribute {
81 name: String,
82 #[serde(flatten)]
83 change: JsonCustomAttributeChange,
84 #[serde(skip_serializing_if = "Vec::is_empty")]
85 hints: Vec<String>,
86 },
87 Tag {
88 name: String,
89 #[serde(flatten)]
90 change: JsonTagChange,
91 #[serde(skip_serializing_if = "Vec::is_empty")]
92 hints: Vec<String>,
93 },
94}
95
96#[derive(Serialize)]
97#[serde(rename_all = "snake_case")]
98enum JsonOp {
99 Added,
100 Removed,
101 Modified,
102 Unchanged,
103}
104
105#[derive(Serialize)]
106struct JsonField {
107 name: String,
108 #[serde(rename = "type")]
109 field_type: &'static str,
110}
111
112#[derive(Serialize)]
113#[serde(tag = "op", rename_all = "snake_case")]
114enum JsonFieldDiff {
115 Added { field: JsonField },
116 Removed { field: JsonField },
117 Modified { from: JsonField, to: JsonField },
118}
119
120#[derive(Serialize)]
121struct JsonTextDiff {
122 additions: usize,
123 deletions: usize,
124}
125
126#[derive(Serialize)]
127#[serde(tag = "op", rename_all = "snake_case")]
128enum JsonCustomAttributeChange {
129 DeprecationToggled { from: bool, to: bool },
130 UnregisteredInGit,
131 PresentInGitOnly,
132 MetadataOnly,
133 Unchanged,
134}
135
136#[derive(Serialize)]
137#[serde(tag = "op", rename_all = "snake_case")]
138enum JsonTagChange {
139 ReferencedButUnregistered,
140 RegisteredButUnreferenced,
141 Unchanged,
142}
143
144impl From<&DiffSummary> for JsonRoot {
149 fn from(s: &DiffSummary) -> Self {
150 Self {
151 version: 1,
152 summary: JsonSummary {
153 changed: s.changed_count(),
154 in_sync: s.in_sync_count(),
155 destructive: s.destructive_count(),
156 orphan: s.orphan_count(),
157 },
158 diffs: s.diffs.iter().map(JsonDiffEntry::from).collect(),
159 }
160 }
161}
162
163impl From<&ResourceDiff> for JsonDiffEntry {
164 fn from(d: &ResourceDiff) -> Self {
165 match d {
166 ResourceDiff::CatalogSchema(c) => Self::from_catalog_schema(c),
167 ResourceDiff::ContentBlock(c) => Self::from_content_block(c),
168 ResourceDiff::EmailTemplate(c) => Self::from_email_template(c),
169 ResourceDiff::CustomAttribute(c) => Self::from_custom_attribute(c),
170 ResourceDiff::Tag(c) => Self::from_tag(c),
171 }
172 }
173}
174
175impl JsonDiffEntry {
176 fn from_catalog_schema(c: &CatalogSchemaDiff) -> Self {
177 Self::CatalogSchema {
178 name: c.name.clone(),
179 op: top_op(&c.op),
180 field_diffs: c.field_diffs.iter().filter_map(json_field_diff).collect(),
181 }
182 }
183
184 fn from_content_block(c: &ContentBlockDiff) -> Self {
185 Self::ContentBlock {
186 name: c.name.clone(),
187 op: top_op(&c.op),
188 orphan: c.orphan,
189 text_diff: c.text_diff.as_ref().map(json_text_diff),
190 }
191 }
192
193 fn from_email_template(c: &EmailTemplateDiff) -> Self {
194 Self::EmailTemplate {
195 name: c.name.clone(),
196 op: top_op(&c.op),
197 subject_changed: c.subject_changed,
198 body_html_diff: c.body_html_diff.as_ref().map(json_text_diff),
199 body_plaintext_diff: c.body_plaintext_diff.as_ref().map(json_text_diff),
200 metadata_changed: c.metadata_changed,
201 orphan: c.orphan,
202 }
203 }
204
205 fn from_custom_attribute(c: &CustomAttributeDiff) -> Self {
206 Self::CustomAttribute {
207 name: c.name.clone(),
208 change: json_custom_attribute_change(&c.op),
209 hints: c.hints.clone(),
210 }
211 }
212
213 fn from_tag(c: &TagDiff) -> Self {
214 Self::Tag {
215 name: c.name.clone(),
216 change: json_tag_change(&c.op),
217 hints: c.hints.clone(),
218 }
219 }
220}
221
222fn json_tag_change(op: &TagOp) -> JsonTagChange {
223 match op {
224 TagOp::ReferencedButUnregistered => JsonTagChange::ReferencedButUnregistered,
225 TagOp::RegisteredButUnreferenced => JsonTagChange::RegisteredButUnreferenced,
226 TagOp::Unchanged => JsonTagChange::Unchanged,
227 }
228}
229
230fn top_op<T>(op: &DiffOp<T>) -> JsonOp {
231 match op {
232 DiffOp::Added(_) => JsonOp::Added,
233 DiffOp::Removed(_) => JsonOp::Removed,
234 DiffOp::Modified { .. } => JsonOp::Modified,
235 DiffOp::Unchanged => JsonOp::Unchanged,
236 }
237}
238
239fn json_field(f: &CatalogField) -> JsonField {
240 JsonField {
241 name: f.name.clone(),
242 field_type: f.field_type.as_str(),
243 }
244}
245
246fn json_field_diff(d: &DiffOp<CatalogField>) -> Option<JsonFieldDiff> {
247 Some(match d {
248 DiffOp::Added(f) => JsonFieldDiff::Added {
249 field: json_field(f),
250 },
251 DiffOp::Removed(f) => JsonFieldDiff::Removed {
252 field: json_field(f),
253 },
254 DiffOp::Modified { from, to } => JsonFieldDiff::Modified {
255 from: json_field(from),
256 to: json_field(to),
257 },
258 DiffOp::Unchanged => return None,
259 })
260}
261
262fn json_text_diff(t: &TextDiffSummary) -> JsonTextDiff {
263 JsonTextDiff {
264 additions: t.additions,
265 deletions: t.deletions,
266 }
267}
268
269fn json_custom_attribute_change(op: &CustomAttributeOp) -> JsonCustomAttributeChange {
270 match op {
271 CustomAttributeOp::DeprecationToggled { from, to } => {
272 JsonCustomAttributeChange::DeprecationToggled {
273 from: *from,
274 to: *to,
275 }
276 }
277 CustomAttributeOp::UnregisteredInGit => JsonCustomAttributeChange::UnregisteredInGit,
278 CustomAttributeOp::PresentInGitOnly => JsonCustomAttributeChange::PresentInGitOnly,
279 CustomAttributeOp::MetadataOnly => JsonCustomAttributeChange::MetadataOnly,
280 CustomAttributeOp::Unchanged => JsonCustomAttributeChange::Unchanged,
281 }
282}
283
284#[cfg(test)]
285mod parses_back_tests {
286 use super::render;
289 use crate::diff::DiffSummary;
290
291 #[test]
292 fn empty_summary_renders_valid_json_with_version_1() {
293 let s = render(&DiffSummary::default());
294 let v: serde_json::Value = serde_json::from_str(&s).unwrap();
295 assert_eq!(v["version"], serde_json::json!(1));
296 assert!(v["diffs"].as_array().unwrap().is_empty());
297 }
298}