1import_stdlib!();
2
3use super::string_util::flanked;
4use crate::{
5 CBOR, CBORCase, Error, TagsStoreOpt, tags_store::TagsStoreTrait, with_tags,
6};
7
8type SummarizerFn =
9 Arc<dyn Fn(CBOR, bool) -> Result<String, Error> + Send + Sync>;
10
11#[derive(Clone, Default)]
12pub struct DiagFormatOpts<'a> {
13 annotate: bool,
14 summarize: bool,
15 flat: bool,
16 tags: TagsStoreOpt<'a>,
17}
18
19impl<'a> DiagFormatOpts<'a> {
20 pub fn annotate(mut self, annotate: bool) -> Self {
22 self.annotate = annotate;
23 self
24 }
25
26 pub fn summarize(mut self, summarize: bool) -> Self {
28 self.summarize = summarize;
29 self.flat = true; self
31 }
32
33 pub fn flat(mut self, flat: bool) -> Self {
35 self.flat = flat;
36 self
37 }
38
39 pub fn tags(mut self, tags: TagsStoreOpt<'a>) -> Self {
41 self.tags = tags;
42 self
43 }
44}
45
46impl CBOR {
48 pub fn diagnostic_opt(&self, opts: &DiagFormatOpts<'_>) -> String {
53 self.diag_item(opts).format(opts)
54 }
55
56 pub fn diagnostic(&self) -> String {
58 self.diagnostic_opt(&DiagFormatOpts::default())
59 }
60
61 pub fn diagnostic_annotated(&self) -> String {
64 self.diagnostic_opt(&DiagFormatOpts::default().annotate(true))
65 }
66
67 pub fn diagnostic_flat(&self) -> String {
68 self.diagnostic_opt(&DiagFormatOpts::default().flat(true))
69 }
70
71 pub fn summary(&self) -> String {
72 self.diagnostic_opt(&DiagFormatOpts::default().summarize(true))
73 }
74}
75
76impl CBOR {
77 fn diag_item(&self, opts: &DiagFormatOpts<'_>) -> DiagItem {
78 match self.as_case() {
79 CBORCase::Unsigned(_)
80 | CBORCase::Negative(_)
81 | CBORCase::ByteString(_)
82 | CBORCase::Text(_)
83 | CBORCase::Simple(_) => DiagItem::Item(format!("{}", self)),
84
85 CBORCase::Array(a) => {
86 let begin = "[".to_string();
87 let end = "]".to_string();
88 let items = a.iter().map(|x| x.diag_item(opts)).collect();
89 let is_pairs = false;
90 let comment = None;
91 DiagItem::Group(begin, end, items, is_pairs, comment)
92 }
93 CBORCase::Map(m) => {
94 let begin = "{".to_string();
95 let end = "}".to_string();
96 let items = m
97 .iter()
98 .flat_map(|(key, value)| {
99 vec![key.diag_item(opts), value.diag_item(opts)]
100 })
101 .collect();
102 let is_pairs = true;
103 let comment = None;
104 DiagItem::Group(begin, end, items, is_pairs, comment)
105 }
106 CBORCase::Tagged(tag, item) => {
107 if opts.summarize {
108 let mut item_to_return: Option<DiagItem> = None;
109
110 let summarizer_fn_opt: Option<SummarizerFn> = match &opts
112 .tags
113 {
114 TagsStoreOpt::Custom(tags_store_trait) => {
115 tags_store_trait.summarizer(tag.value()).cloned() }
117 TagsStoreOpt::Global => {
118 with_tags!(
119 |global_tags_store: &dyn TagsStoreTrait| {
120 global_tags_store
121 .summarizer(tag.value())
122 .cloned()
123 }
124 )
125 }
126 TagsStoreOpt::None => None,
127 };
128
129 if let Some(summarizer_fn) = summarizer_fn_opt {
131 match summarizer_fn(item.clone(), opts.flat) {
132 Ok(summary_text) => {
133 item_to_return =
134 Some(DiagItem::Item(summary_text));
135 }
136 Err(error) => {
137 item_to_return = Some(DiagItem::Item(format!(
138 "<error: {}>",
139 error
140 )));
141 }
142 }
143 }
144
145 if let Some(diag_item) = item_to_return {
148 return diag_item;
149 }
150 }
153
154 let comment = if opts.annotate {
156 match &opts.tags {
157 TagsStoreOpt::None => None,
158 TagsStoreOpt::Custom(tags_store_trait) => {
159 tags_store_trait.assigned_name_for_tag(tag)
160 }
161 TagsStoreOpt::Global => {
162 with_tags!(|tags_store: &dyn TagsStoreTrait| {
163 tags_store.assigned_name_for_tag(tag)
164 })
165 }
166 }
167 } else {
168 None
169 };
170
171 let diag_item = item.diag_item(opts);
172 let begin = tag.value().to_string() + "(";
173 let end = ")".to_string();
174 let items = vec![diag_item];
175 let is_pairs = false;
176 DiagItem::Group(begin, end, items, is_pairs, comment)
177 }
178 }
179 }
180}
181
182#[derive(Debug)]
183enum DiagItem {
184 Item(String),
185 Group(String, String, Vec<DiagItem>, bool, Option<String>),
186}
187
188impl DiagItem {
189 fn format(&self, opts: &DiagFormatOpts<'_>) -> String {
190 self.format_opt(0, "", opts)
191 }
192
193 fn format_opt(
194 &self,
195 level: usize,
196 separator: &str,
197 opts: &DiagFormatOpts<'_>,
198 ) -> String {
199 match self {
200 DiagItem::Item(string) => {
201 self.format_line(level, opts, string, separator, None)
202 }
203 DiagItem::Group(_, _, _, _, _) => {
204 if !opts.flat
205 && (self.contains_group()
206 || self.total_strings_len() > 20
207 || self.greatest_strings_len() > 20)
208 {
209 self.multiline_composition(level, separator, opts)
210 } else {
211 self.single_line_composition(level, separator, opts)
212 }
213 }
214 }
215 }
216
217 fn format_line(
218 &self,
219 level: usize,
220 opts: &DiagFormatOpts<'_>,
221 string: &str,
222 separator: &str,
223 comment: Option<&str>,
224 ) -> String {
225 let indent = if opts.flat {
226 "".to_string()
227 } else {
228 " ".repeat(level * 4)
229 };
230 let result = format!("{}{}{}", indent, string, separator);
231 if let Some(comment) = comment {
232 format!("{} / {} /", result, comment)
233 } else {
234 result
235 }
236 }
237
238 fn single_line_composition(
239 &self,
240 level: usize,
241 separator: &str,
242 opts: &DiagFormatOpts<'_>,
243 ) -> String {
244 let string: String;
245 let comment: Option<&str>;
246 match self {
247 DiagItem::Item(s) => {
248 string = s.clone();
249 comment = None;
250 }
251 DiagItem::Group(begin, end, items, is_pairs, comm) => {
252 let components: Vec<String> = items
253 .iter()
254 .map(|item| match item {
255 DiagItem::Item(string) => string.clone(),
256 DiagItem::Group(_, _, _, _, _) => item
257 .single_line_composition(
258 level + 1,
259 separator,
260 opts,
261 ),
262 })
263 .collect();
264 let pair_separator = if *is_pairs { ": " } else { ", " };
265 string = flanked(
266 &Self::joined(&components, ", ", Some(pair_separator)),
267 begin,
268 end,
269 );
270 comment = comm.as_ref().map(|x| x.as_str());
271 }
272 };
273 self.format_line(level, opts, &string, separator, comment)
274 }
275
276 fn multiline_composition(
277 &self,
278 level: usize,
279 separator: &str,
280 opts: &DiagFormatOpts<'_>,
281 ) -> String {
282 match self {
283 DiagItem::Item(string) => string.to_owned(),
284 DiagItem::Group(begin, end, items, is_pairs, comment) => {
285 let mut lines: Vec<String> = vec![];
286 lines.push(self.format_line(
287 level,
288 &opts.clone().flat(false),
289 begin,
290 "",
291 comment.as_ref().map(|x| x.as_str()),
292 ));
293 for (index, item) in items.iter().enumerate() {
294 let separator = if index == items.len() - 1 {
295 ""
296 } else if *is_pairs && index & 1 == 0 {
297 ":"
298 } else {
299 ","
300 };
301 lines.push(item.format_opt(level + 1, separator, opts));
302 }
303 lines.push(self.format_line(level, opts, end, separator, None));
304 lines.join("\n")
305 }
306 }
307 }
308
309 fn total_strings_len(&self) -> usize {
310 match self {
311 DiagItem::Item(string) => string.len(),
312 DiagItem::Group(_, _, items, _, _) => items
313 .iter()
314 .fold(0, |acc, item| acc + item.total_strings_len()),
315 }
316 }
317
318 fn greatest_strings_len(&self) -> usize {
319 match self {
320 DiagItem::Item(string) => string.len(),
321 DiagItem::Group(_, _, items, _, _) => items
322 .iter()
323 .fold(0, |acc, item| acc.max(item.total_strings_len())),
324 }
325 }
326
327 fn is_group(&self) -> bool {
328 matches!(self, DiagItem::Group(_, _, _, _, _))
329 }
330
331 fn contains_group(&self) -> bool {
332 match self {
333 DiagItem::Item(_) => false,
334 DiagItem::Group(_, _, items, _, _) => {
335 items.iter().any(|x| x.is_group())
336 }
337 }
338 }
339
340 fn joined(
341 elements: &[String],
342 item_separator: &str,
343 pair_separator: Option<&str>,
344 ) -> String {
345 let pair_separator = pair_separator.unwrap_or(item_separator);
346 let mut result = String::new();
347 let len = elements.len();
348 for (index, item) in elements.iter().enumerate() {
349 result += item;
350 if index != len - 1 {
351 if index & 1 != 0 {
352 result += item_separator;
353 } else {
354 result += pair_separator;
355 }
356 }
357 }
358 result
359 }
360}