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