use crate::error::TraverserResult;
use crate::traversal::render::{CUT_MARKER, ItemLimit};
use crate::traversal::table::format_cell;
use std::borrow::Cow;
use crate::value::ElementProperty;
use crate::value::physical::to_display_text;
pub(crate) fn format_elements_as_tree(
elements: &[ElementProperty],
limit: &ItemLimit,
) -> TraverserResult<String> {
if elements.is_empty() {
return Ok(String::new());
}
let mut out = String::new();
let (roots, cut) = flatten_roots(elements, limit);
for (key, value) in roots {
render_node(key.as_deref(), value, "", "", limit, &mut out);
}
if cut {
out.push_str(CUT_MARKER);
out.push('\n');
}
Ok(out.trim_end().to_string())
}
fn flatten_roots<'e>(
elements: &'e [ElementProperty],
limit: &ItemLimit,
) -> (Vec<(Option<String>, &'e ElementProperty)>, bool) {
if let [ElementProperty::Object(obj)] = elements
&& !obj.is_empty()
{
let keep = limit.keep(obj.len());
let roots = obj
.iter()
.take(keep)
.map(|(k, v)| (Some(k.to_string()), v))
.collect();
return (roots, keep < obj.len());
}
if let [ElementProperty::Map(map)] = elements {
let keep = limit.keep(map.len());
let roots = map
.iter()
.take(keep)
.map(|(k, v)| (Some(to_display_text(k).to_string()), v))
.collect();
return (roots, keep < map.len());
}
let roots = elements
.iter()
.enumerate()
.map(|(i, e)| match e {
ElementProperty::Object(obj) if !obj.is_empty() => (Some(format!("[{i}]")), e),
ElementProperty::Map(_) => (Some(format!("[{i}]")), e),
_ => (None, e),
})
.collect();
(roots, false)
}
fn render_node(
key: Option<&str>,
value: &ElementProperty,
header_prefix: &str,
child_prefix: &str,
limit: &ItemLimit,
out: &mut String,
) {
match value {
ElementProperty::Object(obj) if !obj.is_empty() => {
push_header(key, value, header_prefix, limit, out);
render_children(
obj.len(),
obj.iter().map(|(k, v)| (Cow::Borrowed(k.as_str()), v)),
child_prefix,
limit,
out,
);
}
ElementProperty::Map(map) => {
push_header(key, value, header_prefix, limit, out);
let entries = map
.iter()
.map(|(k, v)| (Cow::Owned(to_display_text(k).to_string()), v));
render_children(map.len(), entries, child_prefix, limit, out);
}
ElementProperty::Object(_) => push_header(key, value, header_prefix, limit, out),
other => {
out.push_str(header_prefix);
match key {
Some(k) => {
out.push_str(k);
out.push_str(": ");
out.push_str(&format_cell(other, limit));
}
None => out.push_str(&format_cell(other, limit)),
}
out.push('\n');
}
}
}
fn push_header(
key: Option<&str>,
value: &ElementProperty,
header_prefix: &str,
limit: &ItemLimit,
out: &mut String,
) {
out.push_str(header_prefix);
match key {
Some(k) => out.push_str(k),
None => out.push_str(&format_cell(value, limit)),
}
out.push('\n');
}
fn render_children<'a>(
len: usize,
obj: impl Iterator<Item = (Cow<'a, str>, &'a ElementProperty)>,
prefix: &str,
limit: &ItemLimit,
out: &mut String,
) {
let keep = limit.keep(len);
let cut = keep < len;
let entries: Vec<_> = obj.take(keep).collect();
let last_idx = entries.len().saturating_sub(1);
for (i, (key, value)) in entries.into_iter().enumerate() {
let is_last = i == last_idx && !cut;
let header_prefix = format!("{prefix}{}", if is_last { "└─ " } else { "├─ " });
let child_prefix = format!("{prefix}{}", if is_last { " " } else { "│ " });
render_node(
Some(key.as_ref()),
value,
&header_prefix,
&child_prefix,
limit,
out,
);
}
if cut {
out.push_str(prefix);
out.push_str("└─ ");
out.push_str(CUT_MARKER);
out.push('\n');
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::PropertyMap;
use compact_str::CompactString;
fn obj(pairs: Vec<(&str, ElementProperty)>) -> ElementProperty {
let mut map = PropertyMap::default();
for (k, v) in pairs {
map.insert(CompactString::new(k), v);
}
ElementProperty::Object(Box::new(map))
}
fn leaf() -> ElementProperty {
obj(vec![])
}
#[test]
fn single_root_tree_shape() {
let tree = obj(vec![(
"v[1]",
obj(vec![(
"v[4]",
obj(vec![("v[5]", leaf()), ("v[3]", leaf())]),
)]),
)]);
let rendered =
format_elements_as_tree(std::slice::from_ref(&tree), &ItemLimit::unlimited()).unwrap();
assert_eq!(rendered, "v[1]\n└─ v[4]\n ├─ v[5]\n └─ v[3]");
}
#[test]
fn flat_non_nested_list_degrades_to_root_lines() {
let elements = vec![
ElementProperty::String("a".into()),
ElementProperty::String("b".into()),
];
let rendered = format_elements_as_tree(&elements, &ItemLimit::unlimited()).unwrap();
assert_eq!(rendered, "a\nb");
}
#[test]
fn nested_object_with_scalar_leaves() {
let el = obj(vec![
("name", ElementProperty::String("marko".into())),
("age", ElementProperty::Number(29.into())),
]);
let rendered =
format_elements_as_tree(std::slice::from_ref(&el), &ItemLimit::unlimited()).unwrap();
assert_eq!(rendered, "name: marko\nage: 29");
}
#[test]
fn three_level_branching_merge_shape() {
let tree = obj(vec![(
"root",
obj(vec![("a", obj(vec![("a1", leaf())])), ("b", leaf())]),
)]);
let rendered =
format_elements_as_tree(std::slice::from_ref(&tree), &ItemLimit::unlimited()).unwrap();
assert_eq!(rendered, "root\n├─ a\n│ └─ a1\n└─ b");
}
#[test]
fn multiple_object_roots_get_index_keys_not_duplicated_content() {
let elements = vec![
obj(vec![("name", ElementProperty::String("marko".into()))]),
obj(vec![("name", ElementProperty::String("vadas".into()))]),
];
let rendered = format_elements_as_tree(&elements, &ItemLimit::unlimited()).unwrap();
assert_eq!(rendered, "[0]\n└─ name: marko\n[1]\n└─ name: vadas");
}
#[test]
fn nested_entries_are_cut_with_a_marker_line() {
let tree = obj(vec![(
"root",
obj(vec![("a", leaf()), ("b", leaf()), ("c", leaf())]),
)]);
let limit = ItemLimit::new(Some(2));
let rendered = format_elements_as_tree(std::slice::from_ref(&tree), &limit).unwrap();
assert_eq!(rendered, "root\n├─ a\n├─ b\n└─ …");
assert!(limit.was_cut());
}
#[test]
fn empty_input() {
assert_eq!(
format_elements_as_tree(&[], &ItemLimit::unlimited()).unwrap(),
""
);
}
}