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}