1pub(crate) trait TreeDisplay {
19 fn fmt_tree(&self, w: &mut TreeWriter<'_, '_>) -> std::fmt::Result;
20}
21
22macro_rules! impl_tree_display {
24 ($($ty:ty),* $(,)?) => {
25 $(
26 impl std::fmt::Display for $ty {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 let mut w = crate::TreeWriter::new(f);
29 crate::TreeDisplay::fmt_tree(self, &mut w)
30 }
31 }
32 )*
33 };
34}
35pub(crate) use impl_tree_display;
36
37enum PendingLabel {
39 Named(String),
41 Indexed,
43}
44
45pub(crate) struct TreeWriter<'f, 'a> {
48 f: &'f mut std::fmt::Formatter<'a>,
49 prefix: String,
50 needs_newline: bool,
51 inline_label: Option<PendingLabel>,
52 pending_enum: Option<String>,
53 skip_header: bool,
54}
55
56impl<'f, 'a> TreeWriter<'f, 'a> {
57 pub fn new(f: &'f mut std::fmt::Formatter<'a>) -> Self {
58 Self {
59 f,
60 prefix: String::new(),
61 needs_newline: false,
62 inline_label: None,
63 pending_enum: None,
64 skip_header: false,
65 }
66 }
67
68 pub fn header(&mut self, text: &str) -> std::fmt::Result {
73 let enum_name = self.pending_enum.take();
74 if std::mem::take(&mut self.skip_header) {
75 return Ok(());
76 }
77 if let Some(pending) = self.inline_label.take() {
78 match pending {
79 PendingLabel::Named(label) if label == text => {}
81 PendingLabel::Named(_) => write!(self.f, ": {text}")?,
83 PendingLabel::Indexed => match &enum_name {
84 Some(name) => write!(self.f, ": {name}: {text}")?,
85 None => write!(self.f, ": {text}")?,
86 },
87 }
88 return Ok(());
89 }
90 if self.needs_newline {
91 writeln!(self.f)?;
92 }
93 match &enum_name {
94 Some(name) => write!(self.f, "{}{name}: {text}", self.prefix)?,
95 None => write!(self.f, "{}{}", self.prefix, text)?,
96 }
97 self.needs_newline = true;
98 Ok(())
99 }
100
101 pub fn enum_name(&mut self, name: &str) {
109 match &mut self.pending_enum {
110 Some(pending) => {
111 pending.push_str(": ");
112 pending.push_str(name);
113 }
114 None => self.pending_enum = Some(name.to_owned()),
115 }
116 }
117
118 fn write_connector(&mut self, is_last: bool) -> std::fmt::Result {
119 if self.needs_newline {
120 writeln!(self.f)?;
121 }
122 let connector = if is_last { "└── " } else { "├── " };
123 write!(self.f, "{}{}", self.prefix, connector)?;
124 self.needs_newline = true;
125 Ok(())
126 }
127
128 pub fn leaf(
133 &mut self,
134 label: &str,
135 value: &dyn std::fmt::Display,
136 is_last: bool,
137 ) -> std::fmt::Result {
138 self.write_connector(is_last)?;
139 let value = value.to_string();
140 if value.contains('\n') {
141 write!(self.f, "{label}:")?;
142 let extension = if is_last { " " } else { "│ " };
143 for line in value.lines() {
144 write!(self.f, "\n{}{extension}{line}", self.prefix)?;
145 }
146 Ok(())
147 } else {
148 write!(self.f, "{label}: {value}")
149 }
150 }
151
152 pub fn branch(
154 &mut self,
155 label: &str,
156 is_last: bool,
157 children: impl FnOnce(&mut Self) -> std::fmt::Result,
158 ) -> std::fmt::Result {
159 self.write_connector(is_last)?;
160 write!(self.f, "{label}")?;
161 let extension = if is_last { " " } else { "│ " };
162 let old_len = self.prefix.len();
163 self.prefix.push_str(extension);
164 children(self)?;
165 self.prefix.truncate(old_len);
166 Ok(())
167 }
168
169 pub fn child(
171 &mut self,
172 label: &str,
173 child: &dyn TreeDisplay,
174 is_last: bool,
175 ) -> std::fmt::Result {
176 self.branch(label, is_last, |w| {
177 w.inline_label = Some(PendingLabel::Named(label.to_string()));
178 child.fmt_tree(w)
179 })
180 }
181
182 fn indexed_child(
185 &mut self,
186 index: usize,
187 child: &dyn TreeDisplay,
188 is_last: bool,
189 ) -> std::fmt::Result {
190 self.branch(&index.to_string(), is_last, |w| {
191 w.inline_label = Some(PendingLabel::Indexed);
192 child.fmt_tree(w)
193 })
194 }
195
196 pub fn inline_child(&mut self, child: &dyn TreeDisplay) -> std::fmt::Result {
199 self.skip_header = true;
200 child.fmt_tree(self)
201 }
202
203 pub fn leaves<I>(&mut self, label: &str, items: I, is_last: bool) -> std::fmt::Result
205 where
206 I: IntoIterator,
207 I::Item: std::fmt::Display,
208 I::IntoIter: ExactSizeIterator,
209 {
210 let items = items.into_iter();
211 if items.len() == 0 {
212 self.leaf(label, &"[]", is_last)
213 } else {
214 self.branch(label, is_last, |w| {
215 let last_idx = items.len() - 1;
216 for (i, item) in items.enumerate() {
217 w.leaf(&i.to_string(), &item, i == last_idx)?;
218 }
219 Ok(())
220 })
221 }
222 }
223
224 pub fn children(
226 &mut self,
227 label: &str,
228 items: &[impl TreeDisplay],
229 is_last: bool,
230 ) -> std::fmt::Result {
231 if items.is_empty() {
232 self.leaf(label, &"[]", is_last)
233 } else {
234 self.branch(label, is_last, |w| {
235 let last_idx = items.len() - 1;
236 for (i, item) in items.iter().enumerate() {
237 w.indexed_child(i, item, i == last_idx)?;
238 }
239 Ok(())
240 })
241 }
242 }
243
244 pub fn option_leaf(
246 &mut self,
247 label: &str,
248 opt: &Option<impl std::fmt::Display>,
249 is_last: bool,
250 ) -> std::fmt::Result {
251 match opt {
252 Some(v) => self.leaf(label, v, is_last),
253 None => self.leaf(label, &"None", is_last),
254 }
255 }
256
257 pub fn option_child(
260 &mut self,
261 label: &str,
262 opt: &Option<impl TreeDisplay>,
263 is_last: bool,
264 ) -> std::fmt::Result {
265 match opt {
266 Some(v) => self.child(label, v, is_last),
267 None => self.leaf(label, &"None", is_last),
268 }
269 }
270
271 pub fn base64_leaves(
273 &mut self,
274 label: &str,
275 items: &[Vec<u8>],
276 is_last: bool,
277 ) -> std::fmt::Result {
278 if items.is_empty() {
279 self.leaf(label, &"[]", is_last)
280 } else {
281 use base64ct::Encoding;
282 self.branch(label, is_last, |w| {
283 let last_idx = items.len() - 1;
284 for (i, bytes) in items.iter().enumerate() {
285 w.leaf(
286 &i.to_string(),
287 &base64ct::Base64::encode_string(bytes),
288 i == last_idx,
289 )?;
290 }
291 Ok(())
292 })
293 }
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use std::fmt;
300
301 use super::*;
302
303 struct FmtFn<F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result>(F);
306
307 impl<F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result> fmt::Display for FmtFn<F> {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 (self.0)(f)
310 }
311 }
312
313 #[test]
314 fn writer_starts_without_a_leading_newline() {
315 let output = FmtFn(|f| {
316 let mut w = TreeWriter::new(f);
317 w.leaf("Key", &"value", true)
318 })
319 .to_string();
320
321 assert_eq!(output, "└── Key: value");
322 }
323
324 fn render(body: impl Fn(&mut TreeWriter<'_, '_>) -> fmt::Result) -> String {
326 FmtFn(move |f| body(&mut TreeWriter::new(f))).to_string()
327 }
328
329 struct Point {
331 x: u8,
332 y: u8,
333 }
334
335 impl TreeDisplay for Point {
336 fn fmt_tree(&self, w: &mut TreeWriter<'_, '_>) -> fmt::Result {
337 w.header("Point")?;
338 w.leaf("X", &self.x, false)?;
339 w.leaf("Y", &self.y, true)
340 }
341 }
342
343 impl_tree_display!(Point);
344
345 #[test]
346 fn header_writes_root_label_without_connector() {
347 assert_eq!(render(|w| w.header("Root")), "Root");
348
349 let expected = "
350Root
351└── Key: value";
352 assert_eq!(
353 render(|w| {
354 w.header("Root")?;
355 w.leaf("Key", &"value", true)
356 }),
357 expected.strip_prefix('\n').unwrap()
358 );
359 }
360
361 #[test]
362 fn leaf_connector_depends_on_sibling_position() {
363 let expected = "
364├── First: 1
365└── Last: 2";
366 assert_eq!(
367 render(|w| {
368 w.leaf("First", &1, false)?;
369 w.leaf("Last", &2, true)
370 }),
371 expected.strip_prefix('\n').unwrap()
372 );
373 }
374
375 #[test]
376 fn leaf_indents_multi_line_value_below_its_label() {
377 let expected = "
378└── Label:
379 line1
380 line2";
381 assert_eq!(
382 render(|w| w.leaf("Label", &"line1\nline2", true)),
383 expected.strip_prefix('\n').unwrap()
384 );
385
386 let expected = "
387├── Label:
388│ line1
389│ line2
390└── Last: x";
391 assert_eq!(
392 render(|w| {
393 w.leaf("Label", &"line1\nline2", false)?;
394 w.leaf("Last", &"x", true)
395 }),
396 expected.strip_prefix('\n').unwrap()
397 );
398 }
399
400 #[test]
401 fn branch_extends_prefix_for_children() {
402 let expected = "
403└── Parent
404 └── Child: v";
405 assert_eq!(
406 render(|w| w.branch("Parent", true, |w| w.leaf("Child", &"v", true))),
407 expected.strip_prefix('\n').unwrap()
408 );
409
410 let expected = "
411├── Parent
412│ └── Child: v
413└── Last: x";
414 assert_eq!(
415 render(|w| {
416 w.branch("Parent", false, |w| w.leaf("Child", &"v", true))?;
417 w.leaf("Last", &"x", true)
418 }),
419 expected.strip_prefix('\n').unwrap()
420 );
421 }
422
423 #[test]
424 fn child_appends_header_to_the_label_line() {
425 let expected = "
426└── Origin: Point
427 ├── X: 1
428 └── Y: 2";
429 assert_eq!(
430 render(|w| w.child("Origin", &Point { x: 1, y: 2 }, true)),
431 expected.strip_prefix('\n').unwrap()
432 );
433 }
434
435 #[test]
436 fn child_omits_header_equal_to_the_label() {
437 let expected = "
438└── Point
439 ├── X: 1
440 └── Y: 2";
441 assert_eq!(
442 render(|w| w.child("Point", &Point { x: 1, y: 2 }, true)),
443 expected.strip_prefix('\n').unwrap()
444 );
445 }
446
447 #[test]
448 fn inline_child_renders_fields_without_a_level() {
449 let expected = "
450Origin
451├── X: 1
452└── Y: 2";
453 assert_eq!(
454 render(|w| {
455 w.header("Origin")?;
456 w.inline_child(&Point { x: 1, y: 2 })
457 }),
458 expected.strip_prefix('\n').unwrap()
459 );
460 }
461
462 #[test]
463 fn leaves_writes_one_node_per_item() {
464 let empty: &[u8] = &[];
465 assert_eq!(render(|w| w.leaves("Items", empty, true)), "└── Items: []");
466 let expected = "
467└── Items
468 ├── 0: 10
469 └── 1: 20";
470 assert_eq!(
471 render(|w| w.leaves("Items", [10, 20], true)),
472 expected.strip_prefix('\n').unwrap()
473 );
474 }
475
476 #[test]
477 fn leaves_accepts_any_collection() {
478 let set = std::collections::BTreeSet::from([20, 10]);
479 assert_eq!(
480 render(|w| w.leaves("Items", std::collections::BTreeSet::<u8>::new(), true)),
481 "└── Items: []"
482 );
483 let expected = "
484└── Items
485 ├── 0: 10
486 └── 1: 20";
487 assert_eq!(
488 render(|w| w.leaves("Items", &set, true)),
489 expected.strip_prefix('\n').unwrap()
490 );
491 }
492
493 #[test]
494 fn children_renders_indexed_sub_trees() {
495 let empty: &[Point] = &[];
496 assert_eq!(
497 render(|w| w.children("Points", empty, true)),
498 "└── Points: []"
499 );
500 let expected = "
501└── Points
502 └── 0: Point
503 ├── X: 1
504 └── Y: 2";
505 assert_eq!(
506 render(|w| w.children("Points", &[Point { x: 1, y: 2 }], true)),
507 expected.strip_prefix('\n').unwrap()
508 );
509 }
510
511 #[test]
512 fn option_leaf_renders_value_or_none() {
513 assert_eq!(
514 render(|w| w.option_leaf("Opt", &Some(5), true)),
515 "└── Opt: 5"
516 );
517 assert_eq!(
518 render(|w| w.option_leaf("Opt", &None::<u8>, true)),
519 "└── Opt: None"
520 );
521 }
522
523 #[test]
524 fn option_child_renders_sub_tree_or_none() {
525 let expected = "
526└── Origin: Point
527 ├── X: 1
528 └── Y: 2";
529 assert_eq!(
530 render(|w| w.option_child("Origin", &Some(Point { x: 1, y: 2 }), true)),
531 expected.strip_prefix('\n').unwrap()
532 );
533 assert_eq!(
534 render(|w| w.option_child("Origin", &None::<Point>, true)),
535 "└── Origin: None"
536 );
537 }
538
539 #[test]
540 fn base64_leaves_encodes_each_entry() {
541 assert_eq!(
542 render(|w| w.base64_leaves("Data", &[], true)),
543 "└── Data: []"
544 );
545 let expected = "
546└── Data
547 └── 0: AQID";
548 assert_eq!(
549 render(|w| w.base64_leaves("Data", &[vec![1, 2, 3]], true)),
550 expected.strip_prefix('\n').unwrap()
551 );
552 }
553
554 #[test]
555 fn impl_tree_display_generates_display_from_fmt_tree() {
556 let expected = "
557Point
558├── X: 1
559└── Y: 2";
560 assert_eq!(
561 Point { x: 1, y: 2 }.to_string(),
562 expected.strip_prefix('\n').unwrap()
563 );
564 }
565
566 fn sample_transaction() -> crate::Transaction {
567 use crate::transaction::*;
568
569 Transaction::V1(TransactionV1 {
570 kind: TransactionKind::Programmable(ProgrammableTransaction {
571 inputs: vec![Input::Pure(vec![1, 2, 3])],
572 commands: vec![Command::SplitCoins(SplitCoins {
573 coin: Argument::Gas,
574 amounts: vec![Argument::Input(0)],
575 })],
576 }),
577 sender: crate::Address::ZERO,
578 gas_payment: GasPayment {
579 objects: vec![crate::ObjectReference::new(
580 crate::ObjectId::ZERO,
581 crate::Version::from_u64(42),
582 crate::ObjectDigest::ZERO,
583 )],
584 owner: crate::Address::ZERO,
585 price: 1000,
586 budget: 5_000_000,
587 },
588 expiration: TransactionExpiration::None,
589 })
590 }
591
592 #[test]
593 fn transaction_renders_as_nested_tree() {
594 let expected = "
595Transaction: Transaction V1
596├── Kind: Programmable Transaction
597│ ├── Inputs
598│ │ └── 0: Input: Pure
599│ │ └── Value: 010203
600│ └── Commands
601│ └── 0: Command: Split Coins
602│ ├── Coin: Gas
603│ └── Amounts
604│ └── 0: Input(0)
605├── Sender: 0x0000000000000000000000000000000000000000000000000000000000000000
606├── Gas Payment
607│ ├── Objects
608│ │ └── 0: Object Reference
609│ │ ├── Object ID: 0x0000000000000000000000000000000000000000000000000000000000000000
610│ │ ├── Version: 42
611│ │ └── Digest: 11111111111111111111111111111111
612│ ├── Owner: 0x0000000000000000000000000000000000000000000000000000000000000000
613│ ├── Price: 1000
614│ └── Budget: 5000000
615└── Expiration: None";
616
617 assert_eq!(
618 sample_transaction().to_string(),
619 expected.strip_prefix('\n').unwrap()
620 );
621 }
622
623 #[test]
624 fn tree_typed_field_renders_as_indented_sub_tree() {
625 use crate::crypto::{
626 Ed25519PublicKey, Ed25519Signature, MultisigAggregatedSignature, MultisigCommittee,
627 MultisigMember, MultisigMemberSignature,
628 };
629
630 let committee = MultisigCommittee::new_unchecked(
631 vec![MultisigMember::new(Ed25519PublicKey::new([0; 32]), 1)],
632 1,
633 );
634 let signature = MultisigAggregatedSignature::new_unchecked(
635 vec![MultisigMemberSignature::Ed25519(Ed25519Signature::new(
636 [0; 64],
637 ))],
638 1,
639 committee,
640 );
641
642 let expected = "
643Multisig Aggregated Signature
644├── Committee: Multisig Committee
645│ ├── Members
646│ │ └── 0: Multisig Member
647│ │ ├── Public Key: Ed25519PublicKey(AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=)
648│ │ └── Weight: 1
649│ └── Threshold: 1
650├── Signatures
651│ └── 0: Multisig Member Signature: Ed25519Signature(AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==)
652└── Bitmap: 1";
653
654 assert_eq!(signature.to_string(), expected.strip_prefix('\n').unwrap());
655 }
656
657 #[test]
658 fn multi_line_leaf_value_is_indented_under_its_label() {
659 use crate::crypto::{Intent, IntentAppId, IntentMessage, IntentScope, IntentVersion};
660
661 let message = IntentMessage::new(
662 Intent::new(
663 IntentScope::TransactionData,
664 IntentVersion::V0,
665 IntentAppId::Iota,
666 ),
667 sample_transaction(),
668 );
669
670 let expected = "
671Intent Message
672├── Intent
673│ ├── Scope: TransactionData
674│ ├── Version: V0
675│ └── App ID: Iota
676└── Value:
677 Transaction: Transaction V1
678 ├── Kind: Programmable Transaction
679 │ ├── Inputs
680 │ │ └── 0: Input: Pure
681 │ │ └── Value: 010203
682 │ └── Commands
683 │ └── 0: Command: Split Coins
684 │ ├── Coin: Gas
685 │ └── Amounts
686 │ └── 0: Input(0)
687 ├── Sender: 0x0000000000000000000000000000000000000000000000000000000000000000
688 ├── Gas Payment
689 │ ├── Objects
690 │ │ └── 0: Object Reference
691 │ │ ├── Object ID: 0x0000000000000000000000000000000000000000000000000000000000000000
692 │ │ ├── Version: 42
693 │ │ └── Digest: 11111111111111111111111111111111
694 │ ├── Owner: 0x0000000000000000000000000000000000000000000000000000000000000000
695 │ ├── Price: 1000
696 │ └── Budget: 5000000
697 └── Expiration: None";
698
699 assert_eq!(message.to_string(), expected.strip_prefix('\n').unwrap());
700 }
701}