bc_envelope/format/
notation.rs

1//! Formats an envelope as a CBOR data structure and provides human-readable
2//! text representations.
3//!
4//! This module provides functionality for formatting envelopes in various ways:
5//!
6//! 1. **Envelope Notation**: A human-readable text representation showing the
7//!    semantic structure of an envelope (the `format` methods)
8//!
9//! 2. **CBOR Diagnostics**: Text representation of the underlying CBOR structure, following
10//!    [RFC-8949 ยง8](https://www.rfc-editor.org/rfc/rfc8949.html#name-diagnostic-notation)
11//!    (the `diagnostic` methods)
12//!
13//! 3. **CBOR Hex**: Hexadecimal representation of the raw CBOR bytes (the `hex`
14//!    methods)
15//!
16//! All of these formats can be used for debugging, visualization, and
17//! understanding the structure of envelopes.
18//!
19//! # Examples
20//!
21//! ```
22//! use bc_envelope::prelude::*;
23//!
24//! // Create a simple envelope with assertions
25//! let envelope = Envelope::new("Alice")
26//!     .add_assertion("knows", "Bob")
27//!     .add_assertion("knows", "Carol");
28//!
29//! // Format the envelope as human-readable envelope notation
30//! let formatted = envelope.format();
31//! // Will output: "Alice" [ "knows": "Bob", "knows": "Carol" ]
32//!
33//! // Get the CBOR diagnostic notation (with annotations)
34//! let diagnostic = envelope.diagnostic_annotated();
35//!
36//! // Get the CBOR hex representation
37//! let hex = envelope.hex();
38//! ```
39
40use bc_components::XID;
41use dcbor::prelude::*;
42
43use super::EnvelopeSummary;
44use crate::{
45    Assertion, Envelope, FormatContextOpt, base::envelope::EnvelopeCase,
46    string_utils::StringUtils,
47};
48#[cfg(feature = "known_value")]
49use crate::{KnownValue, known_values};
50
51#[derive(Clone, Default)]
52pub struct EnvelopeFormatOpts<'a> {
53    flat: bool,
54    context: FormatContextOpt<'a>,
55}
56
57impl<'a> EnvelopeFormatOpts<'a> {
58    /// Sets the flat format option.
59    pub fn flat(mut self, flat: bool) -> Self {
60        self.flat = flat;
61        self
62    }
63
64    /// Sets the format context option.
65    pub fn context(mut self, context: FormatContextOpt<'a>) -> Self {
66        self.context = context;
67        self
68    }
69}
70
71/// Support for the various text output formats for ``Envelope``.
72impl Envelope {
73    /// Returns the envelope notation for this envelope.
74    pub fn format_opt(&self, opts: &EnvelopeFormatOpts<'_>) -> String {
75        self.format_item(opts).format(opts).trim().to_string()
76    }
77
78    /// Returns the envelope notation for this envelope.
79    ///
80    /// Uses the current format context.
81    pub fn format(&self) -> String {
82        self.format_opt(&EnvelopeFormatOpts::default())
83    }
84
85    /// Returns the envelope notation for this envelope in flat format.
86    ///
87    /// In flat format, the envelope is printed on a single line.
88    pub fn format_flat(&self) -> String {
89        self.format_opt(&EnvelopeFormatOpts::default().flat(true))
90    }
91}
92
93/// Implementers of this trait define how to be formatted in when output in
94/// envelope notation.
95pub trait EnvelopeFormat {
96    fn format_item(&self, opts: &EnvelopeFormatOpts<'_>) -> EnvelopeFormatItem;
97}
98
99/// This type is returned by implementers of the [`EnvelopeFormat`] trait.
100#[derive(Debug, Clone, Eq)]
101pub enum EnvelopeFormatItem {
102    Begin(String),
103    End(String),
104    Item(String),
105    Separator,
106    List(Vec<EnvelopeFormatItem>),
107}
108
109impl EnvelopeFormatItem {
110    fn flatten(&self) -> Vec<EnvelopeFormatItem> {
111        match self {
112            EnvelopeFormatItem::List(items) => {
113                items.iter().flat_map(|i| i.flatten()).collect()
114            }
115            _ => vec![self.clone()],
116        }
117    }
118
119    fn nicen(items: &[EnvelopeFormatItem]) -> Vec<EnvelopeFormatItem> {
120        let mut input = items.to_vec();
121        let mut result: Vec<EnvelopeFormatItem> = vec![];
122
123        while !input.is_empty() {
124            let current = input.remove(0);
125            if input.is_empty() {
126                result.push(current);
127                break;
128            }
129            if let EnvelopeFormatItem::End(end_string) = current.clone() {
130                if let EnvelopeFormatItem::Begin(begin_string) =
131                    input[0].clone()
132                {
133                    result.push(EnvelopeFormatItem::End(format!(
134                        "{} {}",
135                        end_string, begin_string
136                    )));
137                    result.push(EnvelopeFormatItem::Begin("".to_string()));
138                    input.remove(0);
139                } else {
140                    result.push(current);
141                }
142            } else {
143                result.push(current);
144            }
145        }
146
147        result
148    }
149
150    fn indent(level: usize) -> String { " ".repeat(level * 4) }
151
152    fn add_space_at_end_if_needed(s: &str) -> String {
153        if s.is_empty() {
154            " ".to_string()
155        } else if s.ends_with(' ') {
156            s.to_string()
157        } else {
158            s.to_string() + " "
159        }
160    }
161
162    fn format(&self, opts: &EnvelopeFormatOpts<'_>) -> String {
163        if opts.flat {
164            return self.format_flat();
165        }
166        self.format_hierarchical()
167    }
168
169    fn format_flat(&self) -> String {
170        let mut line: String = "".to_string();
171        let items = self.flatten();
172        for item in items {
173            match item {
174                EnvelopeFormatItem::Begin(s) => {
175                    if !line.ends_with(' ') {
176                        line += " ";
177                    }
178                    line += &s;
179                    line += " ";
180                }
181                EnvelopeFormatItem::End(s) => {
182                    if !line.ends_with(' ') {
183                        line += " ";
184                    }
185                    line += &s;
186                    line += " ";
187                }
188                EnvelopeFormatItem::Item(s) => line += &s,
189                EnvelopeFormatItem::Separator => {
190                    line = line.trim_end().to_string() + ", ";
191                }
192                EnvelopeFormatItem::List(items) => {
193                    for item in items {
194                        line += &item.format_flat();
195                    }
196                }
197            }
198        }
199        line
200    }
201
202    fn format_hierarchical(&self) -> String {
203        let mut lines: Vec<String> = vec![];
204        let mut level = 0;
205        let mut current_line = "".to_string();
206        let items = Self::nicen(&self.flatten());
207        for item in items {
208            match item {
209                EnvelopeFormatItem::Begin(delimiter) => {
210                    if !delimiter.is_empty() {
211                        let c = if current_line.is_empty() {
212                            delimiter
213                        } else {
214                            Self::add_space_at_end_if_needed(&current_line)
215                                + &delimiter
216                        };
217                        lines.push(Self::indent(level) + &c + "\n");
218                    }
219                    level += 1;
220                    current_line = "".to_string();
221                }
222                EnvelopeFormatItem::End(delimiter) => {
223                    if !current_line.is_empty() {
224                        lines.push(Self::indent(level) + &current_line + "\n");
225                        current_line = "".to_string();
226                    }
227                    level -= 1;
228                    lines.push(Self::indent(level) + &delimiter + "\n");
229                }
230                EnvelopeFormatItem::Item(string) => {
231                    current_line += &string;
232                }
233                EnvelopeFormatItem::Separator => {
234                    if !current_line.is_empty() {
235                        lines.push(Self::indent(level) + &current_line + "\n");
236                        current_line = "".to_string();
237                    }
238                }
239                EnvelopeFormatItem::List(_) => {
240                    lines.push("<list>".to_string());
241                }
242            }
243        }
244        if !current_line.is_empty() {
245            lines.push(current_line);
246        }
247        lines.join("")
248    }
249}
250
251/// Implements conversion from string slice to EnvelopeFormatItem.
252impl From<&str> for EnvelopeFormatItem {
253    fn from(s: &str) -> Self { Self::Item(s.to_string()) }
254}
255
256/// Implements string display for EnvelopeFormatItem.
257impl std::fmt::Display for EnvelopeFormatItem {
258    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259        match self {
260            EnvelopeFormatItem::Begin(s) => write!(f, ".begin({})", s),
261            EnvelopeFormatItem::End(s) => write!(f, ".end({})", s),
262            EnvelopeFormatItem::Item(s) => write!(f, ".item({})", s),
263            EnvelopeFormatItem::Separator => write!(f, ".separator"),
264            EnvelopeFormatItem::List(items) => write!(f, ".list({:?})", items),
265        }
266    }
267}
268
269/// Implements partial equality for EnvelopeFormatItem.
270impl PartialEq for EnvelopeFormatItem {
271    fn eq(&self, other: &Self) -> bool {
272        match (self, other) {
273            (EnvelopeFormatItem::Begin(s1), EnvelopeFormatItem::Begin(s2)) => {
274                s1 == s2
275            }
276            (EnvelopeFormatItem::End(s1), EnvelopeFormatItem::End(s2)) => {
277                s1 == s2
278            }
279            (EnvelopeFormatItem::Item(s1), EnvelopeFormatItem::Item(s2)) => {
280                s1 == s2
281            }
282            (EnvelopeFormatItem::Separator, EnvelopeFormatItem::Separator) => {
283                true
284            }
285            (
286                EnvelopeFormatItem::List(items1),
287                EnvelopeFormatItem::List(items2),
288            ) => items1 == items2,
289            _ => false,
290        }
291    }
292}
293
294impl EnvelopeFormatItem {
295    fn index(&self) -> u32 {
296        match self {
297            EnvelopeFormatItem::Begin(_) => 1,
298            EnvelopeFormatItem::End(_) => 2,
299            EnvelopeFormatItem::Item(_) => 3,
300            EnvelopeFormatItem::Separator => 4,
301            EnvelopeFormatItem::List(_) => 5,
302        }
303    }
304}
305
306/// Implements partial ordering for EnvelopeFormatItem.
307impl PartialOrd for EnvelopeFormatItem {
308    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
309        Some(self.cmp(other))
310    }
311}
312
313/// Implements total ordering for EnvelopeFormatItem.
314impl Ord for EnvelopeFormatItem {
315    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
316        let l_index = self.index();
317        let r_index = other.index();
318        match l_index.cmp(&r_index) {
319            std::cmp::Ordering::Less => return std::cmp::Ordering::Less,
320            std::cmp::Ordering::Greater => return std::cmp::Ordering::Greater,
321            _ => {}
322        }
323        match (self, other) {
324            (EnvelopeFormatItem::Begin(l), EnvelopeFormatItem::Begin(r)) => {
325                l.cmp(r)
326            }
327            (EnvelopeFormatItem::End(l), EnvelopeFormatItem::End(r)) => {
328                l.cmp(r)
329            }
330            (EnvelopeFormatItem::Item(l), EnvelopeFormatItem::Item(r)) => {
331                l.cmp(r)
332            }
333            (EnvelopeFormatItem::Separator, EnvelopeFormatItem::Separator) => {
334                std::cmp::Ordering::Equal
335            }
336            (EnvelopeFormatItem::List(l), EnvelopeFormatItem::List(r)) => {
337                l.cmp(r)
338            }
339            _ => std::cmp::Ordering::Equal,
340        }
341    }
342}
343
344/// Implements formatting for CBOR values in envelope notation.
345impl EnvelopeFormat for CBOR {
346    fn format_item(&self, opts: &EnvelopeFormatOpts<'_>) -> EnvelopeFormatItem {
347        match self.as_case() {
348            CBORCase::Tagged(tag, cbor) if tag == &Envelope::cbor_tags()[0] => {
349                Envelope::from_untagged_cbor(cbor.clone())
350                    .map(|envelope| envelope.format_item(opts))
351                    .unwrap_or_else(|_| "<error>".into())
352            }
353            _ => EnvelopeFormatItem::Item(
354                self.envelope_summary(usize::MAX, &opts.context)
355                    .unwrap_or_else(|_| "<error>".into()),
356            ),
357        }
358    }
359}
360
361/// Implements formatting for Envelope in envelope notation.
362impl EnvelopeFormat for Envelope {
363    fn format_item(&self, opts: &EnvelopeFormatOpts<'_>) -> EnvelopeFormatItem {
364        match self.case() {
365            EnvelopeCase::Leaf { cbor, .. } => cbor.format_item(opts),
366            EnvelopeCase::Wrapped { envelope, .. } => {
367                EnvelopeFormatItem::List(vec![
368                    EnvelopeFormatItem::Begin("{".to_string()),
369                    envelope.format_item(opts),
370                    EnvelopeFormatItem::End("}".to_string()),
371                ])
372            }
373            EnvelopeCase::Assertion(assertion) => assertion.format_item(opts),
374            #[cfg(feature = "known_value")]
375            EnvelopeCase::KnownValue { value, .. } => value.format_item(opts),
376            #[cfg(feature = "encrypt")]
377            EnvelopeCase::Encrypted(_) => {
378                EnvelopeFormatItem::Item("ENCRYPTED".to_string())
379            }
380            #[cfg(feature = "compress")]
381            EnvelopeCase::Compressed(_) => {
382                EnvelopeFormatItem::Item("COMPRESSED".to_string())
383            }
384            EnvelopeCase::Node { subject, assertions, .. } => {
385                let mut items: Vec<EnvelopeFormatItem> = Vec::new();
386
387                let subject_item = subject.format_item(opts);
388                let mut elided_count = 0;
389                #[cfg(feature = "encrypt")]
390                let mut encrypted_count = 0;
391                #[cfg(feature = "compress")]
392                let mut compressed_count = 0;
393                #[cfg(feature = "known_value")]
394                let mut type_assertion_items: Vec<
395                    Vec<EnvelopeFormatItem>,
396                > = Vec::new();
397                let mut assertion_items: Vec<Vec<EnvelopeFormatItem>> =
398                    Vec::new();
399
400                for assertion in assertions {
401                    match assertion.case() {
402                        EnvelopeCase::Elided(_) => {
403                            elided_count += 1;
404                        }
405                        #[cfg(feature = "encrypt")]
406                        EnvelopeCase::Encrypted(_) => {
407                            encrypted_count += 1;
408                        }
409                        #[cfg(feature = "compress")]
410                        EnvelopeCase::Compressed(_) => {
411                            compressed_count += 1;
412                        }
413                        _ => {
414                            let item = vec![assertion.format_item(opts)];
415                            #[cfg(feature = "known_value")]
416                            {
417                                let mut is_type_assertion = false;
418                                if let Some(predicate) =
419                                    assertion.as_predicate()
420                                    && let Some(known_value) =
421                                        predicate.subject().as_known_value()
422                                    && *known_value == known_values::IS_A
423                                {
424                                    is_type_assertion = true;
425                                }
426
427                                if is_type_assertion {
428                                    type_assertion_items.push(item);
429                                } else {
430                                    assertion_items.push(item);
431                                }
432                            }
433                            #[cfg(not(feature = "known_value"))]
434                            assertion_items.push(item);
435                        }
436                    }
437                }
438                #[cfg(feature = "known_value")]
439                type_assertion_items.sort();
440                assertion_items.sort();
441                #[cfg(feature = "known_value")]
442                assertion_items.splice(0..0, type_assertion_items);
443                #[cfg(feature = "compress")]
444                if compressed_count > 1 {
445                    assertion_items.push(vec![EnvelopeFormatItem::Item(
446                        format!("COMPRESSED ({})", compressed_count),
447                    )]);
448                } else if compressed_count > 0 {
449                    assertion_items.push(vec![EnvelopeFormatItem::Item(
450                        "COMPRESSED".to_string(),
451                    )]);
452                }
453                if elided_count > 1 {
454                    assertion_items.push(vec![EnvelopeFormatItem::Item(
455                        format!("ELIDED ({})", elided_count),
456                    )]);
457                } else if elided_count > 0 {
458                    assertion_items.push(vec![EnvelopeFormatItem::Item(
459                        "ELIDED".to_string(),
460                    )]);
461                }
462                #[cfg(feature = "encrypt")]
463                if encrypted_count > 1 {
464                    assertion_items.push(vec![EnvelopeFormatItem::Item(
465                        format!("ENCRYPTED ({})", encrypted_count),
466                    )]);
467                } else if encrypted_count > 0 {
468                    assertion_items.push(vec![EnvelopeFormatItem::Item(
469                        "ENCRYPTED".to_string(),
470                    )]);
471                }
472                let joined_assertions_items: Vec<Vec<EnvelopeFormatItem>> =
473                    itertools::intersperse_with(assertion_items, || {
474                        vec![EnvelopeFormatItem::Separator]
475                    })
476                    .collect();
477
478                let needs_braces = subject.is_subject_assertion();
479
480                if needs_braces {
481                    items.push(EnvelopeFormatItem::Begin("{".to_string()));
482                }
483                items.push(subject_item);
484                if needs_braces {
485                    items.push(EnvelopeFormatItem::End("}".to_string()));
486                }
487                items.push(EnvelopeFormatItem::Begin("[".to_string()));
488                items.extend(joined_assertions_items.into_iter().flatten());
489                items.push(EnvelopeFormatItem::End("]".to_string()));
490                EnvelopeFormatItem::List(items)
491            }
492            EnvelopeCase::Elided(_) => {
493                EnvelopeFormatItem::Item("ELIDED".to_string())
494            }
495        }
496    }
497}
498
499/// Implements formatting for Assertion in envelope notation.
500impl EnvelopeFormat for Assertion {
501    fn format_item(&self, opts: &EnvelopeFormatOpts<'_>) -> EnvelopeFormatItem {
502        EnvelopeFormatItem::List(vec![
503            self.predicate().format_item(opts),
504            EnvelopeFormatItem::Item(": ".to_string()),
505            self.object().format_item(opts),
506        ])
507    }
508}
509
510#[cfg(feature = "known_value")]
511/// Implements formatting for KnownValue in envelope notation.
512impl EnvelopeFormat for KnownValue {
513    fn format_item(&self, opts: &EnvelopeFormatOpts<'_>) -> EnvelopeFormatItem {
514        let name = match opts.context {
515            FormatContextOpt::None => {
516                self.name().to_string().flanked_by("'", "'")
517            }
518            FormatContextOpt::Global => {
519                crate::with_format_context!(|context: &crate::FormatContext| {
520                    context
521                        .known_values()
522                        .assigned_name(self)
523                        .map(|s| s.to_string())
524                        .unwrap_or_else(|| self.name().to_string())
525                })
526            }
527            FormatContextOpt::Custom(format_context) => format_context
528                .known_values()
529                .assigned_name(self)
530                .map(|s| s.to_string())
531                .unwrap_or_else(|| self.name().to_string()),
532        };
533        EnvelopeFormatItem::Item(name.flanked_by("'", "'"))
534    }
535}
536
537/// Implements formatting for XID in envelope notation.
538impl EnvelopeFormat for XID {
539    fn format_item(
540        &self,
541        _opts: &EnvelopeFormatOpts<'_>,
542    ) -> EnvelopeFormatItem {
543        EnvelopeFormatItem::Item(hex::encode(self.data()))
544    }
545}
546
547impl Envelope {
548    fn description(&self, opts: &EnvelopeFormatOpts<'_>) -> String {
549        match self.case() {
550            EnvelopeCase::Node { subject, assertions, .. } => {
551                let assertions = assertions
552                    .iter()
553                    .map(|a| a.to_string())
554                    .collect::<Vec<String>>()
555                    .join(", ")
556                    .flanked_by("[", "]");
557                format!(".node({}, {})", subject, assertions)
558            }
559            EnvelopeCase::Leaf { cbor, .. } => {
560                format!(".cbor({})", cbor.format_item(opts))
561            }
562            EnvelopeCase::Wrapped { envelope, .. } => {
563                format!(".wrapped({})", envelope)
564            }
565            EnvelopeCase::Assertion(assertion) => format!(
566                ".assertion({}, {})",
567                assertion.predicate(),
568                assertion.object()
569            ),
570            EnvelopeCase::Elided(_) => ".elided".to_string(),
571            #[cfg(feature = "known_value")]
572            EnvelopeCase::KnownValue { value, .. } => {
573                format!(".knownValue({})", value)
574            }
575            #[cfg(feature = "encrypt")]
576            EnvelopeCase::Encrypted(_) => ".encrypted".to_string(),
577            #[cfg(feature = "compress")]
578            EnvelopeCase::Compressed(_) => ".compressed".to_string(),
579        }
580    }
581}
582
583/// Implements string display for Envelope.
584impl std::fmt::Display for Envelope {
585    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
586        f.write_str(&self.description(&EnvelopeFormatOpts::default()))
587    }
588}