bc_envelope/format/
tree.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::FormatContextOpt;
47use crate::{EdgeType, Envelope, FormatContext, with_format_context};
48
49#[derive(Clone, Copy, Default)]
50pub enum DigestDisplayFormat {
51    /// Default: Display a shortened version of the digest (first 8 characters).
52    #[default]
53    Short,
54    /// Display the full digest for each element in the tree.
55    Full,
56    /// Display a `ur:digest` UR for each element in the tree.
57    UR,
58}
59
60#[derive(Clone, Default)]
61pub struct TreeFormatOpts<'a> {
62    hide_nodes: bool,
63    highlighting_target: HashSet<Digest>,
64    context: FormatContextOpt<'a>,
65    digest_display: DigestDisplayFormat,
66}
67
68impl<'a> TreeFormatOpts<'a> {
69    /// Sets whether to hide NODE identifiers in the tree representation.
70    pub fn hide_nodes(mut self, hide: bool) -> Self {
71        self.hide_nodes = hide;
72        self
73    }
74
75    /// Sets the set of digests to highlight in the tree representation.
76    pub fn highlighting_target(mut self, target: HashSet<Digest>) -> Self {
77        self.highlighting_target = target;
78        self
79    }
80
81    /// Sets the formatting context for the tree representation.
82    pub fn context(mut self, context: FormatContextOpt<'a>) -> Self {
83        self.context = context;
84        self
85    }
86
87    /// Sets the digest display option for the tree representation.
88    pub fn digest_display(mut self, opt: DigestDisplayFormat) -> Self {
89        self.digest_display = opt;
90        self
91    }
92}
93
94/// Support for tree-formatting envelopes.
95impl Envelope {
96    /// Returns a tree-formatted string representation of the envelope with
97    /// default options.
98    pub fn tree_format(&self) -> String {
99        self.tree_format_opt(&TreeFormatOpts::default())
100    }
101
102    /// Returns a tree-formatted string representation of the envelope with the
103    /// specified options.
104    ///
105    /// # Options
106    /// * `hide_nodes` - If true, hides NODE identifiers and only shows the
107    ///   semantic content. Default is `false`.
108    /// * `highlighting_target` - Set of digests to highlight in the tree
109    ///   representation. Default is an empty set.
110    /// * `context` - Formatting context. Default is
111    ///   `TreeFormatContext::Global`.
112    pub fn tree_format_opt(&self, opts: &TreeFormatOpts<'_>) -> String {
113        let elements: RefCell<Vec<TreeElement>> = RefCell::new(Vec::new());
114        let visitor = |envelope: &Envelope,
115                       level: usize,
116                       incoming_edge: EdgeType,
117                       _: ()|
118         -> (_, bool) {
119            let elem = TreeElement::new(
120                level,
121                envelope.clone(),
122                incoming_edge,
123                !opts.hide_nodes,
124                opts.highlighting_target.contains(&envelope.digest()),
125            );
126            elements.borrow_mut().push(elem);
127            ((), false)
128        };
129        self.walk(opts.hide_nodes, (), &visitor);
130
131        let elements = elements.borrow();
132
133        // Closure to format elements with a given context and digest option
134        let format_elements =
135            |elements: &[TreeElement], context: &FormatContext| -> String {
136                elements
137                    .iter()
138                    .map(|e| e.string(context, opts.digest_display))
139                    .collect::<Vec<_>>()
140                    .join("\n")
141            };
142
143        match &opts.context {
144            FormatContextOpt::None => {
145                let context_ref = &FormatContext::default();
146                format_elements(&elements, context_ref)
147            }
148            FormatContextOpt::Global => {
149                with_format_context!(|context| {
150                    format_elements(&elements, context)
151                })
152            }
153            FormatContextOpt::Custom(ctx) => format_elements(&elements, ctx),
154        }
155    }
156}
157
158impl Envelope {
159    /// Returns a text representation of the envelope's digest.
160    pub fn short_id(&self, opt: DigestDisplayFormat) -> String {
161        match opt {
162            DigestDisplayFormat::Short => self.digest().short_description(),
163            DigestDisplayFormat::Full => self.digest().hex(),
164            DigestDisplayFormat::UR => self.digest().ur_string(),
165        }
166    }
167}
168
169/// Represents an element in the tree representation of an envelope.
170#[derive(Debug)]
171struct TreeElement {
172    /// Indentation level of the element in the tree
173    level: usize,
174    /// The envelope element
175    envelope: Envelope,
176    /// The type of edge connecting this element to its parent
177    incoming_edge: EdgeType,
178    /// Whether to show the element's ID (digest)
179    show_id: bool,
180    /// Whether this element should be highlighted in the output
181    is_highlighted: bool,
182}
183
184impl TreeElement {
185    /// Creates a new TreeElement.
186    ///
187    /// # Arguments
188    /// * `level` - Indentation level of the element in the tree
189    /// * `envelope` - The envelope element
190    /// * `incoming_edge` - The type of edge connecting this element to its
191    ///   parent
192    /// * `show_id` - Whether to show the element's ID (digest)
193    /// * `is_highlighted` - Whether this element should be highlighted in the
194    ///   output
195    fn new(
196        level: usize,
197        envelope: Envelope,
198        incoming_edge: EdgeType,
199        show_id: bool,
200        is_highlighted: bool,
201    ) -> Self {
202        Self {
203            level,
204            envelope,
205            incoming_edge,
206            show_id,
207            is_highlighted,
208        }
209    }
210
211    /// Formats the tree element as a string.
212    fn string(
213        &self,
214        context: &FormatContext,
215        digest_display: DigestDisplayFormat,
216    ) -> String {
217        let line = vec![
218            if self.is_highlighted {
219                Some("*".to_string())
220            } else {
221                None
222            },
223            if self.show_id {
224                Some(self.envelope.short_id(digest_display))
225            } else {
226                None
227            },
228            self.incoming_edge.label().map(|s| s.to_string()),
229            Some(self.envelope.summary(40, context)),
230        ]
231        .into_iter()
232        .flatten()
233        .collect::<Vec<_>>()
234        .join(" ");
235        let indent = " ".repeat(self.level * 4);
236        format!("{}{}", indent, line)
237    }
238}