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