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;
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}
22
23impl Default for PrettyPrinter {
24    fn default() -> Self {
25        Self {
26            indent_size: 2,
27            max_depth: None,
28            color_generator: ColorGenerator::default(),
29            use_colors: true,
30        }
31    }
32}
33
34/// Stack state for iterative formatting
35enum StackState {
36    Start,
37    ProcessStructField { field_index: usize },
38    ProcessListItem { item_index: usize },
39    ProcessMapEntry,
40    Finish,
41}
42
43/// Stack item for iterative traversal
44struct StackItem<'a> {
45    peek: Peek<'a>,
46    format_depth: usize,
47    type_depth: usize,
48    state: StackState,
49}
50
51impl PrettyPrinter {
52    /// Create a new PrettyPrinter with default settings
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    /// Set the indentation size
58    pub fn with_indent_size(mut self, size: usize) -> Self {
59        self.indent_size = size;
60        self
61    }
62
63    /// Set the maximum depth for recursive printing
64    pub fn with_max_depth(mut self, depth: usize) -> Self {
65        self.max_depth = Some(depth);
66        self
67    }
68
69    /// Set the color generator
70    pub fn with_color_generator(mut self, generator: ColorGenerator) -> Self {
71        self.color_generator = generator;
72        self
73    }
74
75    /// Enable or disable colors
76    pub fn with_colors(mut self, use_colors: bool) -> Self {
77        self.use_colors = use_colors;
78        self
79    }
80
81    /// Format a value to a string
82    pub fn format<T: Facet>(&self, value: &T) -> String {
83        let peek = Peek::new(value);
84
85        let mut output = String::new();
86        self.format_peek_internal(peek, &mut output, 0, 0, &mut HashMap::new())
87            .expect("Formatting failed");
88
89        output
90    }
91
92    /// Format a value to a formatter
93    pub fn format_to<T: Facet>(&self, value: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        let peek = Peek::new(value);
95        self.format_peek_internal(peek, f, 0, 0, &mut HashMap::new())
96    }
97
98    /// Format a Peek value to a string
99    pub fn format_peek(&self, peek: Peek<'_>) -> String {
100        let mut output = String::new();
101        self.format_peek_internal(peek, &mut output, 0, 0, &mut HashMap::new())
102            .expect("Formatting failed");
103        output
104    }
105
106    /// Internal method to format a Peek value
107    pub(crate) fn format_peek_internal(
108        &self,
109        peek: Peek<'_>,
110        f: &mut impl Write,
111        format_depth: usize,
112        type_depth: usize,
113        visited: &mut HashMap<*const (), usize>,
114    ) -> fmt::Result {
115        // Create a queue for our stack items
116        let mut stack = VecDeque::new();
117
118        // Push the initial item
119        stack.push_back(StackItem {
120            peek,
121            format_depth,
122            type_depth,
123            state: StackState::Start,
124        });
125
126        // Process items until the stack is empty
127        while let Some(mut item) = stack.pop_back() {
128            match item.state {
129                StackState::Start => {
130                    // Check if we've reached the maximum depth
131                    if let Some(max_depth) = self.max_depth {
132                        if item.format_depth > max_depth {
133                            self.write_punctuation(f, "[")?;
134                            write!(f, "...")?;
135                            continue;
136                        }
137                    }
138
139                    // Get the data pointer for cycle detection
140                    let ptr = unsafe { item.peek.data().as_ptr() };
141
142                    // Check for cycles - if we've seen this pointer before at a different type_depth
143                    if let Some(&ptr_type_depth) = visited.get(&ptr) {
144                        // If the current type_depth is significantly deeper than when we first saw this pointer,
145                        // we have a true cycle, not just a transparent wrapper
146                        if item.type_depth > ptr_type_depth + 1 {
147                            self.write_type_name(f, &item.peek)?;
148                            self.write_punctuation(f, " { ")?;
149                            self.write_comment(
150                                f,
151                                &format!(
152                                    "/* cycle detected at {:p} (first seen at type_depth {}) */",
153                                    ptr, ptr_type_depth
154                                ),
155                            )?;
156                            self.write_punctuation(f, " }")?;
157                            continue;
158                        }
159                    } else {
160                        // First time seeing this pointer, record its type_depth
161                        visited.insert(ptr, item.type_depth);
162                    }
163
164                    // Process based on the peek variant
165                    match item.peek {
166                        Peek::Value(value) => {
167                            self.format_value(value, f)?;
168                        }
169                        Peek::Struct(struct_) => {
170                            // When recursing into a struct, always increment format_depth
171                            // Only increment type_depth if we're moving to a different address
172                            let new_type_depth =
173                                if core::ptr::eq(unsafe { struct_.data().as_ptr() }, ptr) {
174                                    item.type_depth // Same pointer, don't increment type_depth
175                                } else {
176                                    item.type_depth + 1 // Different pointer, increment type_depth
177                                };
178
179                            // Print the struct name
180                            self.write_type_name(f, &struct_)?;
181                            self.write_punctuation(f, " {")?;
182
183                            if struct_.field_count() == 0 {
184                                self.write_punctuation(f, " }")?;
185                                continue;
186                            }
187
188                            writeln!(f)?;
189
190                            // Push back the item with the next state to continue processing fields
191                            item.state = StackState::ProcessStructField { field_index: 0 };
192                            item.format_depth += 1;
193                            item.type_depth = new_type_depth;
194                            stack.push_back(item);
195                        }
196                        Peek::List(list) => {
197                            // When recursing into a list, always increment format_depth
198                            // Only increment type_depth if we're moving to a different address
199                            let new_type_depth =
200                                if core::ptr::eq(unsafe { list.data().as_ptr() }, ptr) {
201                                    item.type_depth // Same pointer, don't increment type_depth
202                                } else {
203                                    item.type_depth + 1 // Different pointer, increment type_depth
204                                };
205
206                            // Print the list name
207                            self.write_type_name(f, &list)?;
208                            self.write_punctuation(f, " [")?;
209                            writeln!(f)?;
210
211                            // Push back the item with the next state to continue processing list items
212                            item.state = StackState::ProcessListItem { item_index: 0 };
213                            item.format_depth += 1;
214                            item.type_depth = new_type_depth;
215                            stack.push_back(item);
216                        }
217                        Peek::Map(map) => {
218                            // Print the map name
219                            self.write_type_name(f, &map)?;
220                            self.write_punctuation(f, " {")?;
221                            writeln!(f)?;
222
223                            // Push back the item with the next state to continue processing map
224                            item.state = StackState::ProcessMapEntry;
225                            item.format_depth += 1;
226                            // When recursing into a map, always increment format_depth
227                            // Only increment type_depth if we're moving to a different address
228                            item.type_depth = if core::ptr::eq(unsafe { map.data().as_ptr() }, ptr)
229                            {
230                                item.type_depth // Same pointer, don't increment type_depth
231                            } else {
232                                item.type_depth + 1 // Different pointer, increment type_depth
233                            };
234                            stack.push_back(item);
235                        }
236                        _ => {
237                            writeln!(f, "unsupported peek variant: {:?}", item.peek)?;
238                        }
239                    }
240                }
241                StackState::ProcessStructField { field_index } => {
242                    if let Peek::Struct(struct_) = item.peek {
243                        let fields: Vec<_> = struct_.fields_with_metadata().collect();
244
245                        if field_index >= fields.len() {
246                            // All fields processed, write closing brace
247                            write!(
248                                f,
249                                "{:width$}{}",
250                                "",
251                                self.style_punctuation("}"),
252                                width = (item.format_depth - 1) * self.indent_size
253                            )?;
254                            continue;
255                        }
256
257                        let (_, field_name, field_value, flags) = &fields[field_index];
258
259                        // Indent
260                        write!(
261                            f,
262                            "{:width$}",
263                            "",
264                            width = item.format_depth * self.indent_size
265                        )?;
266
267                        // Field name
268                        self.write_field_name(f, field_name)?;
269                        self.write_punctuation(f, ": ")?;
270
271                        // Check if field is sensitive
272                        if flags.contains(facet_trait::FieldFlags::SENSITIVE) {
273                            // Field value is sensitive, use write_redacted
274                            self.write_redacted(f, "[REDACTED]")?;
275                            self.write_punctuation(f, ",")?;
276                            writeln!(f)?;
277
278                            // Process next field
279                            item.state = StackState::ProcessStructField {
280                                field_index: field_index + 1,
281                            };
282                            stack.push_back(item);
283                        } else {
284                            // Field value is not sensitive, format normally
285                            // Push back current item to continue after formatting field value
286                            item.state = StackState::ProcessStructField {
287                                field_index: field_index + 1,
288                            };
289
290                            let finish_item = StackItem {
291                                peek: *field_value,
292                                format_depth: item.format_depth,
293                                type_depth: item.type_depth + 1,
294                                state: StackState::Finish,
295                            };
296                            let start_item = StackItem {
297                                peek: *field_value,
298                                format_depth: item.format_depth,
299                                type_depth: item.type_depth + 1,
300                                state: StackState::Start,
301                            };
302
303                            stack.push_back(item);
304                            stack.push_back(finish_item);
305                            stack.push_back(start_item);
306                        }
307                    }
308                }
309                StackState::ProcessListItem { item_index } => {
310                    if let Peek::List(list) = item.peek {
311                        if item_index >= list.len() {
312                            // All items processed, write closing bracket
313                            write!(
314                                f,
315                                "{:width$}",
316                                "",
317                                width = (item.format_depth - 1) * self.indent_size
318                            )?;
319                            self.write_punctuation(f, "]")?;
320                            continue;
321                        }
322
323                        // Indent
324                        write!(
325                            f,
326                            "{:width$}",
327                            "",
328                            width = item.format_depth * self.indent_size
329                        )?;
330
331                        // Push back current item to continue after formatting list item
332                        item.state = StackState::ProcessListItem {
333                            item_index: item_index + 1,
334                        };
335                        let next_format_depth = item.format_depth;
336                        let next_type_depth = item.type_depth + 1;
337                        stack.push_back(item);
338
339                        // Push list item to format first
340                        let list_item = list.iter().nth(item_index).unwrap();
341                        stack.push_back(StackItem {
342                            peek: list_item,
343                            format_depth: next_format_depth,
344                            type_depth: next_type_depth,
345                            state: StackState::Finish,
346                        });
347
348                        // When we push a list item to format, we need to process it from the beginning
349                        stack.push_back(StackItem {
350                            peek: list_item,
351                            format_depth: next_format_depth,
352                            type_depth: next_type_depth,
353                            state: StackState::Start, // Use Start state to properly process the item
354                        });
355                    }
356                }
357                StackState::ProcessMapEntry => {
358                    if let Peek::Map(_) = item.peek {
359                        // TODO: Implement proper map iteration when available in facet_peek
360
361                        // Indent
362                        write!(
363                            f,
364                            "{:width$}",
365                            "",
366                            width = item.format_depth * self.indent_size
367                        )?;
368                        write!(f, "{}", self.style_comment("/* Map contents */"))?;
369                        writeln!(f)?;
370
371                        // Closing brace with proper indentation
372                        write!(
373                            f,
374                            "{:width$}{}",
375                            "",
376                            self.style_punctuation("}"),
377                            width = (item.format_depth - 1) * self.indent_size
378                        )?;
379                    }
380                }
381                StackState::Finish => {
382                    // This state is reached after processing a field or list item
383                    // Add comma and newline for struct fields and list items
384                    self.write_punctuation(f, ",")?;
385                    writeln!(f)?;
386                }
387            }
388        }
389
390        Ok(())
391    }
392
393    /// Format a scalar value
394    fn format_value(&self, value: facet_peek::PeekValue, f: &mut impl Write) -> fmt::Result {
395        // Generate a color for this shape
396        let mut hasher = DefaultHasher::new();
397        value.shape().def.hash(&mut hasher);
398        let hash = hasher.finish();
399        let color = self.color_generator.generate_color(hash);
400
401        // Apply color if needed
402        if self.use_colors {
403            color.write_fg(f)?;
404        }
405
406        // Display the value
407        struct DisplayWrapper<'a>(&'a facet_peek::PeekValue<'a>);
408
409        impl fmt::Display for DisplayWrapper<'_> {
410            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
411                if self.0.display(f).is_none() {
412                    // If the value doesn't implement Display, use Debug
413                    if self.0.debug(f).is_none() {
414                        // If the value doesn't implement Debug either, just show the type name
415                        self.0.type_name(f, facet_trait::TypeNameOpts::infinite())?;
416                        write!(f, "(⋯)")?;
417                    }
418                }
419                Ok(())
420            }
421        }
422
423        write!(f, "{}", DisplayWrapper(&value))?;
424
425        // Reset color if needed
426        if self.use_colors {
427            ansi::write_reset(f)?;
428        }
429
430        Ok(())
431    }
432
433    /// Write styled type name to formatter
434    fn write_type_name<W: fmt::Write>(
435        &self,
436        f: &mut W,
437        peek: &facet_peek::PeekValue,
438    ) -> fmt::Result {
439        struct TypeNameWriter<'a, 'b: 'a>(&'b facet_peek::PeekValue<'a>);
440
441        impl core::fmt::Display for TypeNameWriter<'_, '_> {
442            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
443                self.0.type_name(f, facet_trait::TypeNameOpts::infinite())
444            }
445        }
446        let type_name = TypeNameWriter(peek);
447
448        if self.use_colors {
449            ansi::write_bold(f)?;
450            write!(f, "{}", type_name)?;
451            ansi::write_reset(f)
452        } else {
453            write!(f, "{}", type_name)
454        }
455    }
456
457    /// Style a type name and return it as a string
458    #[allow(dead_code)]
459    fn style_type_name(&self, peek: &facet_peek::PeekValue) -> String {
460        let mut result = String::new();
461        self.write_type_name(&mut result, peek).unwrap();
462        result
463    }
464
465    /// Write styled field name to formatter
466    fn write_field_name<W: fmt::Write>(&self, f: &mut W, name: &str) -> fmt::Result {
467        if self.use_colors {
468            ansi::write_rgb(f, 114, 160, 193)?;
469            write!(f, "{}", name)?;
470            ansi::write_reset(f)
471        } else {
472            write!(f, "{}", name)
473        }
474    }
475
476    /// Write styled punctuation to formatter
477    fn write_punctuation<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
478        if self.use_colors {
479            ansi::write_dim(f)?;
480            write!(f, "{}", text)?;
481            ansi::write_reset(f)
482        } else {
483            write!(f, "{}", text)
484        }
485    }
486
487    /// Style punctuation and return it as a string
488    fn style_punctuation(&self, text: &str) -> String {
489        let mut result = String::new();
490        self.write_punctuation(&mut result, text).unwrap();
491        result
492    }
493
494    /// Write styled comment to formatter
495    fn write_comment<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
496        if self.use_colors {
497            ansi::write_dim(f)?;
498            write!(f, "{}", text)?;
499            ansi::write_reset(f)
500        } else {
501            write!(f, "{}", text)
502        }
503    }
504
505    /// Style a comment and return it as a string
506    fn style_comment(&self, text: &str) -> String {
507        let mut result = String::new();
508        self.write_comment(&mut result, text).unwrap();
509        result
510    }
511
512    /// Write styled redacted value to formatter
513    fn write_redacted<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
514        if self.use_colors {
515            ansi::write_rgb(f, 224, 49, 49)?; // Use bright red for redacted values
516            ansi::write_bold(f)?;
517            write!(f, "{}", text)?;
518            ansi::write_reset(f)
519        } else {
520            write!(f, "{}", text)
521        }
522    }
523
524    /// Style a redacted value and return it as a string
525    #[allow(dead_code)]
526    fn style_redacted(&self, text: &str) -> String {
527        let mut result = String::new();
528        self.write_redacted(&mut result, text).unwrap();
529        result
530    }
531}
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536
537    // Basic tests for the PrettyPrinter
538    #[test]
539    fn test_pretty_printer_default() {
540        let printer = PrettyPrinter::default();
541        assert_eq!(printer.indent_size, 2);
542        assert_eq!(printer.max_depth, None);
543        assert!(printer.use_colors);
544    }
545
546    #[test]
547    fn test_pretty_printer_with_methods() {
548        let printer = PrettyPrinter::new()
549            .with_indent_size(4)
550            .with_max_depth(3)
551            .with_colors(false);
552
553        assert_eq!(printer.indent_size, 4);
554        assert_eq!(printer.max_depth, Some(3));
555        assert!(!printer.use_colors);
556    }
557}