bc_envelope/base/
tree_format.rs

1//! Creates a textual tree representation of an envelope for debugging and
2//! visualization.
3//!
4//! This module provides functionality for creating a textual tree
5//! representation of an envelope, which is useful for debugging and visualizing
6//! the structure of complex envelopes.
7//!
8//! The tree format displays each component of an envelope (subject and
9//! assertions) as nodes in a tree, making it easy to understand the
10//! hierarchical structure of nested envelopes. Each node includes:
11//!
12//! * The first 8 characters of the element's digest (for easy reference)
13//! * The type of the element (NODE, ASSERTION, ELIDED, etc.)
14//! * The content of the element (for leaf nodes)
15//!
16//! # Examples
17//!
18//! ```
19//! use bc_envelope::prelude::*;
20//!
21//! // Create a complex envelope with nested assertions
22//! let envelope = Envelope::new("Alice").add_assertion(
23//!     "knows",
24//!     Envelope::new("Bob").add_assertion("email", "bob@example.com"),
25//! );
26//!
27//! // Get a tree representation of the envelope
28//! let tree = envelope.tree_format();
29//! // Output will look like:
30//! // 9e3b0673 NODE
31//! //     13941b48 subj "Alice"
32//! //     f45afd77 ASSERTION
33//! //         db7dd21c pred "knows"
34//! //         76543210 obj NODE
35//! //             13b74194 subj "Bob"
36//! //             ee23dcba ASSERTION
37//! //                 a9e85a47 pred "email"
38//! //                 84fd6e57 obj "bob@example.com"
39//! ```
40
41use std::{cell::RefCell, collections::HashSet};
42
43use bc_components::{Digest, DigestProvider};
44use bc_ur::UREncodable;
45
46use super::{
47    EnvelopeSummary, FormatContextOpt, envelope::EnvelopeCase, walk::EdgeType,
48};
49use crate::{Envelope, FormatContext, with_format_context};
50#[cfg(feature = "known_value")]
51use crate::{extension::KnownValuesStore, string_utils::StringUtils};
52
53#[derive(Clone, Copy)]
54pub enum DigestDisplayFormat {
55    /// Default: Display a shortened version of the digest (first 8 characters).
56    Short,
57    /// Display the full digest for each element in the tree.
58    Full,
59    /// Display a `ur:digest` UR for each element in the tree.
60    UR,
61}
62
63impl Default for DigestDisplayFormat {
64    fn default() -> Self { DigestDisplayFormat::Short }
65}
66
67#[derive(Clone, Default)]
68pub struct TreeFormatOpts {
69    hide_nodes: bool,
70    highlighting_target: HashSet<Digest>,
71    context: FormatContextOpt,
72    digest_display: DigestDisplayFormat,
73}
74
75impl TreeFormatOpts {
76    /// Sets whether to hide NODE identifiers in the tree representation.
77    pub fn hide_nodes(mut self, hide: bool) -> Self {
78        self.hide_nodes = hide;
79        self
80    }
81
82    /// Sets the set of digests to highlight in the tree representation.
83    pub fn highlighting_target(mut self, target: HashSet<Digest>) -> Self {
84        self.highlighting_target = target;
85        self
86    }
87
88    /// Sets the formatting context for the tree representation.
89    pub fn context(mut self, context: FormatContextOpt) -> Self {
90        self.context = context;
91        self
92    }
93
94    /// Sets the digest display option for the tree representation.
95    pub fn digest_display(mut self, opt: DigestDisplayFormat) -> Self {
96        self.digest_display = opt;
97        self
98    }
99}
100
101/// Support for tree-formatting envelopes.
102impl Envelope {
103    /// Returns a tree-formatted string representation of the envelope with
104    /// default options.
105    pub fn tree_format(&self) -> String {
106        self.tree_format_opt(TreeFormatOpts::default())
107    }
108
109    /// Returns a tree-formatted string representation of the envelope with the
110    /// specified options.
111    ///
112    /// # Options
113    /// * `hide_nodes` - If true, hides NODE identifiers and only shows the
114    ///   semantic content. Default is `false`.
115    /// * `highlighting_target` - Set of digests to highlight in the tree
116    ///   representation. Default is an empty set.
117    /// * `context` - Formatting context. Default is
118    ///   `TreeFormatContext::Global`.
119    pub fn tree_format_opt(&self, opts: TreeFormatOpts) -> String {
120        let elements: RefCell<Vec<TreeElement>> = RefCell::new(Vec::new());
121        let visitor = |envelope: Self,
122                       level: usize,
123                       incoming_edge: EdgeType,
124                       _: Option<&()>|
125         -> _ {
126            let elem = TreeElement::new(
127                level,
128                envelope.clone(),
129                incoming_edge,
130                !opts.hide_nodes,
131                opts.highlighting_target.contains(&envelope.digest()),
132            );
133            elements.borrow_mut().push(elem);
134            None
135        };
136        let s = self.clone();
137        s.walk(opts.hide_nodes, &visitor);
138
139        let elements = elements.borrow();
140
141        // Closure to format elements with a given context and digest option
142        let format_elements =
143            |elements: &[TreeElement], context: &FormatContext| -> String {
144                elements
145                    .iter()
146                    .map(|e| e.string(context, opts.digest_display))
147                    .collect::<Vec<_>>()
148                    .join("\n")
149            };
150
151        match &opts.context {
152            FormatContextOpt::None => {
153                let context_ref = &FormatContext::default();
154                format_elements(&elements, context_ref)
155            }
156            FormatContextOpt::Global => {
157                with_format_context!(|context| {
158                    format_elements(&elements, context)
159                })
160            }
161            FormatContextOpt::Custom(ctx) => format_elements(&elements, ctx),
162        }
163    }
164}
165
166impl Envelope {
167    /// Returns a text representation of the envelope's digest.
168    pub fn short_id(&self, opt: DigestDisplayFormat) -> String {
169        match opt {
170            DigestDisplayFormat::Short => self.digest().short_description(),
171            DigestDisplayFormat::Full => self.digest().hex(),
172            DigestDisplayFormat::UR => self.digest().ur_string(),
173        }
174    }
175
176    /// Returns a short summary of the envelope's content with a maximum length.
177    ///
178    /// # Arguments
179    /// * `max_length` - The maximum length of the summary
180    /// * `context` - The formatting context
181    pub fn summary(
182        &self,
183        max_length: usize,
184        context: &FormatContext,
185    ) -> String {
186        match self.case() {
187            EnvelopeCase::Node { .. } => "NODE".to_string(),
188            EnvelopeCase::Leaf { cbor, .. } => {
189                cbor.envelope_summary(max_length, context).unwrap()
190            }
191            EnvelopeCase::Wrapped { .. } => "WRAPPED".to_string(),
192            EnvelopeCase::Assertion(_) => "ASSERTION".to_string(),
193            EnvelopeCase::Elided(_) => "ELIDED".to_string(),
194            #[cfg(feature = "known_value")]
195            EnvelopeCase::KnownValue { value, .. } => {
196                let known_value = KnownValuesStore::known_value_for_raw_value(
197                    value.value(),
198                    Some(context.known_values()),
199                );
200                known_value.to_string().flanked_by("'", "'")
201            }
202            #[cfg(feature = "encrypt")]
203            EnvelopeCase::Encrypted(_) => "ENCRYPTED".to_string(),
204            #[cfg(feature = "compress")]
205            EnvelopeCase::Compressed(_) => "COMPRESSED".to_string(),
206        }
207    }
208}
209
210/// Represents an element in the tree representation of an envelope.
211#[derive(Debug)]
212struct TreeElement {
213    /// Indentation level of the element in the tree
214    level: usize,
215    /// The envelope element
216    envelope: Envelope,
217    /// The type of edge connecting this element to its parent
218    incoming_edge: EdgeType,
219    /// Whether to show the element's ID (digest)
220    show_id: bool,
221    /// Whether this element should be highlighted in the output
222    is_highlighted: bool,
223}
224
225impl TreeElement {
226    /// Creates a new TreeElement.
227    ///
228    /// # Arguments
229    /// * `level` - Indentation level of the element in the tree
230    /// * `envelope` - The envelope element
231    /// * `incoming_edge` - The type of edge connecting this element to its
232    ///   parent
233    /// * `show_id` - Whether to show the element's ID (digest)
234    /// * `is_highlighted` - Whether this element should be highlighted in the
235    ///   output
236    fn new(
237        level: usize,
238        envelope: Envelope,
239        incoming_edge: EdgeType,
240        show_id: bool,
241        is_highlighted: bool,
242    ) -> Self {
243        Self {
244            level,
245            envelope,
246            incoming_edge,
247            show_id,
248            is_highlighted,
249        }
250    }
251
252    /// Formats the tree element as a string.
253    fn string(
254        &self,
255        context: &FormatContext,
256        digest_display: DigestDisplayFormat,
257    ) -> String {
258        let line = vec![
259            if self.is_highlighted {
260                Some("*".to_string())
261            } else {
262                None
263            },
264            if self.show_id {
265                Some(self.envelope.short_id(digest_display))
266            } else {
267                None
268            },
269            self.incoming_edge.label().map(|s| s.to_string()),
270            Some(self.envelope.summary(40, context)),
271        ]
272        .into_iter()
273        .flatten()
274        .collect::<Vec<_>>()
275        .join(" ");
276        let indent = " ".repeat(self.level * 4);
277        format!("{}{}", indent, line)
278    }
279}