use crate::event::Event;
use crate::pipeline;
use rhai::Dynamic;
pub struct InspectFormatter {
max_inline_chars: usize,
}
struct LineSpec<'a> {
indent: usize,
name: &'a str,
name_width: usize,
type_width: usize,
type_label: &'a str,
value_repr: &'a str,
}
impl InspectFormatter {
const KEY_WIDTH_CAP: usize = 40;
pub fn new(verbosity: u8) -> Self {
let max_inline_chars = match verbosity {
0 => 80,
1 => 160,
_ => usize::MAX,
};
Self { max_inline_chars }
}
fn format_entries<'a, I>(&self, lines: &mut Vec<String>, entries: I, indent: usize)
where
I: IntoIterator<Item = (&'a str, &'a Dynamic)>,
{
let collected: Vec<(&str, &Dynamic)> = entries.into_iter().collect();
if collected.is_empty() {
return;
}
let name_width = self.compute_key_width(collected.iter().map(|(k, _)| *k));
let type_width = self.compute_type_width(collected.iter().map(|(_, v)| *v));
for (key, value) in collected {
self.format_entry_with_width(lines, key, value, indent, name_width, type_width);
}
}
fn format_entry_with_width(
&self,
lines: &mut Vec<String>,
name: &str,
value: &Dynamic,
indent: usize,
name_width: usize,
type_width: usize,
) {
if let Some(map) = value.clone().try_cast::<rhai::Map>() {
let entries: Vec<(String, Dynamic)> =
map.into_iter().map(|(k, v)| (k.into(), v)).collect();
let type_label = format!("map({})", entries.len());
self.push_line(
lines,
LineSpec {
indent,
name,
name_width,
type_width,
type_label: &type_label,
value_repr: "{",
},
);
if !entries.is_empty() {
let child_width = self.compute_key_width(entries.iter().map(|(k, _)| k.as_str()));
let child_type_width = self.compute_type_width(entries.iter().map(|(_, v)| v));
for (child_key, child_value) in &entries {
self.format_entry_with_width(
lines,
child_key,
child_value,
indent + 1,
child_width,
child_type_width,
);
}
}
lines.push(format!("{}{}", " ".repeat(indent), "}"));
} else if let Some(array) = value.clone().try_cast::<rhai::Array>() {
let elements: Vec<Dynamic> = array.into_iter().collect();
let type_label = format!("array({})", elements.len());
self.push_line(
lines,
LineSpec {
indent,
name,
name_width,
type_width,
type_label: &type_label,
value_repr: "[",
},
);
if !elements.is_empty() {
let index_labels: Vec<String> =
(0..elements.len()).map(|i| format!("[{}]", i)).collect();
let child_width = self.compute_key_width(index_labels.iter().map(|s| s.as_str()));
let child_type_width = self.compute_type_width(elements.iter());
for (idx, element) in elements.iter().enumerate() {
let child_name = &index_labels[idx];
self.format_entry_with_width(
lines,
child_name,
element,
indent + 1,
child_width,
child_type_width,
);
}
}
lines.push(format!("{}{}", " ".repeat(indent), "]"));
} else {
let (type_label, value_repr) = self.describe_scalar(value);
self.push_line(
lines,
LineSpec {
indent,
name,
name_width,
type_width,
type_label: &type_label,
value_repr: &value_repr,
},
);
}
}
fn push_line(&self, lines: &mut Vec<String>, spec: LineSpec<'_>) {
let indent_str = " ".repeat(spec.indent);
let name_cell = if spec.name_width > 0 {
format!("{name:<width$}", name = spec.name, width = spec.name_width)
} else {
spec.name.to_string()
};
let effective_type_width = spec.type_width.max(spec.type_label.len());
let type_cell = format!(
"{type_label:<width$}",
type_label = spec.type_label,
width = effective_type_width
);
lines.push(format!(
"{indent}{name_cell} | {type_cell} | {value}",
indent = indent_str,
name_cell = name_cell,
type_cell = type_cell,
value = spec.value_repr
));
}
fn compute_key_width<'a, I>(&self, keys: I) -> usize
where
I: Iterator<Item = &'a str>,
{
keys.map(|k| k.len())
.max()
.unwrap_or(0)
.min(Self::KEY_WIDTH_CAP)
}
fn compute_type_width<'a, I>(&self, values: I) -> usize
where
I: Iterator<Item = &'a Dynamic>,
{
values
.map(|value| self.type_label_for(value).len())
.max()
.unwrap_or(0)
}
fn type_label_for(&self, value: &Dynamic) -> String {
if let Some(map) = value.clone().try_cast::<rhai::Map>() {
return format!("map({})", map.len());
}
if let Some(array) = value.clone().try_cast::<rhai::Array>() {
return format!("array({})", array.len());
}
if value.is_string() {
return "string".to_string();
}
if value.is_bool() {
return "bool".to_string();
}
if value.is_int() {
return "int".to_string();
}
if value.is_float() {
return "float".to_string();
}
if value.is_char() {
return "char".to_string();
}
if value.is_unit() {
return "null".to_string();
}
value.type_name().to_string()
}
fn describe_scalar(&self, value: &Dynamic) -> (String, String) {
if value.is_string() {
if let Ok(inner) = value.clone().into_string() {
let escaped = self.escape_for_display(&inner);
let (truncated, was_truncated) = self.truncate_value(&escaped);
let mut rendered = format!("\"{}\"", truncated);
if was_truncated {
rendered.push_str("...");
}
return ("string".to_string(), rendered);
}
}
if value.is_bool() {
if let Ok(b) = value.as_bool() {
return ("bool".to_string(), b.to_string());
}
}
if value.is_int() {
if let Ok(i) = value.as_int() {
return ("int".to_string(), i.to_string());
}
}
if value.is_float() {
if let Ok(f) = value.as_float() {
return ("float".to_string(), format!("{f}"));
}
}
if value.is_char() {
if let Ok(c) = value.as_char() {
return (
"char".to_string(),
format!("'{}'", self.escape_for_display(&c.to_string())),
);
}
}
if value.is_unit() {
return ("null".to_string(), "null".to_string());
}
let type_label = value.type_name().to_string();
let rendered = self.escape_for_display(&value.to_string());
let (truncated, was_truncated) = self.truncate_value(&rendered);
let mut repr = truncated;
if was_truncated {
repr.push_str("...");
}
(type_label, repr)
}
fn escape_for_display(&self, input: &str) -> String {
let mut escaped = String::with_capacity(input.len());
for ch in input.chars() {
match ch {
'\\' => escaped.push_str("\\\\"),
'\n' => escaped.push_str("\\n"),
'\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
c if c.is_control() => {
escaped.push_str(&format!("\\x{:02X}", c as u32));
}
c => escaped.push(c),
}
}
escaped
}
fn truncate_value(&self, value: &str) -> (String, bool) {
if self.max_inline_chars == usize::MAX || value.chars().count() <= self.max_inline_chars {
return (value.to_string(), false);
}
let truncated: String = value.chars().take(self.max_inline_chars).collect();
(truncated, true)
}
}
impl pipeline::Formatter for InspectFormatter {
fn format(&self, event: &Event) -> String {
if event.fields.is_empty() {
return "---".to_string();
}
let mut lines = Vec::new();
self.format_entries(
&mut lines,
crate::event::ordered_fields(event)
.into_iter()
.map(|(k, v)| (k.as_str(), v)),
0,
);
lines.insert(0, "---".to_string());
lines.join("\n")
}
}