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}