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