1use std::{
4 collections::HashSet,
5 fmt::{self, Write},
6 hash::{DefaultHasher, Hash, Hasher},
7 str,
8};
9
10use shapely_core::{FieldFlags, Innards, Scalar, ScalarContents, Shape, ShapeDesc, Shapely};
11
12use crate::{
13 ansi,
14 color::{self, ColorGenerator},
15};
16
17pub 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 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn with_indent_size(mut self, size: usize) -> Self {
44 self.indent_size = size;
45 self
46 }
47
48 pub fn with_max_depth(mut self, depth: usize) -> Self {
50 self.max_depth = Some(depth);
51 self
52 }
53
54 pub fn with_color_generator(mut self, generator: ColorGenerator) -> Self {
56 self.color_generator = generator;
57 self
58 }
59
60 pub fn with_colors(mut self, use_colors: bool) -> Self {
62 self.use_colors = use_colors;
63 self
64 }
65
66 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 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 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 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 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 let shape = shape_desc.get();
118
119 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 match &shape.innards {
127 Innards::Scalar(scalar) => self.format_scalar(ptr, *scalar, f, color),
128 Innards::Struct { fields }
129 | Innards::TupleStruct { fields }
130 | Innards::Tuple { fields } => {
131 self.format_struct(ptr, shape, fields, f, depth, visited)
132 }
133 Innards::Map {
134 vtable: _,
135 value_shape,
136 } => self.format_hashmap(ptr, shape, *value_shape, f, depth, visited),
137 Innards::List {
138 vtable: _,
139 item_shape,
140 } => self.format_array(ptr, shape, *item_shape, f, depth, visited),
141 Innards::Transparent(inner_shape) => {
142 self.format_transparent(ptr, shape, *inner_shape, f, depth, visited)
143 }
144 Innards::Enum { variants, repr: _ } => {
145 self.format_enum(ptr, shape, variants, f, depth, visited)
146 }
147 }
148 }
149
150 fn format_scalar(
152 &self,
153 ptr: *mut u8,
154 scalar: Scalar,
155 f: &mut impl Write,
156 color: color::RGB,
157 ) -> fmt::Result {
158 let contents = unsafe { scalar.get_contents(ptr) };
160
161 if self.use_colors {
163 color.write_fg(f)?;
164 }
165
166 match contents {
168 ScalarContents::String(s) => {
169 write!(f, "\"")?;
170 for c in s.escape_debug() {
171 write!(f, "{}", c)?;
172 }
173 write!(f, "\"")?;
174 }
175 ScalarContents::Bytes(b) => {
176 write!(f, "b\"")?;
177 for &byte in b.iter().take(64) {
178 write!(f, "\\x{:02x}", byte)?;
179 }
180 if b.len() > 64 {
181 write!(f, "...")?;
182 }
183 write!(f, "\"")?;
184 }
185 ScalarContents::I8(v) => write!(f, "{}", v)?,
186 ScalarContents::I16(v) => write!(f, "{}", v)?,
187 ScalarContents::I32(v) => write!(f, "{}", v)?,
188 ScalarContents::I64(v) => write!(f, "{}", v)?,
189 ScalarContents::I128(v) => write!(f, "{}", v)?,
190 ScalarContents::U8(v) => write!(f, "{}", v)?,
191 ScalarContents::U16(v) => write!(f, "{}", v)?,
192 ScalarContents::U32(v) => write!(f, "{}", v)?,
193 ScalarContents::U64(v) => write!(f, "{}", v)?,
194 ScalarContents::U128(v) => write!(f, "{}", v)?,
195 ScalarContents::F32(v) => write!(f, "{}", v)?,
196 ScalarContents::F64(v) => write!(f, "{}", v)?,
197 ScalarContents::Boolean(v) => write!(f, "{}", v)?,
198 ScalarContents::Nothing => write!(f, "()")?,
199 ScalarContents::Unknown => write!(f, "<unknown scalar>")?,
200 _ => write!(f, "<unknown scalar type>")?,
202 }
203
204 if self.use_colors {
206 ansi::write_reset(f)?;
207 }
208
209 Ok(())
210 }
211
212 fn format_struct(
214 &self,
215 ptr: *mut u8,
216 shape: Shape,
217 fields: &'static [shapely_core::Field],
218 f: &mut impl Write,
219 depth: usize,
220 visited: &mut HashSet<*mut u8>,
221 ) -> fmt::Result {
222 if !visited.insert(ptr) {
224 self.write_type_name(f, &shape.to_string())?;
225 self.write_punctuation(f, " { ")?;
226 self.write_comment(f, "/* cycle detected */")?;
227 self.write_punctuation(f, " }")?;
228 return Ok(());
229 }
230
231 self.write_type_name(f, &shape.to_string())?;
233 self.write_punctuation(f, " {")?;
234
235 if fields.is_empty() {
236 self.write_punctuation(f, " }")?;
237 visited.remove(&ptr);
238 return Ok(());
239 }
240
241 writeln!(f)?;
242
243 for field in fields {
245 write!(f, "{:width$}", "", width = (depth + 1) * self.indent_size)?;
247
248 write!(f, "{}: ", self.style_field_name(field.name))?;
250
251 if field.flags.contains(FieldFlags::SENSITIVE) {
253 write!(f, "{}", self.style_redacted("[REDACTED]"))?;
255 } else {
256 let field_ptr = unsafe { ptr.add(field.offset) };
258 self.format_value(field_ptr, field.shape, f, depth + 1, visited)?;
259 }
260
261 writeln!(f, "{}", self.style_punctuation(","))?;
262 }
263
264 write!(
266 f,
267 "{:width$}{}",
268 "",
269 self.style_punctuation("}"),
270 width = depth * self.indent_size
271 )?;
272
273 visited.remove(&ptr);
275
276 Ok(())
277 }
278
279 fn format_hashmap(
281 &self,
282 _ptr: *mut u8,
283 shape: Shape,
284 _value_shape: ShapeDesc,
285 f: &mut impl Write,
286 depth: usize,
287 _visited: &mut HashSet<*mut u8>,
288 ) -> fmt::Result {
289 write!(f, "{}", self.style_type_name(&shape.to_string()))?;
293 write!(f, "{}", self.style_punctuation(" {"))?;
294 writeln!(f)?;
295
296 write!(f, "{:width$}", "", width = (depth + 1) * self.indent_size)?;
298 write!(f, "{}", self.style_comment("/* HashMap contents */"))?;
299 writeln!(f)?;
300
301 write!(
303 f,
304 "{:width$}{}",
305 "",
306 self.style_punctuation("}"),
307 width = depth * self.indent_size
308 )
309 }
310
311 fn format_array(
313 &self,
314 _ptr: *mut u8,
315 shape: Shape,
316 _elem_shape: ShapeDesc,
317 f: &mut impl Write,
318 depth: usize,
319 _visited: &mut HashSet<*mut u8>,
320 ) -> fmt::Result {
321 write!(f, "{}", self.style_type_name(&shape.to_string()))?;
325 write!(f, "{}", self.style_punctuation(" ["))?;
326 writeln!(f)?;
327
328 write!(f, "{:width$}", "", width = (depth + 1) * self.indent_size)?;
330 write!(f, "{}", self.style_comment("/* Array contents */"))?;
331 writeln!(f)?;
332
333 write!(
335 f,
336 "{:width$}{}",
337 "",
338 self.style_punctuation("]"),
339 width = depth * self.indent_size
340 )
341 }
342
343 fn format_transparent(
345 &self,
346 ptr: *mut u8,
347 shape: Shape,
348 inner_shape: ShapeDesc,
349 f: &mut impl Write,
350 depth: usize,
351 visited: &mut HashSet<*mut u8>,
352 ) -> fmt::Result {
353 write!(f, "{}", self.style_type_name(&shape.to_string()))?;
355 write!(f, "{}", self.style_punctuation("("))?;
356
357 self.format_value(ptr, inner_shape, f, depth, visited)?;
359
360 write!(f, "{}", self.style_punctuation(")"))
362 }
363
364 fn format_enum(
366 &self,
367 _ptr: *mut u8,
368 shape: Shape,
369 _variants: &'static [shapely_core::Variant],
370 f: &mut impl Write,
371 depth: usize,
372 _visited: &mut HashSet<*mut u8>,
373 ) -> fmt::Result {
374 self.write_type_name(f, &format!("{}", shape))?;
377 writeln!(f, " {{")?;
378 if let Some(max_depth) = self.max_depth {
379 if depth >= max_depth {
380 writeln!(
381 f,
382 "{}{}",
383 " ".repeat(self.indent_size),
384 self.style_comment("// Enum contents omitted due to depth limit")
385 )?;
386 writeln!(f, "}}")?;
387 return Ok(());
388 }
389 }
390 writeln!(
391 f,
392 "{}{}",
393 " ".repeat(self.indent_size),
394 self.style_comment(
395 format!(
396 "// Enum with {} variants (variant access not yet implemented)",
397 _variants.len()
398 )
399 .as_str()
400 )
401 )?;
402 writeln!(f, "}}")?;
403 Ok(())
404 }
405
406 fn write_type_name<W: fmt::Write>(&self, f: &mut W, name: &str) -> fmt::Result {
408 if self.use_colors {
409 ansi::write_bold(f)?;
410 write!(f, "{}", name)?;
411 ansi::write_reset(f)
412 } else {
413 write!(f, "{}", name)
414 }
415 }
416
417 fn style_type_name(&self, name: &str) -> String {
419 let mut result = String::new();
420 self.write_type_name(&mut result, name).unwrap();
421 result
422 }
423
424 fn write_field_name<W: fmt::Write>(&self, f: &mut W, name: &str) -> fmt::Result {
426 if self.use_colors {
427 ansi::write_rgb(f, 114, 160, 193)?;
428 write!(f, "{}", name)?;
429 ansi::write_reset(f)
430 } else {
431 write!(f, "{}", name)
432 }
433 }
434
435 fn style_field_name(&self, name: &str) -> String {
437 let mut result = String::new();
438 self.write_field_name(&mut result, name).unwrap();
439 result
440 }
441
442 fn write_punctuation<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
444 if self.use_colors {
445 ansi::write_dim(f)?;
446 write!(f, "{}", text)?;
447 ansi::write_reset(f)
448 } else {
449 write!(f, "{}", text)
450 }
451 }
452
453 fn style_punctuation(&self, text: &str) -> String {
455 let mut result = String::new();
456 self.write_punctuation(&mut result, text).unwrap();
457 result
458 }
459
460 fn write_comment<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
462 if self.use_colors {
463 ansi::write_dim(f)?;
464 write!(f, "{}", text)?;
465 ansi::write_reset(f)
466 } else {
467 write!(f, "{}", text)
468 }
469 }
470
471 fn style_comment(&self, text: &str) -> String {
473 let mut result = String::new();
474 self.write_comment(&mut result, text).unwrap();
475 result
476 }
477
478 fn write_redacted<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
480 if self.use_colors {
481 ansi::write_rgb(f, 224, 49, 49)?; ansi::write_bold(f)?;
483 write!(f, "{}", text)?;
484 ansi::write_reset(f)
485 } else {
486 write!(f, "{}", text)
487 }
488 }
489
490 fn style_redacted(&self, text: &str) -> String {
492 let mut result = String::new();
493 self.write_redacted(&mut result, text).unwrap();
494 result
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501
502 #[test]
504 fn test_pretty_printer_default() {
505 let printer = PrettyPrinter::default();
506 assert_eq!(printer.indent_size, 2);
507 assert_eq!(printer.max_depth, None);
508 assert!(printer.use_colors);
509 }
510
511 #[test]
512 fn test_pretty_printer_with_methods() {
513 let printer = PrettyPrinter::new()
514 .with_indent_size(4)
515 .with_max_depth(3)
516 .with_colors(false);
517
518 assert_eq!(printer.indent_size, 4);
519 assert_eq!(printer.max_depth, Some(3));
520 assert!(!printer.use_colors);
521 }
522
523 #[test]
524 fn test_style_methods() {
525 let printer_with_colors = PrettyPrinter::new().with_colors(true);
526 let printer_without_colors = PrettyPrinter::new().with_colors(false);
527
528 assert_eq!(
530 printer_with_colors.style_type_name("Test"),
531 format!("{}Test{}", ansi::BOLD, ansi::RESET)
532 );
533
534 assert_eq!(printer_without_colors.style_type_name("Test"), "Test");
536 }
537}