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_peek::Peek;
11use facet_trait::{Facet, ShapeExt};
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                        _ => {
250                            writeln!(f, "unsupported peek variant: {:?}", item.peek)?;
251                        }
252                    }
253                }
254                StackState::ProcessStructField { field_index } => {
255                    if let Peek::Struct(struct_) = item.peek {
256                        let fields: Vec<_> = struct_.fields_with_metadata().collect();
257
258                        if field_index >= fields.len() {
259                            // All fields processed, write closing brace
260                            write!(
261                                f,
262                                "{:width$}{}",
263                                "",
264                                self.style_punctuation("}"),
265                                width = (item.format_depth - 1) * self.indent_size
266                            )?;
267                            continue;
268                        }
269
270                        let (_, field_name, field_value, flags) = &fields[field_index];
271
272                        // Indent
273                        write!(
274                            f,
275                            "{:width$}",
276                            "",
277                            width = item.format_depth * self.indent_size
278                        )?;
279
280                        // Field name
281                        self.write_field_name(f, field_name)?;
282                        self.write_punctuation(f, ": ")?;
283
284                        // Check if field is sensitive
285                        if flags.contains(facet_trait::FieldFlags::SENSITIVE) {
286                            // Field value is sensitive, use write_redacted
287                            self.write_redacted(f, "[REDACTED]")?;
288                            self.write_punctuation(f, ",")?;
289                            writeln!(f)?;
290
291                            // Process next field
292                            item.state = StackState::ProcessStructField {
293                                field_index: field_index + 1,
294                            };
295                            stack.push_back(item);
296                        } else {
297                            // Field value is not sensitive, format normally
298                            // Push back current item to continue after formatting field value
299                            item.state = StackState::ProcessStructField {
300                                field_index: field_index + 1,
301                            };
302
303                            let finish_item = StackItem {
304                                peek: *field_value,
305                                format_depth: item.format_depth,
306                                type_depth: item.type_depth + 1,
307                                state: StackState::Finish,
308                            };
309                            let start_item = StackItem {
310                                peek: *field_value,
311                                format_depth: item.format_depth,
312                                type_depth: item.type_depth + 1,
313                                state: StackState::Start,
314                            };
315
316                            stack.push_back(item);
317                            stack.push_back(finish_item);
318                            stack.push_back(start_item);
319                        }
320                    }
321                }
322                StackState::ProcessListItem { item_index } => {
323                    if let Peek::List(list) = item.peek {
324                        if item_index >= list.len() {
325                            // All items processed, write closing bracket
326                            write!(
327                                f,
328                                "{:width$}",
329                                "",
330                                width = (item.format_depth - 1) * self.indent_size
331                            )?;
332                            self.write_punctuation(f, "]")?;
333                            continue;
334                        }
335
336                        // Indent
337                        write!(
338                            f,
339                            "{:width$}",
340                            "",
341                            width = item.format_depth * self.indent_size
342                        )?;
343
344                        // Push back current item to continue after formatting list item
345                        item.state = StackState::ProcessListItem {
346                            item_index: item_index + 1,
347                        };
348                        let next_format_depth = item.format_depth;
349                        let next_type_depth = item.type_depth + 1;
350                        stack.push_back(item);
351
352                        // Push list item to format first
353                        let list_item = list.iter().nth(item_index).unwrap();
354                        stack.push_back(StackItem {
355                            peek: list_item,
356                            format_depth: next_format_depth,
357                            type_depth: next_type_depth,
358                            state: StackState::Finish,
359                        });
360
361                        // When we push a list item to format, we need to process it from the beginning
362                        stack.push_back(StackItem {
363                            peek: list_item,
364                            format_depth: next_format_depth,
365                            type_depth: next_type_depth,
366                            state: StackState::Start, // Use Start state to properly process the item
367                        });
368                    }
369                }
370                StackState::ProcessBytesItem { item_index } => {
371                    if let Peek::List(list) = item.peek {
372                        if item_index >= list.len() {
373                            // All items processed, write closing bracket
374                            write!(
375                                f,
376                                "{:width$}",
377                                "",
378                                width = (item.format_depth - 1) * self.indent_size
379                            )?;
380                            continue;
381                        }
382
383                        // On the first byte, write the opening byte sequence indicator
384                        if item_index == 0 {
385                            write!(f, " ")?;
386                        }
387
388                        // Only display 16 bytes per line
389                        if item_index > 0 && item_index % 16 == 0 {
390                            writeln!(f)?;
391                            write!(
392                                f,
393                                "{:width$}",
394                                "",
395                                width = item.format_depth * self.indent_size
396                            )?;
397                        } else if item_index > 0 {
398                            write!(f, " ")?;
399                        }
400
401                        // Get the byte
402                        if let Some(Peek::Value(value)) = list.iter().nth(item_index) {
403                            let byte = unsafe { value.data().read::<u8>() };
404
405                            // Generate a color for this byte based on its value
406                            let mut hasher = DefaultHasher::new();
407                            byte.hash(&mut hasher);
408                            let hash = hasher.finish();
409                            let color = self.color_generator.generate_color(hash);
410
411                            // Apply color if needed
412                            if self.use_colors {
413                                color.write_fg(f)?;
414                            }
415
416                            // Display the byte in hex format
417                            write!(f, "{:02x}", byte)?;
418
419                            // Reset color if needed
420                            if self.use_colors {
421                                ansi::write_reset(f)?;
422                            }
423                        } else {
424                            unreachable!()
425                        }
426
427                        // Push back current item to continue after formatting byte
428                        item.state = StackState::ProcessBytesItem {
429                            item_index: item_index + 1,
430                        };
431                        stack.push_back(item);
432                    }
433                }
434                StackState::ProcessMapEntry => {
435                    if let Peek::Map(_) = item.peek {
436                        // TODO: Implement proper map iteration when available in facet_peek
437
438                        // Indent
439                        write!(
440                            f,
441                            "{:width$}",
442                            "",
443                            width = item.format_depth * self.indent_size
444                        )?;
445                        write!(f, "{}", self.style_comment("/* Map contents */"))?;
446                        writeln!(f)?;
447
448                        // Closing brace with proper indentation
449                        write!(
450                            f,
451                            "{:width$}{}",
452                            "",
453                            self.style_punctuation("}"),
454                            width = (item.format_depth - 1) * self.indent_size
455                        )?;
456                    }
457                }
458                StackState::Finish => {
459                    // This state is reached after processing a field or list item
460                    // Add comma and newline for struct fields and list items
461                    self.write_punctuation(f, ",")?;
462                    writeln!(f)?;
463                }
464            }
465        }
466
467        Ok(())
468    }
469
470    /// Format a scalar value
471    fn format_value(&self, value: facet_peek::PeekValue, f: &mut impl Write) -> fmt::Result {
472        // Generate a color for this shape
473        let mut hasher = DefaultHasher::new();
474        value.shape().def.hash(&mut hasher);
475        let hash = hasher.finish();
476        let color = self.color_generator.generate_color(hash);
477
478        // Apply color if needed
479        if self.use_colors {
480            color.write_fg(f)?;
481        }
482
483        // Display the value
484        struct DisplayWrapper<'a>(&'a facet_peek::PeekValue<'a>);
485
486        impl fmt::Display for DisplayWrapper<'_> {
487            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
488                if self.0.display(f).is_none() {
489                    // If the value doesn't implement Display, use Debug
490                    if self.0.debug(f).is_none() {
491                        // If the value doesn't implement Debug either, just show the type name
492                        self.0.type_name(f, facet_trait::TypeNameOpts::infinite())?;
493                        write!(f, "(⋯)")?;
494                    }
495                }
496                Ok(())
497            }
498        }
499
500        write!(f, "{}", DisplayWrapper(&value))?;
501
502        // Reset color if needed
503        if self.use_colors {
504            ansi::write_reset(f)?;
505        }
506
507        Ok(())
508    }
509
510    /// Write styled type name to formatter
511    fn write_type_name<W: fmt::Write>(
512        &self,
513        f: &mut W,
514        peek: &facet_peek::PeekValue,
515    ) -> fmt::Result {
516        struct TypeNameWriter<'a, 'b: 'a>(&'b facet_peek::PeekValue<'a>);
517
518        impl core::fmt::Display for TypeNameWriter<'_, '_> {
519            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
520                self.0.type_name(f, facet_trait::TypeNameOpts::infinite())
521            }
522        }
523        let type_name = TypeNameWriter(peek);
524
525        if self.use_colors {
526            ansi::write_bold(f)?;
527            write!(f, "{}", type_name)?;
528            ansi::write_reset(f)
529        } else {
530            write!(f, "{}", type_name)
531        }
532    }
533
534    /// Style a type name and return it as a string
535    #[allow(dead_code)]
536    fn style_type_name(&self, peek: &facet_peek::PeekValue) -> String {
537        let mut result = String::new();
538        self.write_type_name(&mut result, peek).unwrap();
539        result
540    }
541
542    /// Write styled field name to formatter
543    fn write_field_name<W: fmt::Write>(&self, f: &mut W, name: &str) -> fmt::Result {
544        if self.use_colors {
545            ansi::write_rgb(f, 114, 160, 193)?;
546            write!(f, "{}", name)?;
547            ansi::write_reset(f)
548        } else {
549            write!(f, "{}", name)
550        }
551    }
552
553    /// Write styled punctuation to formatter
554    fn write_punctuation<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
555        if self.use_colors {
556            ansi::write_dim(f)?;
557            write!(f, "{}", text)?;
558            ansi::write_reset(f)
559        } else {
560            write!(f, "{}", text)
561        }
562    }
563
564    /// Style punctuation and return it as a string
565    fn style_punctuation(&self, text: &str) -> String {
566        let mut result = String::new();
567        self.write_punctuation(&mut result, text).unwrap();
568        result
569    }
570
571    /// Write styled comment to formatter
572    fn write_comment<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
573        if self.use_colors {
574            ansi::write_dim(f)?;
575            write!(f, "{}", text)?;
576            ansi::write_reset(f)
577        } else {
578            write!(f, "{}", text)
579        }
580    }
581
582    /// Style a comment and return it as a string
583    fn style_comment(&self, text: &str) -> String {
584        let mut result = String::new();
585        self.write_comment(&mut result, text).unwrap();
586        result
587    }
588
589    /// Write styled redacted value to formatter
590    fn write_redacted<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
591        if self.use_colors {
592            ansi::write_rgb(f, 224, 49, 49)?; // Use bright red for redacted values
593            ansi::write_bold(f)?;
594            write!(f, "{}", text)?;
595            ansi::write_reset(f)
596        } else {
597            write!(f, "{}", text)
598        }
599    }
600
601    /// Style a redacted value and return it as a string
602    #[allow(dead_code)]
603    fn style_redacted(&self, text: &str) -> String {
604        let mut result = String::new();
605        self.write_redacted(&mut result, text).unwrap();
606        result
607    }
608}
609
610#[cfg(test)]
611mod tests {
612    use super::*;
613
614    // Basic tests for the PrettyPrinter
615    #[test]
616    fn test_pretty_printer_default() {
617        let printer = PrettyPrinter::default();
618        assert_eq!(printer.indent_size, 2);
619        assert_eq!(printer.max_depth, None);
620        assert!(printer.use_colors);
621    }
622
623    #[test]
624    fn test_pretty_printer_with_methods() {
625        let printer = PrettyPrinter::new()
626            .with_indent_size(4)
627            .with_max_depth(3)
628            .with_colors(false);
629
630        assert_eq!(printer.indent_size, 4);
631        assert_eq!(printer.max_depth, Some(3));
632        assert!(!printer.use_colors);
633    }
634}