dcbor/
diag.rs

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    /// Sets whether to annotate the diagnostic notation with tags.
21    pub fn annotate(mut self, annotate: bool) -> Self {
22        self.annotate = annotate;
23        self
24    }
25
26    /// Sets whether to summarize the diagnostic notation.
27    pub fn summarize(mut self, summarize: bool) -> Self {
28        self.summarize = summarize;
29        self.flat = true; // Summarization implies flat output
30        self
31    }
32
33    /// Sets whether to format the diagnostic notation in a flat manner.
34    pub fn flat(mut self, flat: bool) -> Self {
35        self.flat = flat;
36        self
37    }
38
39    /// Sets the tags for the diagnostic notation.
40    pub fn tags(mut self, tags: TagsStoreOpt<'a>) -> Self {
41        self.tags = tags;
42        self
43    }
44}
45
46/// Affordances for viewing CBOR in diagnostic notation.
47impl CBOR {
48    /// Returns a representation of this CBOR in diagnostic notation.
49    ///
50    /// Optionally annotates the output, e.g. formatting dates and adding names
51    /// of known tags.
52    pub fn diagnostic_opt(&self, opts: &DiagFormatOpts<'_>) -> String {
53        self.diag_item(opts).format(opts)
54    }
55
56    /// Returns a representation of this CBOR in diagnostic notation.
57    pub fn diagnostic(&self) -> String {
58        self.diagnostic_opt(&DiagFormatOpts::default())
59    }
60
61    /// Returns a representation of this CBOR in diagnostic notation, with
62    /// annotations.
63    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                    // Attempt to get a summarizer function based on opts.tags
111                    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() // Clone the Arc
116                        }
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 a summarizer function was found, execute it.
130                    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 summarization produced a DiagItem (either success or
146                    // error string), return it.
147                    if let Some(diag_item) = item_to_return {
148                        return diag_item;
149                    }
150                    // Otherwise (no summarizer found), fall through to default
151                    // tagged item formatting.
152                }
153
154                // Get a possible comment before we move opts
155                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}