shapely_pretty/
printer.rs

1//! Pretty printer implementation for Shapely types
2
3use std::{
4    collections::HashSet,
5    fmt::{self, Write},
6    hash::{DefaultHasher, Hash, Hasher},
7    str,
8};
9
10use shapely_core::{Innards, Scalar, ScalarContents, Shape, ShapeDesc, Shapely};
11
12use crate::{
13    ansi,
14    color::{self, ColorGenerator},
15};
16
17/// A formatter for pretty-printing Shapely types
18pub struct PrettyPrinter {
19    indent_size: usize,
20    max_depth: Option<usize>,
21    color_generator: ColorGenerator,
22    use_colors: bool,
23}
24
25impl Default for PrettyPrinter {
26    fn default() -> Self {
27        Self {
28            indent_size: 2,
29            max_depth: None,
30            color_generator: ColorGenerator::default(),
31            use_colors: true,
32        }
33    }
34}
35
36impl PrettyPrinter {
37    /// Create a new PrettyPrinter with default settings
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Set the indentation size
43    pub fn with_indent_size(mut self, size: usize) -> Self {
44        self.indent_size = size;
45        self
46    }
47
48    /// Set the maximum depth for recursive printing
49    pub fn with_max_depth(mut self, depth: usize) -> Self {
50        self.max_depth = Some(depth);
51        self
52    }
53
54    /// Set the color generator
55    pub fn with_color_generator(mut self, generator: ColorGenerator) -> Self {
56        self.color_generator = generator;
57        self
58    }
59
60    /// Enable or disable colors
61    pub fn with_colors(mut self, use_colors: bool) -> Self {
62        self.use_colors = use_colors;
63        self
64    }
65
66    /// Pretty-print a value that implements Shapely
67    pub fn print<T: Shapely>(&self, value: &T) {
68        let shape_desc = T::shape_desc();
69        let ptr = value as *const T as *mut u8;
70
71        let mut output = String::new();
72        self.format_value(ptr, shape_desc, &mut output, 0, &mut HashSet::new())
73            .expect("Formatting failed");
74
75        print!("{}", output);
76    }
77
78    /// Format a value to a string
79    pub fn format<T: Shapely>(&self, value: &T) -> String {
80        let shape_desc = T::shape_desc();
81        let ptr = value as *const T as *mut u8;
82
83        let mut output = String::new();
84        self.format_value(ptr, shape_desc, &mut output, 0, &mut HashSet::new())
85            .expect("Formatting failed");
86
87        output
88    }
89
90    /// Format a value to a formatter
91    pub fn format_to<T: Shapely>(&self, value: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        let shape_desc = T::shape_desc();
93        let ptr = value as *const T as *mut u8;
94
95        self.format_value(ptr, shape_desc, f, 0, &mut HashSet::new())
96    }
97
98    /// Internal method to format a value at a specific memory address
99    pub(crate) fn format_value(
100        &self,
101        ptr: *mut u8,
102        shape_desc: ShapeDesc,
103        f: &mut impl Write,
104        depth: usize,
105        visited: &mut HashSet<*mut u8>,
106    ) -> fmt::Result {
107        // Check if we've reached the maximum depth
108        if let Some(max_depth) = self.max_depth {
109            if depth > max_depth {
110                self.write_punctuation(f, "[")?;
111                write!(f, "...")?;
112                return Ok(());
113            }
114        }
115
116        // Get the shape
117        let shape = shape_desc.get();
118
119        // Generate a color for this shape
120        let mut hasher = DefaultHasher::new();
121        shape.typeid.hash(&mut hasher);
122        let hash = hasher.finish();
123        let color = self.color_generator.generate_color(hash);
124
125        // Format based on the shape's innards
126        match &shape.innards {
127            Innards::Scalar(scalar) => self.format_scalar(ptr, *scalar, f, color),
128            Innards::Struct { fields } => self.format_struct(ptr, shape, fields, f, depth, visited),
129            Innards::HashMap { value_shape } => {
130                self.format_hashmap(ptr, shape, *value_shape, f, depth, visited)
131            }
132            Innards::Array(elem_shape) => {
133                self.format_array(ptr, shape, *elem_shape, f, depth, visited)
134            }
135            Innards::Transparent(inner_shape) => {
136                self.format_transparent(ptr, shape, *inner_shape, f, depth, visited)
137            }
138        }
139    }
140
141    /// Format a scalar value
142    fn format_scalar(
143        &self,
144        ptr: *mut u8,
145        scalar: Scalar,
146        f: &mut impl Write,
147        color: color::RGB,
148    ) -> fmt::Result {
149        // Use Scalar::get_contents for safe access to the scalar value
150        let contents = unsafe { scalar.get_contents(ptr) };
151
152        // Apply color if needed
153        if self.use_colors {
154            color.write_fg(f)?;
155        }
156
157        // Format the content
158        match contents {
159            ScalarContents::String(s) => {
160                write!(f, "\"")?;
161                for c in s.escape_debug() {
162                    write!(f, "{}", c)?;
163                }
164                write!(f, "\"")?;
165            }
166            ScalarContents::Bytes(b) => {
167                write!(f, "b\"")?;
168                for &byte in b.iter().take(64) {
169                    write!(f, "\\x{:02x}", byte)?;
170                }
171                if b.len() > 64 {
172                    write!(f, "...")?;
173                }
174                write!(f, "\"")?;
175            }
176            ScalarContents::I8(v) => write!(f, "{}", v)?,
177            ScalarContents::I16(v) => write!(f, "{}", v)?,
178            ScalarContents::I32(v) => write!(f, "{}", v)?,
179            ScalarContents::I64(v) => write!(f, "{}", v)?,
180            ScalarContents::I128(v) => write!(f, "{}", v)?,
181            ScalarContents::U8(v) => write!(f, "{}", v)?,
182            ScalarContents::U16(v) => write!(f, "{}", v)?,
183            ScalarContents::U32(v) => write!(f, "{}", v)?,
184            ScalarContents::U64(v) => write!(f, "{}", v)?,
185            ScalarContents::U128(v) => write!(f, "{}", v)?,
186            ScalarContents::F32(v) => write!(f, "{}", v)?,
187            ScalarContents::F64(v) => write!(f, "{}", v)?,
188            ScalarContents::Boolean(v) => write!(f, "{}", v)?,
189            ScalarContents::Nothing => write!(f, "()")?,
190            ScalarContents::Unknown => write!(f, "<unknown scalar>")?,
191            // Handle future variants that might be added to the non-exhaustive enum
192            _ => write!(f, "<unknown scalar type>")?,
193        }
194
195        // Reset color if needed
196        if self.use_colors {
197            ansi::write_reset(f)?;
198        }
199
200        Ok(())
201    }
202
203    /// Format a struct
204    fn format_struct(
205        &self,
206        ptr: *mut u8,
207        shape: Shape,
208        fields: &'static [shapely_core::Field],
209        f: &mut impl Write,
210        depth: usize,
211        visited: &mut HashSet<*mut u8>,
212    ) -> fmt::Result {
213        // Check for cycles
214        if !visited.insert(ptr) {
215            self.write_type_name(f, &shape.to_string())?;
216            self.write_punctuation(f, " { ")?;
217            self.write_comment(f, "/* cycle detected */")?;
218            self.write_punctuation(f, " }")?;
219            return Ok(());
220        }
221
222        // Print the struct name
223        self.write_type_name(f, &shape.to_string())?;
224        self.write_punctuation(f, " {")?;
225
226        if fields.is_empty() {
227            self.write_punctuation(f, " }")?;
228            visited.remove(&ptr);
229            return Ok(());
230        }
231
232        writeln!(f)?;
233
234        // Print each field
235        for field in fields {
236            // Indent
237            write!(f, "{:width$}", "", width = (depth + 1) * self.indent_size)?;
238
239            // Field name
240            write!(f, "{}: ", self.style_field_name(field.name))?;
241
242            // Check if field is sensitive
243            if field.flags.is_sensitive() {
244                // For sensitive fields, display [REDACTED] instead of the actual value
245                write!(f, "{}", self.style_redacted("[REDACTED]"))?;
246            } else {
247                // Field value - compute the field address
248                let field_ptr = unsafe { ptr.add(field.offset) };
249                self.format_value(field_ptr, field.shape, f, depth + 1, visited)?;
250            }
251
252            writeln!(f, "{}", self.style_punctuation(","))?;
253        }
254
255        // Closing brace with proper indentation
256        write!(
257            f,
258            "{:width$}{}",
259            "",
260            self.style_punctuation("}"),
261            width = depth * self.indent_size
262        )?;
263
264        // Remove from visited set when we're done with this struct
265        visited.remove(&ptr);
266
267        Ok(())
268    }
269
270    /// Format a HashMap
271    fn format_hashmap(
272        &self,
273        _ptr: *mut u8,
274        shape: Shape,
275        _value_shape: ShapeDesc,
276        f: &mut impl Write,
277        depth: usize,
278        _visited: &mut HashSet<*mut u8>,
279    ) -> fmt::Result {
280        // In a real implementation, we would need to iterate over the HashMap
281        // For now, we'll just print a placeholder
282
283        write!(f, "{}", self.style_type_name(&shape.to_string()))?;
284        write!(f, "{}", self.style_punctuation(" {"))?;
285        writeln!(f)?;
286
287        // Indent
288        write!(f, "{:width$}", "", width = (depth + 1) * self.indent_size)?;
289        write!(f, "{}", self.style_comment("/* HashMap contents */"))?;
290        writeln!(f)?;
291
292        // Closing brace with proper indentation
293        write!(
294            f,
295            "{:width$}{}",
296            "",
297            self.style_punctuation("}"),
298            width = depth * self.indent_size
299        )
300    }
301
302    /// Format an array
303    fn format_array(
304        &self,
305        _ptr: *mut u8,
306        shape: Shape,
307        _elem_shape: ShapeDesc,
308        f: &mut impl Write,
309        depth: usize,
310        _visited: &mut HashSet<*mut u8>,
311    ) -> fmt::Result {
312        // In a real implementation, we would need to iterate over the array
313        // For now, we'll just print a placeholder
314
315        write!(f, "{}", self.style_type_name(&shape.to_string()))?;
316        write!(f, "{}", self.style_punctuation(" ["))?;
317        writeln!(f)?;
318
319        // Indent
320        write!(f, "{:width$}", "", width = (depth + 1) * self.indent_size)?;
321        write!(f, "{}", self.style_comment("/* Array contents */"))?;
322        writeln!(f)?;
323
324        // Closing bracket with proper indentation
325        write!(
326            f,
327            "{:width$}{}",
328            "",
329            self.style_punctuation("]"),
330            width = depth * self.indent_size
331        )
332    }
333
334    /// Format a transparent wrapper
335    fn format_transparent(
336        &self,
337        ptr: *mut u8,
338        shape: Shape,
339        inner_shape: ShapeDesc,
340        f: &mut impl Write,
341        depth: usize,
342        visited: &mut HashSet<*mut u8>,
343    ) -> fmt::Result {
344        // Print the wrapper type name
345        write!(f, "{}", self.style_type_name(&shape.to_string()))?;
346        write!(f, "{}", self.style_punctuation("("))?;
347
348        // Format the inner value
349        self.format_value(ptr, inner_shape, f, depth, visited)?;
350
351        // Closing parenthesis
352        write!(f, "{}", self.style_punctuation(")"))
353    }
354
355    /// Write styled type name to formatter
356    fn write_type_name<W: fmt::Write>(&self, f: &mut W, name: &str) -> fmt::Result {
357        if self.use_colors {
358            ansi::write_bold(f)?;
359            write!(f, "{}", name)?;
360            ansi::write_reset(f)
361        } else {
362            write!(f, "{}", name)
363        }
364    }
365
366    /// Style a type name and return it as a string
367    fn style_type_name(&self, name: &str) -> String {
368        let mut result = String::new();
369        self.write_type_name(&mut result, name).unwrap();
370        result
371    }
372
373    /// Write styled field name to formatter
374    fn write_field_name<W: fmt::Write>(&self, f: &mut W, name: &str) -> fmt::Result {
375        if self.use_colors {
376            ansi::write_rgb(f, 114, 160, 193)?;
377            write!(f, "{}", name)?;
378            ansi::write_reset(f)
379        } else {
380            write!(f, "{}", name)
381        }
382    }
383
384    /// Style a field name and return it as a string
385    fn style_field_name(&self, name: &str) -> String {
386        let mut result = String::new();
387        self.write_field_name(&mut result, name).unwrap();
388        result
389    }
390
391    /// Write styled punctuation to formatter
392    fn write_punctuation<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
393        if self.use_colors {
394            ansi::write_dim(f)?;
395            write!(f, "{}", text)?;
396            ansi::write_reset(f)
397        } else {
398            write!(f, "{}", text)
399        }
400    }
401
402    /// Style punctuation and return it as a string
403    fn style_punctuation(&self, text: &str) -> String {
404        let mut result = String::new();
405        self.write_punctuation(&mut result, text).unwrap();
406        result
407    }
408
409    /// Write styled comment to formatter
410    fn write_comment<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
411        if self.use_colors {
412            ansi::write_dim(f)?;
413            write!(f, "{}", text)?;
414            ansi::write_reset(f)
415        } else {
416            write!(f, "{}", text)
417        }
418    }
419
420    /// Style a comment and return it as a string
421    fn style_comment(&self, text: &str) -> String {
422        let mut result = String::new();
423        self.write_comment(&mut result, text).unwrap();
424        result
425    }
426
427    /// Write styled redacted value to formatter
428    fn write_redacted<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
429        if self.use_colors {
430            ansi::write_rgb(f, 224, 49, 49)?; // Use bright red for redacted values
431            ansi::write_bold(f)?;
432            write!(f, "{}", text)?;
433            ansi::write_reset(f)
434        } else {
435            write!(f, "{}", text)
436        }
437    }
438
439    /// Style a redacted value and return it as a string
440    fn style_redacted(&self, text: &str) -> String {
441        let mut result = String::new();
442        self.write_redacted(&mut result, text).unwrap();
443        result
444    }
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450
451    // Basic tests for the PrettyPrinter
452    #[test]
453    fn test_pretty_printer_default() {
454        let printer = PrettyPrinter::default();
455        assert_eq!(printer.indent_size, 2);
456        assert_eq!(printer.max_depth, None);
457        assert!(printer.use_colors);
458    }
459
460    #[test]
461    fn test_pretty_printer_with_methods() {
462        let printer = PrettyPrinter::new()
463            .with_indent_size(4)
464            .with_max_depth(3)
465            .with_colors(false);
466
467        assert_eq!(printer.indent_size, 4);
468        assert_eq!(printer.max_depth, Some(3));
469        assert!(!printer.use_colors);
470    }
471
472    #[test]
473    fn test_style_methods() {
474        let printer_with_colors = PrettyPrinter::new().with_colors(true);
475        let printer_without_colors = PrettyPrinter::new().with_colors(false);
476
477        // With colors
478        assert_eq!(
479            printer_with_colors.style_type_name("Test"),
480            format!("{}Test{}", ansi::BOLD, ansi::RESET)
481        );
482
483        // Without colors
484        assert_eq!(printer_without_colors.style_type_name("Test"), "Test");
485    }
486}