1use crate::{Allowance, FORMAT, Kind, Side};
8use std::fmt::Write as _;
9
10#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct Report {
13 pub old: Side,
14 pub new: Side,
15 pub removed: Vec<Removed>,
18 pub renamed: Vec<Renamed>,
21 pub added: Vec<Added>,
24 pub unused: Vec<Unused>,
26}
27
28#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
29pub struct Removed {
30 pub kind: Kind,
31 pub id: String,
32 pub allowed: bool,
34}
35
36#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
37pub struct Renamed {
38 pub kind: Kind,
39 pub from: String,
40 pub to: String,
41 pub basis: Basis,
42 pub allowed: bool,
44}
45
46#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
47pub struct Added {
48 pub kind: Kind,
49 pub id: String,
50}
51
52#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
54pub enum Basis {
55 Declared,
57 Content,
60}
61
62impl Basis {
63 #[must_use]
64 pub const fn as_str(self) -> &'static str {
65 match self {
66 Self::Declared => "declared",
67 Self::Content => "content",
68 }
69 }
70}
71
72#[derive(Clone, Debug, Eq, PartialEq)]
74pub struct Unused {
75 pub allowance: Allowance,
76 pub reason: Reason,
77}
78
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
80pub enum Reason {
81 NotRemoved,
83 TargetMissing,
86}
87
88impl Reason {
89 #[must_use]
90 pub const fn as_str(self) -> &'static str {
91 match self {
92 Self::NotRemoved => "not_removed",
93 Self::TargetMissing => "target_missing",
94 }
95 }
96}
97
98impl Report {
99 #[must_use]
102 pub fn breaking(&self) -> bool {
103 self.removed.iter().any(|removed| !removed.allowed)
104 || self.renamed.iter().any(|renamed| !renamed.allowed)
105 }
106
107 #[must_use]
109 pub fn not_allowed(&self) -> usize {
110 self.removed
111 .iter()
112 .filter(|removed| !removed.allowed)
113 .count()
114 + self
115 .renamed
116 .iter()
117 .filter(|renamed| !renamed.allowed)
118 .count()
119 }
120
121 #[must_use]
125 pub fn to_json(&self) -> String {
126 let mut output = String::from("{\"format\":");
127 json_string(FORMAT, &mut output);
128 output.push_str(",\"old\":");
129 side_json(&self.old, &mut output);
130 output.push_str(",\"new\":");
131 side_json(&self.new, &mut output);
132 let _ = write!(output, ",\"breaking\":{}", self.breaking());
133 output.push_str(",\"removed\":");
134 list(&mut output, &self.removed, |removed, output| {
135 entry(output, removed.kind, &[("id", &removed.id)]);
136 let _ = write!(output, ",\"allowed\":{}}}", removed.allowed);
137 });
138 output.push_str(",\"renamed\":");
139 list(&mut output, &self.renamed, |renamed, output| {
140 entry(
141 output,
142 renamed.kind,
143 &[
144 ("from", &renamed.from),
145 ("to", &renamed.to),
146 ("basis", renamed.basis.as_str()),
147 ],
148 );
149 let _ = write!(output, ",\"allowed\":{}}}", renamed.allowed);
150 });
151 output.push_str(",\"added\":");
152 list(&mut output, &self.added, |added, output| {
153 entry(output, added.kind, &[("id", &added.id)]);
154 output.push('}');
155 });
156 output.push_str(",\"unused\":");
157 list(&mut output, &self.unused, |unused, output| {
158 output.push_str("{\"allowance\":");
159 match &unused.allowance {
160 Allowance::Remove { kind, id } => {
161 json_string("remove", output);
162 output.push(',');
163 entry_members(output, *kind, &[("id", id)]);
164 }
165 Allowance::Rename { kind, from, to } => {
166 json_string("rename", output);
167 output.push(',');
168 entry_members(output, *kind, &[("from", from), ("to", to)]);
169 }
170 }
171 output.push_str(",\"reason\":");
172 json_string(unused.reason.as_str(), output);
173 output.push('}');
174 });
175 output.push_str("}\n");
176 output
177 }
178
179 #[must_use]
183 pub fn to_text(&self) -> String {
184 let allowed = |allowed: bool| if allowed { " (allowed)" } else { "" };
185 let mut output = String::new();
186 for removed in &self.removed {
187 let _ = writeln!(
188 output,
189 "removed {} {}{}",
190 removed.kind,
191 removed.id,
192 allowed(removed.allowed)
193 );
194 }
195 for renamed in &self.renamed {
196 let basis = match renamed.basis {
197 crate::Basis::Declared => "declared",
198 crate::Basis::Content => "same content",
199 };
200 let _ = writeln!(
201 output,
202 "renamed {} {} -> {} ({basis}{})",
203 renamed.kind,
204 renamed.from,
205 renamed.to,
206 if renamed.allowed { ", allowed" } else { "" }
207 );
208 }
209 for added in &self.added {
210 let _ = writeln!(output, "added {} {}", added.kind, added.id);
211 }
212 for unused in &self.unused {
213 let reason = match unused.reason {
214 Reason::NotRemoved => "not removed",
215 Reason::TargetMissing => "target missing",
216 };
217 let _ = match &unused.allowance {
218 Allowance::Remove { kind, id } => {
219 writeln!(output, "unused allowance: remove {kind} {id} ({reason})")
220 }
221 Allowance::Rename { kind, from, to } => writeln!(
222 output,
223 "unused allowance: rename {kind} {from} -> {to} ({reason})"
224 ),
225 };
226 }
227 let _ = writeln!(
228 output,
229 "summary: {} removed, {} renamed, {} added, {} not allowed",
230 self.removed.len(),
231 self.renamed.len(),
232 self.added.len(),
233 self.not_allowed()
234 );
235 output
236 }
237}
238
239fn list<T>(output: &mut String, items: &[T], mut write: impl FnMut(&T, &mut String)) {
241 output.push('[');
242 for (index, item) in items.iter().enumerate() {
243 if index > 0 {
244 output.push(',');
245 }
246 write(item, output);
247 }
248 output.push(']');
249}
250
251fn entry(output: &mut String, kind: Kind, members: &[(&str, &str)]) {
253 output.push('{');
254 entry_members(output, kind, members);
255}
256
257fn entry_members(output: &mut String, kind: Kind, members: &[(&str, &str)]) {
258 output.push_str("\"kind\":");
259 json_string(kind.as_str(), output);
260 for (key, value) in members {
261 output.push(',');
262 json_string(key, output);
263 output.push(':');
264 json_string(value, output);
265 }
266}
267
268fn side_json(side: &Side, output: &mut String) {
269 output.push_str("{\"input\":");
270 json_string(side.input.as_str(), output);
271 output.push_str(",\"profile\":");
272 json_string(&side.profile, output);
273 output.push_str(",\"profile_version\":");
274 json_string(&side.profile_version, output);
275 output.push_str(",\"source_digest\":");
276 json_string(&side.source_digest, output);
277 output.push('}');
278}
279
280fn json_string(text: &str, output: &mut String) {
283 output.push('"');
284 for character in text.chars() {
285 match character {
286 '"' => output.push_str("\\\""),
287 '\\' => output.push_str("\\\\"),
288 character if character.is_control() => {
289 let _ = write!(output, "\\u{:04x}", u32::from(character));
290 }
291 character => output.push(character),
292 }
293 }
294 output.push('"');
295}