use std::{
collections::HashSet,
fmt::{self, Write},
hash::{DefaultHasher, Hash, Hasher},
str,
};
use shapely_core::{FieldFlags, Innards, Scalar, ScalarContents, Shape, ShapeDesc, Shapely};
use crate::{
ansi,
color::{self, ColorGenerator},
};
pub struct PrettyPrinter {
indent_size: usize,
max_depth: Option<usize>,
color_generator: ColorGenerator,
use_colors: bool,
}
impl Default for PrettyPrinter {
fn default() -> Self {
Self {
indent_size: 2,
max_depth: None,
color_generator: ColorGenerator::default(),
use_colors: true,
}
}
}
impl PrettyPrinter {
pub fn new() -> Self {
Self::default()
}
pub fn with_indent_size(mut self, size: usize) -> Self {
self.indent_size = size;
self
}
pub fn with_max_depth(mut self, depth: usize) -> Self {
self.max_depth = Some(depth);
self
}
pub fn with_color_generator(mut self, generator: ColorGenerator) -> Self {
self.color_generator = generator;
self
}
pub fn with_colors(mut self, use_colors: bool) -> Self {
self.use_colors = use_colors;
self
}
pub fn print<T: Shapely>(&self, value: &T) {
let shape_desc = T::shape_desc();
let ptr = value as *const T as *mut u8;
let mut output = String::new();
self.format_value(ptr, shape_desc, &mut output, 0, &mut HashSet::new())
.expect("Formatting failed");
print!("{}", output);
}
pub fn format<T: Shapely>(&self, value: &T) -> String {
let shape_desc = T::shape_desc();
let ptr = value as *const T as *mut u8;
let mut output = String::new();
self.format_value(ptr, shape_desc, &mut output, 0, &mut HashSet::new())
.expect("Formatting failed");
output
}
pub fn format_to<T: Shapely>(&self, value: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let shape_desc = T::shape_desc();
let ptr = value as *const T as *mut u8;
self.format_value(ptr, shape_desc, f, 0, &mut HashSet::new())
}
pub(crate) fn format_value(
&self,
ptr: *mut u8,
shape_desc: ShapeDesc,
f: &mut impl Write,
depth: usize,
visited: &mut HashSet<*mut u8>,
) -> fmt::Result {
if let Some(max_depth) = self.max_depth {
if depth > max_depth {
self.write_punctuation(f, "[")?;
write!(f, "...")?;
return Ok(());
}
}
let shape = shape_desc.get();
let mut hasher = DefaultHasher::new();
shape.typeid.hash(&mut hasher);
let hash = hasher.finish();
let color = self.color_generator.generate_color(hash);
match &shape.innards {
Innards::Scalar(scalar) => self.format_scalar(ptr, *scalar, f, color),
Innards::Struct { fields }
| Innards::TupleStruct { fields }
| Innards::Tuple { fields } => {
self.format_struct(ptr, shape, fields, f, depth, visited)
}
Innards::Map {
vtable: _,
value_shape,
} => self.format_hashmap(ptr, shape, *value_shape, f, depth, visited),
Innards::List {
vtable: _,
item_shape,
} => self.format_array(ptr, shape, *item_shape, f, depth, visited),
Innards::Transparent(inner_shape) => {
self.format_transparent(ptr, shape, *inner_shape, f, depth, visited)
}
Innards::Enum { variants, repr: _ } => {
self.format_enum(ptr, shape, variants, f, depth, visited)
}
}
}
fn format_scalar(
&self,
ptr: *mut u8,
scalar: Scalar,
f: &mut impl Write,
color: color::RGB,
) -> fmt::Result {
let contents = unsafe { scalar.get_contents(ptr) };
if self.use_colors {
color.write_fg(f)?;
}
match contents {
ScalarContents::String(s) => {
write!(f, "\"")?;
for c in s.escape_debug() {
write!(f, "{}", c)?;
}
write!(f, "\"")?;
}
ScalarContents::Bytes(b) => {
write!(f, "b\"")?;
for &byte in b.iter().take(64) {
write!(f, "\\x{:02x}", byte)?;
}
if b.len() > 64 {
write!(f, "...")?;
}
write!(f, "\"")?;
}
ScalarContents::I8(v) => write!(f, "{}", v)?,
ScalarContents::I16(v) => write!(f, "{}", v)?,
ScalarContents::I32(v) => write!(f, "{}", v)?,
ScalarContents::I64(v) => write!(f, "{}", v)?,
ScalarContents::I128(v) => write!(f, "{}", v)?,
ScalarContents::U8(v) => write!(f, "{}", v)?,
ScalarContents::U16(v) => write!(f, "{}", v)?,
ScalarContents::U32(v) => write!(f, "{}", v)?,
ScalarContents::U64(v) => write!(f, "{}", v)?,
ScalarContents::U128(v) => write!(f, "{}", v)?,
ScalarContents::F32(v) => write!(f, "{}", v)?,
ScalarContents::F64(v) => write!(f, "{}", v)?,
ScalarContents::Boolean(v) => write!(f, "{}", v)?,
ScalarContents::Nothing => write!(f, "()")?,
ScalarContents::Unknown => write!(f, "<unknown scalar>")?,
_ => write!(f, "<unknown scalar type>")?,
}
if self.use_colors {
ansi::write_reset(f)?;
}
Ok(())
}
fn format_struct(
&self,
ptr: *mut u8,
shape: Shape,
fields: &'static [shapely_core::Field],
f: &mut impl Write,
depth: usize,
visited: &mut HashSet<*mut u8>,
) -> fmt::Result {
if !visited.insert(ptr) {
self.write_type_name(f, &shape.to_string())?;
self.write_punctuation(f, " { ")?;
self.write_comment(f, "/* cycle detected */")?;
self.write_punctuation(f, " }")?;
return Ok(());
}
self.write_type_name(f, &shape.to_string())?;
self.write_punctuation(f, " {")?;
if fields.is_empty() {
self.write_punctuation(f, " }")?;
visited.remove(&ptr);
return Ok(());
}
writeln!(f)?;
for field in fields {
write!(f, "{:width$}", "", width = (depth + 1) * self.indent_size)?;
write!(f, "{}: ", self.style_field_name(field.name))?;
if field.flags.contains(FieldFlags::SENSITIVE) {
write!(f, "{}", self.style_redacted("[REDACTED]"))?;
} else {
let field_ptr = unsafe { ptr.add(field.offset) };
self.format_value(field_ptr, field.shape, f, depth + 1, visited)?;
}
writeln!(f, "{}", self.style_punctuation(","))?;
}
write!(
f,
"{:width$}{}",
"",
self.style_punctuation("}"),
width = depth * self.indent_size
)?;
visited.remove(&ptr);
Ok(())
}
fn format_hashmap(
&self,
_ptr: *mut u8,
shape: Shape,
_value_shape: ShapeDesc,
f: &mut impl Write,
depth: usize,
_visited: &mut HashSet<*mut u8>,
) -> fmt::Result {
write!(f, "{}", self.style_type_name(&shape.to_string()))?;
write!(f, "{}", self.style_punctuation(" {"))?;
writeln!(f)?;
write!(f, "{:width$}", "", width = (depth + 1) * self.indent_size)?;
write!(f, "{}", self.style_comment("/* HashMap contents */"))?;
writeln!(f)?;
write!(
f,
"{:width$}{}",
"",
self.style_punctuation("}"),
width = depth * self.indent_size
)
}
fn format_array(
&self,
_ptr: *mut u8,
shape: Shape,
_elem_shape: ShapeDesc,
f: &mut impl Write,
depth: usize,
_visited: &mut HashSet<*mut u8>,
) -> fmt::Result {
write!(f, "{}", self.style_type_name(&shape.to_string()))?;
write!(f, "{}", self.style_punctuation(" ["))?;
writeln!(f)?;
write!(f, "{:width$}", "", width = (depth + 1) * self.indent_size)?;
write!(f, "{}", self.style_comment("/* Array contents */"))?;
writeln!(f)?;
write!(
f,
"{:width$}{}",
"",
self.style_punctuation("]"),
width = depth * self.indent_size
)
}
fn format_transparent(
&self,
ptr: *mut u8,
shape: Shape,
inner_shape: ShapeDesc,
f: &mut impl Write,
depth: usize,
visited: &mut HashSet<*mut u8>,
) -> fmt::Result {
write!(f, "{}", self.style_type_name(&shape.to_string()))?;
write!(f, "{}", self.style_punctuation("("))?;
self.format_value(ptr, inner_shape, f, depth, visited)?;
write!(f, "{}", self.style_punctuation(")"))
}
fn format_enum(
&self,
_ptr: *mut u8,
shape: Shape,
_variants: &'static [shapely_core::Variant],
f: &mut impl Write,
depth: usize,
_visited: &mut HashSet<*mut u8>,
) -> fmt::Result {
self.write_type_name(f, &format!("{}", shape))?;
writeln!(f, " {{")?;
if let Some(max_depth) = self.max_depth {
if depth >= max_depth {
writeln!(
f,
"{}{}",
" ".repeat(self.indent_size),
self.style_comment("// Enum contents omitted due to depth limit")
)?;
writeln!(f, "}}")?;
return Ok(());
}
}
writeln!(
f,
"{}{}",
" ".repeat(self.indent_size),
self.style_comment(
format!(
"// Enum with {} variants (variant access not yet implemented)",
_variants.len()
)
.as_str()
)
)?;
writeln!(f, "}}")?;
Ok(())
}
fn write_type_name<W: fmt::Write>(&self, f: &mut W, name: &str) -> fmt::Result {
if self.use_colors {
ansi::write_bold(f)?;
write!(f, "{}", name)?;
ansi::write_reset(f)
} else {
write!(f, "{}", name)
}
}
fn style_type_name(&self, name: &str) -> String {
let mut result = String::new();
self.write_type_name(&mut result, name).unwrap();
result
}
fn write_field_name<W: fmt::Write>(&self, f: &mut W, name: &str) -> fmt::Result {
if self.use_colors {
ansi::write_rgb(f, 114, 160, 193)?;
write!(f, "{}", name)?;
ansi::write_reset(f)
} else {
write!(f, "{}", name)
}
}
fn style_field_name(&self, name: &str) -> String {
let mut result = String::new();
self.write_field_name(&mut result, name).unwrap();
result
}
fn write_punctuation<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
if self.use_colors {
ansi::write_dim(f)?;
write!(f, "{}", text)?;
ansi::write_reset(f)
} else {
write!(f, "{}", text)
}
}
fn style_punctuation(&self, text: &str) -> String {
let mut result = String::new();
self.write_punctuation(&mut result, text).unwrap();
result
}
fn write_comment<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
if self.use_colors {
ansi::write_dim(f)?;
write!(f, "{}", text)?;
ansi::write_reset(f)
} else {
write!(f, "{}", text)
}
}
fn style_comment(&self, text: &str) -> String {
let mut result = String::new();
self.write_comment(&mut result, text).unwrap();
result
}
fn write_redacted<W: fmt::Write>(&self, f: &mut W, text: &str) -> fmt::Result {
if self.use_colors {
ansi::write_rgb(f, 224, 49, 49)?; ansi::write_bold(f)?;
write!(f, "{}", text)?;
ansi::write_reset(f)
} else {
write!(f, "{}", text)
}
}
fn style_redacted(&self, text: &str) -> String {
let mut result = String::new();
self.write_redacted(&mut result, text).unwrap();
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pretty_printer_default() {
let printer = PrettyPrinter::default();
assert_eq!(printer.indent_size, 2);
assert_eq!(printer.max_depth, None);
assert!(printer.use_colors);
}
#[test]
fn test_pretty_printer_with_methods() {
let printer = PrettyPrinter::new()
.with_indent_size(4)
.with_max_depth(3)
.with_colors(false);
assert_eq!(printer.indent_size, 4);
assert_eq!(printer.max_depth, Some(3));
assert!(!printer.use_colors);
}
#[test]
fn test_style_methods() {
let printer_with_colors = PrettyPrinter::new().with_colors(true);
let printer_without_colors = PrettyPrinter::new().with_colors(false);
assert_eq!(
printer_with_colors.style_type_name("Test"),
format!("{}Test{}", ansi::BOLD, ansi::RESET)
);
assert_eq!(printer_without_colors.style_type_name("Test"), "Test");
}
}