facet_pretty/printer.rs
1//! Pretty printer implementation for Facet types
2
3use alloc::collections::VecDeque;
4use core::{
5 fmt::{self, Write},
6 hash::{Hash, Hasher},
7 str,
8};
9use std::{collections::HashMap, hash::DefaultHasher};
10use yansi::Paint as _;
11
12use facet_core::{Def, Facet, FieldFlags, StructKind, TypeNameOpts};
13use facet_reflect::{Peek, ValueId};
14
15use crate::color::ColorGenerator;
16
17/// A formatter for pretty-printing Facet types
18pub struct PrettyPrinter {
19 indent_size: usize,
20 max_depth: Option<usize>,
21 color_generator: ColorGenerator,
22 use_colors: bool,
23 list_u8_as_bytes: bool,
24}
25
26impl Default for PrettyPrinter {
27 fn default() -> Self {
28 Self {
29 indent_size: 2,
30 max_depth: None,
31 color_generator: ColorGenerator::default(),
32 use_colors: true,
33 list_u8_as_bytes: true,
34 }
35 }
36}
37
38/// Stack state for iterative formatting
39enum StackState {
40 Start,
41 ProcessStructField { field_index: usize },
42 ProcessListItem { item_index: usize },
43 ProcessBytesItem { item_index: usize },
44 ProcessMapEntry,
45 Finish,
46 OptionFinish,
47}
48
49/// Stack item for iterative traversal
50struct StackItem<'a> {
51 value: Peek<'a>,
52 format_depth: usize,
53 type_depth: usize,
54 state: StackState,
55}
56
57impl PrettyPrinter {
58 /// Create a new PrettyPrinter with default settings
59 pub fn new() -> Self {
60 Self::default()
61 }
62
63 /// Set the indentation size
64 pub fn with_indent_size(mut self, size: usize) -> Self {
65 self.indent_size = size;
66 self
67 }
68
69 /// Set the maximum depth for recursive printing
70 pub fn with_max_depth(mut self, depth: usize) -> Self {
71 self.max_depth = Some(depth);
72 self
73 }
74
75 /// Set the color generator
76 pub fn with_color_generator(mut self, generator: ColorGenerator) -> Self {
77 self.color_generator = generator;
78 self
79 }
80
81 /// Enable or disable colors
82 pub fn with_colors(mut self, use_colors: bool) -> Self {
83 self.use_colors = use_colors;
84 self
85 }
86
87 /// Format a value to a string
88 pub fn format<'a, T: Facet<'a>>(&self, value: &'a T) -> String {
89 let value = Peek::new(value);
90
91 let mut output = String::new();
92 self.format_peek_internal(value, &mut output, &mut HashMap::new())
93 .expect("Formatting failed");
94
95 output
96 }
97
98 /// Format a value to a formatter
99 pub fn format_to<'a, T: Facet<'a>>(
100 &self,
101 value: &'a T,
102 f: &mut fmt::Formatter<'_>,
103 ) -> fmt::Result {
104 let value = Peek::new(value);
105 self.format_peek_internal(value, f, &mut HashMap::new())
106 }
107
108 /// Format a value to a string
109 pub fn format_peek(&self, value: Peek<'_>) -> String {
110 let mut output = String::new();
111 self.format_peek_internal(value, &mut output, &mut HashMap::new())
112 .expect("Formatting failed");
113 output
114 }
115
116 /// Internal method to format a Peek value
117 pub(crate) fn format_peek_internal(
118 &self,
119 initial_value: Peek<'_>,
120 f: &mut impl Write,
121 visited: &mut HashMap<ValueId, usize>,
122 ) -> fmt::Result {
123 // Create a queue for our stack items
124 let mut stack = VecDeque::new();
125
126 // Push the initial item
127 stack.push_back(StackItem {
128 value: initial_value,
129 format_depth: 0,
130 type_depth: 0,
131 state: StackState::Start,
132 });
133
134 // Process items until the stack is empty
135 while let Some(mut item) = stack.pop_back() {
136 match item.state {
137 StackState::Start => {
138 // Check if we've reached the maximum depth
139 if let Some(max_depth) = self.max_depth {
140 if item.format_depth > max_depth {
141 self.write_punctuation(f, "[")?;
142 write!(f, "...")?;
143 continue;
144 }
145 }
146
147 // Check for cycles - if we've seen this value before at a different type_depth
148 if let Some(&ptr_type_depth) = visited.get(&item.value.id()) {
149 // If the current type_depth is significantly deeper than when we first saw this value,
150 // we have a true cycle, not just a transparent wrapper
151 if item.type_depth > ptr_type_depth + 1 {
152 self.write_type_name(f, &item.value)?;
153 self.write_punctuation(f, " { ")?;
154 self.write_comment(
155 f,
156 &format!(
157 "/* cycle detected at {} (first seen at type_depth {}) */",
158 item.value.id(),
159 ptr_type_depth
160 ),
161 )?;
162 self.write_punctuation(f, " }")?;
163 continue;
164 }
165 } else {
166 // First time seeing this value, record its type_depth
167 visited.insert(item.value.id(), item.type_depth);
168 }
169
170 // Process based on the peek variant
171 match item.value.shape().def {
172 Def::Scalar(_def) => {
173 self.format_scalar(item.value, f)?;
174 }
175 Def::Option(_def) => {
176 let option = item.value.into_option().unwrap();
177
178 // Print the Option name
179 self.write_type_name(f, &item.value)?;
180
181 if option.is_some() {
182 self.write_punctuation(f, "::Some(")?;
183
184 if let Some(inner_value) = option.value() {
185 // Create a custom stack item for Option::Some value
186 let start_item = StackItem {
187 value: inner_value,
188 format_depth: item.format_depth,
189 type_depth: item.type_depth + 1,
190 state: StackState::Start,
191 };
192
193 // Add a special close parenthesis item
194 let close_paren_item = StackItem {
195 value: item.value,
196 format_depth: item.format_depth,
197 type_depth: item.type_depth,
198 state: StackState::OptionFinish,
199 };
200
201 // Process the value first, then handle closing
202 stack.push_back(close_paren_item);
203 stack.push_back(start_item);
204 }
205
206 // Skip to next item
207 continue;
208 } else {
209 self.write_punctuation(f, "::None")?;
210 }
211 }
212 Def::Struct(_def) => {
213 let struct_ = item.value.into_struct().unwrap();
214
215 // Get struct doc comments from the shape
216 let doc_comments = item.value.shape().doc;
217 if !doc_comments.is_empty() {
218 for line in doc_comments {
219 self.write_comment(f, &format!("///{}", line))?;
220 writeln!(f)?;
221 }
222 }
223
224 // Print the struct name
225 self.write_type_name(f, &item.value)?;
226 self.write_punctuation(f, " {")?;
227
228 if struct_.field_count() == 0 {
229 self.write_punctuation(f, "}")?;
230 continue;
231 }
232
233 writeln!(f)?;
234
235 // Push back the item with the next state to continue processing fields
236 item.state = StackState::ProcessStructField { field_index: 0 };
237 item.format_depth += 1;
238 stack.push_back(item);
239 }
240 Def::List(_) => {
241 let list = item.value.into_list().unwrap();
242 // When recursing into a list, always increment format_depth
243 // Only increment type_depth if we're moving to a different address
244 let new_type_depth =
245 // Incrementing type_depth for all list operations
246 item.type_depth + 1; // Always increment type_depth for list operations
247
248 // Print the list name
249 self.write_type_name(f, &item.value)?;
250
251 if list.def().t().is_type::<u8>() && self.list_u8_as_bytes {
252 // Push back the item with the next state to continue processing list items
253 item.state = StackState::ProcessBytesItem { item_index: 0 };
254 writeln!(f)?;
255 write!(f, " ")?;
256
257 // TODO: write all the bytes here instead?
258 } else {
259 // Push back the item with the next state to continue processing list items
260 item.state = StackState::ProcessListItem { item_index: 0 };
261 self.write_punctuation(f, " [")?;
262 writeln!(f)?;
263 }
264
265 item.format_depth += 1;
266 item.type_depth = new_type_depth;
267 stack.push_back(item);
268 }
269 Def::Map(_) => {
270 let _map = item.value.into_map().unwrap();
271 // Print the map name
272 self.write_type_name(f, &item.value)?;
273 self.write_punctuation(f, " {")?;
274 writeln!(f)?;
275
276 // Push back the item with the next state to continue processing map
277 item.state = StackState::ProcessMapEntry;
278 item.format_depth += 1;
279 // When recursing into a map, always increment format_depth
280 item.type_depth += 1; // Always increment type_depth for map operations
281 stack.push_back(item);
282 }
283 Def::Enum(_enum) => {
284 // When recursing into an enum, increment format_depth
285 // Only increment type_depth if we're moving to a different address
286 let enum_peek = item.value.into_enum().unwrap();
287
288 // Get the active variant
289 let variant = enum_peek.active_variant();
290
291 // Get enum and variant doc comments
292 let doc_comments = item.value.shape().doc;
293
294 // Display doc comments before the type name
295 for line in doc_comments {
296 self.write_comment(f, &format!("///{}", line))?;
297 writeln!(f)?;
298 }
299
300 // Show variant docs
301 for line in variant.doc {
302 self.write_comment(f, &format!("///{}", line))?;
303 writeln!(f)?;
304 }
305
306 // Print the enum name and separator
307 self.write_type_name(f, &item.value)?;
308 self.write_punctuation(f, "::")?;
309
310 // Variant docs are already handled above
311
312 // Get the active variant name
313 let variant = enum_peek.active_variant();
314
315 // Apply color for variant name
316 if self.use_colors {
317 write!(f, "{}", variant.name.bold())?;
318 } else {
319 write!(f, "{}", variant.name)?;
320 }
321
322 // Process the variant fields based on the variant kind
323 match variant.data.kind {
324 StructKind::Unit => {
325 // Unit variant has no fields, nothing more to print
326 }
327 StructKind::Tuple => {
328 // Tuple variant, print the fields like a tuple
329 self.write_punctuation(f, "(")?;
330
331 // Check if there are any fields to print
332 if variant.data.fields.is_empty() {
333 self.write_punctuation(f, ")")?;
334 continue;
335 }
336
337 writeln!(f)?;
338
339 // Push back item to process fields
340 item.state = StackState::ProcessStructField { field_index: 0 };
341 item.format_depth += 1;
342 stack.push_back(item);
343 }
344 StructKind::Struct => {
345 // Struct variant, print the fields like a struct
346 self.write_punctuation(f, " {")?;
347
348 // Check if there are any fields to print
349 let has_fields = !variant.data.fields.is_empty();
350
351 if !has_fields {
352 self.write_punctuation(f, " }")?;
353 continue;
354 }
355
356 writeln!(f)?;
357
358 // Push back item to process fields
359 item.state = StackState::ProcessStructField { field_index: 0 };
360 item.format_depth += 1;
361 stack.push_back(item);
362 }
363 _ => {
364 // Other variant kinds that might be added in the future
365 write!(f, " /* unsupported variant kind */")?;
366 }
367 }
368 }
369 Def::FunctionPointer(_) => {
370 self.write_type_name(f, &item.value)?;
371 }
372 _ => {
373 write!(f, "unsupported peek variant: {:?}", item.value)?;
374 }
375 }
376 }
377 StackState::ProcessStructField { field_index } => {
378 // Handle both struct and enum fields
379 if let Def::Struct(struct_) = item.value.shape().def {
380 let peek_struct = item.value.into_struct().unwrap();
381 if field_index >= struct_.fields.len() {
382 // All fields processed, write closing brace
383 write!(
384 f,
385 "{:width$}{}",
386 "",
387 self.style_punctuation("}"),
388 width = (item.format_depth - 1) * self.indent_size
389 )?;
390 continue;
391 }
392
393 let field = struct_.fields[field_index];
394 let field_value = peek_struct.field(field_index).unwrap();
395
396 // Field doc comment
397 if !field.doc.is_empty() {
398 // Only add new line if not the first field
399 if field_index > 0 {
400 writeln!(f)?;
401 }
402 // Hard-code consistent indentation for doc comments
403 for line in field.doc {
404 // Use exactly the same indentation as fields (2 spaces)
405 write!(
406 f,
407 "{:width$}",
408 "",
409 width = item.format_depth * self.indent_size
410 )?;
411 self.write_comment(f, &format!("///{}", line))?;
412 writeln!(f)?;
413 }
414 }
415
416 // Field name
417 write!(
418 f,
419 "{:width$}",
420 "",
421 width = item.format_depth * self.indent_size
422 )?;
423 self.write_field_name(f, field.name)?;
424 self.write_punctuation(f, ": ")?;
425
426 // Check if field is sensitive
427 if field.flags.contains(FieldFlags::SENSITIVE) {
428 // Field value is sensitive, use write_redacted
429 self.write_redacted(f, "[REDACTED]")?;
430 self.write_punctuation(f, ",")?;
431 writeln!(f)?;
432
433 item.state = StackState::ProcessStructField {
434 field_index: field_index + 1,
435 };
436 stack.push_back(item);
437 } else {
438 // Field value is not sensitive, format normally
439 // Push back current item to continue after formatting field value
440 item.state = StackState::ProcessStructField {
441 field_index: field_index + 1,
442 };
443
444 let finish_item = StackItem {
445 value: field_value,
446 format_depth: item.format_depth,
447 type_depth: item.type_depth + 1,
448 state: StackState::Finish,
449 };
450 let start_item = StackItem {
451 value: field_value,
452 format_depth: item.format_depth,
453 type_depth: item.type_depth + 1,
454 state: StackState::Start,
455 };
456
457 stack.push_back(item);
458 stack.push_back(finish_item);
459 stack.push_back(start_item);
460 }
461 } else if let Def::Enum(_def) = item.value.shape().def {
462 let enum_val = item.value.into_enum().unwrap();
463
464 let variant = enum_val.active_variant();
465 if field_index >= variant.data.fields.len() {
466 // Determine variant kind to use the right closing delimiter
467 match variant.data.kind {
468 StructKind::Tuple => {
469 // Close tuple variant with )
470 write!(
471 f,
472 "{:width$}{}",
473 "",
474 self.style_punctuation(")"),
475 width = (item.format_depth - 1) * self.indent_size
476 )?;
477 }
478 StructKind::Struct => {
479 // Close struct variant with }
480 write!(
481 f,
482 "{:width$}{}",
483 "",
484 self.style_punctuation("}"),
485 width = (item.format_depth - 1) * self.indent_size
486 )?;
487 }
488 _ => {}
489 }
490 continue;
491 }
492
493 let field = variant.data.fields[field_index];
494 let field_value = enum_val.field(field_index).unwrap();
495
496 // Add field doc comments if available
497 // Only add new line if not the first field
498 write!(
499 f,
500 "{:width$}",
501 "",
502 width = item.format_depth * self.indent_size
503 )?;
504
505 if !field.doc.is_empty() {
506 for line in field.doc {
507 self.write_comment(f, &format!("///{}", line))?;
508 write!(
509 f,
510 "\n{:width$}",
511 "",
512 width = item.format_depth * self.indent_size
513 )?;
514 }
515 }
516
517 // For struct variants, print field name
518 if let StructKind::Struct = enum_val.active_variant().data.kind {
519 self.write_field_name(f, field.name)?;
520 self.write_punctuation(f, ": ")?;
521 }
522
523 // Set up to process the next field after this one
524 item.state = StackState::ProcessStructField {
525 field_index: field_index + 1,
526 };
527
528 // Create finish and start items for processing the field value
529 let finish_item = StackItem {
530 value: field_value,
531 format_depth: item.format_depth,
532 type_depth: item.type_depth + 1,
533 state: StackState::Finish,
534 };
535 let start_item = StackItem {
536 value: field_value,
537 format_depth: item.format_depth,
538 type_depth: item.type_depth + 1,
539 state: StackState::Start,
540 };
541
542 // Push items to stack in the right order
543 stack.push_back(item);
544 stack.push_back(finish_item);
545 stack.push_back(start_item);
546 }
547 }
548 StackState::ProcessListItem { item_index } => {
549 let list = item.value.into_list().unwrap();
550 if item_index >= list.len() {
551 // All items processed, write closing bracket
552 write!(
553 f,
554 "{:width$}",
555 "",
556 width = (item.format_depth - 1) * self.indent_size
557 )?;
558 self.write_punctuation(f, "]")?;
559 continue;
560 }
561
562 // Indent
563 write!(
564 f,
565 "{:width$}",
566 "",
567 width = item.format_depth * self.indent_size
568 )?;
569
570 // Push back current item to continue after formatting list item
571 item.state = StackState::ProcessListItem {
572 item_index: item_index + 1,
573 };
574 let next_format_depth = item.format_depth;
575 let next_type_depth = item.type_depth + 1;
576 stack.push_back(item);
577
578 // Push list item to format first
579 let list_item = list.get(item_index).unwrap();
580 stack.push_back(StackItem {
581 value: list_item,
582 format_depth: next_format_depth,
583 type_depth: next_type_depth,
584 state: StackState::Finish,
585 });
586
587 // When we push a list item to format, we need to process it from the beginning
588 stack.push_back(StackItem {
589 value: list_item,
590 format_depth: next_format_depth,
591 type_depth: next_type_depth,
592 state: StackState::Start, // Use Start state to properly process the item
593 });
594 }
595 StackState::ProcessBytesItem { item_index } => {
596 let list = item.value.into_list().unwrap();
597 if item_index >= list.len() {
598 // All items processed, write closing bracket
599 write!(
600 f,
601 "{:width$}",
602 "",
603 width = (item.format_depth - 1) * self.indent_size
604 )?;
605 continue;
606 }
607
608 // On the first byte, write the opening byte sequence indicator
609 if item_index == 0 {
610 write!(f, " ")?;
611 }
612
613 // Only display 16 bytes per line
614 if item_index > 0 && item_index % 16 == 0 {
615 writeln!(f)?;
616 write!(
617 f,
618 "{:width$}",
619 "",
620 width = item.format_depth * self.indent_size
621 )?;
622 } else if item_index > 0 {
623 write!(f, " ")?;
624 }
625
626 // Get the byte
627 let byte_value = list.get(item_index).unwrap();
628 // Get the byte value as u8
629 let byte = byte_value.get::<u8>().unwrap_or(&0);
630
631 // Generate a color for this byte based on its value
632 let mut hasher = DefaultHasher::new();
633 byte.hash(&mut hasher);
634 let hash = hasher.finish();
635 let color = self.color_generator.generate_color(hash);
636
637 // Apply color if needed
638 if self.use_colors {
639 write!(f, "\x1b[38;2;{};{};{}m", color.r, color.g, color.b)?;
640 }
641
642 // Display the byte in hex format
643 write!(f, "{:02x}", *byte)?;
644
645 // Reset color if needed
646 // Reset color already handled by stylize
647
648 // Push back current item to continue after formatting byte
649 item.state = StackState::ProcessBytesItem {
650 item_index: item_index + 1,
651 };
652 stack.push_back(item);
653 }
654 StackState::ProcessMapEntry => {
655 // TODO: Implement proper map iteration when available in facet
656
657 // Indent
658 write!(
659 f,
660 "{:width$}",
661 "",
662 width = item.format_depth * self.indent_size
663 )?;
664 write!(f, "{}", self.style_comment("/* Map contents */"))?;
665 writeln!(f)?;
666
667 // Closing brace with proper indentation
668 write!(
669 f,
670 "{:width$}{}",
671 "",
672 self.style_punctuation("}"),
673 width = (item.format_depth - 1) * self.indent_size
674 )?;
675 }
676 StackState::Finish => {
677 // Add comma and newline for struct fields and list items
678 self.write_punctuation(f, ",")?;
679 writeln!(f)?;
680 }
681 StackState::OptionFinish => {
682 // Just close the Option::Some parenthesis, with no comma
683 self.write_punctuation(f, ")")?;
684 }
685 }
686 }
687
688 Ok(())
689 }
690
691 /// Format a scalar value
692 fn format_scalar(&self, value: Peek, f: &mut impl Write) -> fmt::Result {
693 // Generate a color for this shape
694 let mut hasher = DefaultHasher::new();
695 value.shape().def.hash(&mut hasher);
696 let hash = hasher.finish();
697 let color = self.color_generator.generate_color(hash);
698
699 // Display the value
700 struct DisplayWrapper<'a>(&'a Peek<'a>);
701
702 impl fmt::Display for DisplayWrapper<'_> {
703 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
704 if self.0.shape().is_display() {
705 write!(f, "{}", self.0)?;
706 } else if self.0.shape().is_debug() {
707 write!(f, "{:?}", self.0)?;
708 } else {
709 write!(f, "{}", self.0.shape())?;
710 write!(f, "(⋯)")?;
711 }
712 Ok(())
713 }
714 }
715
716 // Apply color if needed and display
717 if self.use_colors {
718 // We need to use direct ANSI codes for RGB colors
719 write!(
720 f,
721 "\x1b[38;2;{};{};{}m{}",
722 color.r,
723 color.g,
724 color.b,
725 DisplayWrapper(&value)
726 )?;
727 write!(f, "\x1b[0m")?;
728 } else {
729 write!(f, "{}", DisplayWrapper(&value))?;
730 }
731
732 Ok(())
733 }
734
735 /// Write styled type name to formatter
736 fn write_type_name<W: fmt::Write>(&self, f: &mut W, peek: &Peek) -> fmt::Result {
737 struct TypeNameWriter<'a, 'b: 'a>(&'b Peek<'a>);
738
739 impl core::fmt::Display for TypeNameWriter<'_, '_> {
740 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
741 self.0.type_name(f, TypeNameOpts::infinite())
742 }
743 }
744 let type_name = TypeNameWriter(peek);
745
746 if self.use_colors {
747 write!(f, "{}", type_name.bold())
748 } else {
749 write!(f, "{}", type_name)
750 }
751 }
752
753 /// Style a type name and return it as a string
754 #[allow(dead_code)]
755 fn style_type_name(&self, peek: &Peek) -> String {
756 let mut result = String::new();
757 self.write_type_name(&mut result, peek).unwrap();
758 result
759 }
760
761 /// Write styled field name to formatter
762 fn write_field_name<W: fmt::Write>(&self, f: &mut W, name: &str) -> fmt::Result {
763 if self.use_colors {
764 // Use cyan color for field names (approximating original RGB color)
765 write!(f, "{}", name.cyan())
766 } else {
767 write!(f, "{}", name)
768 }
769 }
770
771 /// Write styled punctuation to formatter
772 fn write_punctuation<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
773 if self.use_colors {
774 write!(f, "{}", text.dim())
775 } else {
776 write!(f, "{}", text)
777 }
778 }
779
780 /// Style punctuation and return it as a string
781 fn style_punctuation(&self, text: &str) -> String {
782 let mut result = String::new();
783 self.write_punctuation(&mut result, text).unwrap();
784 result
785 }
786
787 /// Write styled comment to formatter
788 fn write_comment<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
789 if self.use_colors {
790 write!(f, "{}", text.dim())
791 } else {
792 write!(f, "{}", text)
793 }
794 }
795
796 /// Style a comment and return it as a string
797 fn style_comment(&self, text: &str) -> String {
798 let mut result = String::new();
799 self.write_comment(&mut result, text).unwrap();
800 result
801 }
802
803 /// Write styled redacted value to formatter
804 fn write_redacted<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
805 if self.use_colors {
806 // Use bright red and bold for redacted values
807 write!(f, "{}", text.bright_red().bold())
808 } else {
809 write!(f, "{}", text)
810 }
811 }
812
813 /// Style a redacted value and return it as a string
814 #[allow(dead_code)]
815 fn style_redacted(&self, text: &str) -> String {
816 let mut result = String::new();
817 self.write_redacted(&mut result, text).unwrap();
818 result
819 }
820}
821
822#[cfg(test)]
823mod tests {
824 use super::*;
825
826 // Basic tests for the PrettyPrinter
827 #[test]
828 fn test_pretty_printer_default() {
829 let printer = PrettyPrinter::default();
830 assert_eq!(printer.indent_size, 2);
831 assert_eq!(printer.max_depth, None);
832 assert!(printer.use_colors);
833 }
834
835 #[test]
836 fn test_pretty_printer_with_methods() {
837 let printer = PrettyPrinter::new()
838 .with_indent_size(4)
839 .with_max_depth(3)
840 .with_colors(false);
841
842 assert_eq!(printer.indent_size, 4);
843 assert_eq!(printer.max_depth, Some(3));
844 assert!(!printer.use_colors);
845 }
846}