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}