facet_pretty/
printer.rs

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