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 _ => {
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_core::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_core::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_core::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}