use std::borrow::Cow;
use std::collections::HashSet;
use std::sync::Arc;
use rich::measure::Measurement;
use rich::table::Cell;
use rich::{Console, ConsoleOptions, Justify, Renderable, Segment, Table, Text};
use super::table::cell_text;
use super::{Explorer, Node, Path, PathSegment, escape_controls, quote_str, style, summary};
use super::{Redactor, Value};
pub trait OpenBranch {
fn open_branch(&mut self, path: &Path) -> bool;
fn close_branch(&mut self, path: &Path) -> bool;
}
#[derive(Clone, Debug)]
#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
pub struct RecordView<'a> {
node: Cow<'a, Node>,
depth: usize,
expanded: HashSet<Path>,
collapsed: HashSet<Path>,
max_items: Option<usize>,
max_string: Option<usize>,
show_types: bool,
title: Option<String>,
}
impl<'a> RecordView<'a> {
pub fn new(node: impl Into<Cow<'a, Node>>) -> Self {
RecordView {
node: node.into(),
depth: 1,
expanded: HashSet::new(),
collapsed: HashSet::new(),
max_items: Some(20),
max_string: Some(200),
show_types: true,
title: None,
}
}
pub fn depth(mut self, depth: usize) -> Self {
self.depth = depth;
self
}
pub fn expand(mut self, path: Path) -> Self {
self.open_branch(&path);
self
}
pub fn collapse(mut self, path: Path) -> Self {
self.close_branch(&path);
self
}
pub fn max_items(mut self, items: impl Into<Option<usize>>) -> Self {
self.max_items = items.into();
self
}
pub fn max_string(mut self, length: impl Into<Option<usize>>) -> Self {
self.max_string = length.into();
self
}
pub fn show_types(mut self, show: bool) -> Self {
self.show_types = show;
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn redact(mut self, redactor: &dyn Redactor) -> Self {
self.node = Cow::Owned(self.node.redacted(redactor));
self
}
pub fn node(&self) -> &Node {
&self.node
}
pub fn branches(&self) -> Vec<Path> {
let root = Path::root();
fields(&self.node)
.into_iter()
.filter(|(_, node)| node.is_container() && !node.is_empty())
.map(|(segment, _)| child(&root, segment))
.collect()
}
pub fn is_open(&self, path: &Path) -> bool {
if path.is_root() {
return true;
}
if self.collapsed.contains(path) {
return false;
}
path.len() <= self.depth
|| self
.expanded
.iter()
.any(|open| open.segments().starts_with(path.segments()))
}
pub fn branch(&self, path: &Path) -> Option<Explorer<'static>> {
let node = self.node.at(path)?;
node.is_container().then(|| self.tree(path, node))
}
fn tree(&self, path: &Path, node: &Node) -> Explorer<'static> {
let limit = self.max_items.unwrap_or(usize::MAX);
let mut explorer = Explorer::new(node.clone());
if let Some(items) = self.max_items {
explorer = explorer.max_length(items);
}
if let Some(length) = self.max_string {
explorer = explorer.max_string(length);
}
let mut stack: Vec<(Path, Path, &Node)> = vec![(path.clone(), Path::root(), node)];
while let Some((absolute, relative, node)) = stack.pop() {
if !node.is_container() || node.is_empty() {
continue;
}
if !self.is_open(&absolute) && !absolute.is_root() {
explorer = explorer.fold(relative);
continue;
}
for (segment, child_node) in fields(node).into_iter().take(limit) {
stack.push((
child(&absolute, segment.clone()),
child(&relative, segment),
child_node,
));
}
}
explorer
}
pub fn to_table(&self, console: &Console) -> Table {
let mut table = Table::new();
if let Some(title) = &self.title {
table = table.title(rich::markup::escape(title));
}
table.add_column_text(Text::new("field"), Justify::Left);
if self.show_types {
table.add_column_text(Text::new("type"), Justify::Left);
}
table.add_column_text(Text::new("value"), Justify::Left);
let type_cell =
|node: &Node| Cell::Text(Text::styled(node.type_name(), style(console, "data.type")));
if !self.node.is_container() {
let mut row = vec![Cell::Text(Text::styled(
"(value)",
style(console, "data.summary"),
))];
if self.show_types {
row.push(type_cell(&self.node));
}
row.push(Cell::Text(self.scalar(console, &self.node)));
table.add_row_cells(row);
return table;
}
let root = Path::root();
let entries = fields(&self.node);
let total = entries.len();
let shown = match &self.node.value {
Value::Seq(_) => self.max_items.unwrap_or(usize::MAX).min(total),
_ => total,
};
for (segment, node) in entries.into_iter().take(shown) {
let name = match &segment {
PathSegment::Key(key) => {
Text::styled(escape_controls(key), style(console, "json.key"))
}
PathSegment::Index(index) => {
Text::styled(format!("[{index}]"), style(console, "data.index"))
}
};
let path = child(&root, segment);
let value = if node.is_container() && !node.is_empty() && self.is_open(&path) {
Cell::Renderable(Arc::new(self.tree(&path, node)))
} else if node.is_container() {
Cell::Text(Text::styled(summary(node), style(console, "data.summary")))
} else {
Cell::Text(self.scalar(console, node))
};
let mut row = vec![Cell::Text(name)];
if self.show_types {
row.push(type_cell(node));
}
row.push(value);
table.add_row_cells(row);
}
if shown < total {
table = table.caption(format!("… {} more", total - shown));
}
table
}
fn scalar(&self, console: &Console, node: &Node) -> Text {
let Value::String(s) = &node.value else {
return cell_text(console, node, None);
};
let count = s.chars().count();
match self.max_string.filter(|&max| count > max) {
Some(max) => {
let cut: String = s.chars().take(max).collect();
let mut text = Text::new(format!("{}…", escape_controls(&cut)));
text.append(
&format!(" ({count} chars)"),
Some(style(console, "data.summary").into()),
);
text
}
None if !s.is_empty() && s.trim().is_empty() => {
Text::styled(quote_str(s), style(console, "json.str"))
}
None => Text::new(escape_controls(s)),
}
}
}
impl OpenBranch for RecordView<'_> {
fn open_branch(&mut self, path: &Path) -> bool {
let container = self.node.at(path).is_some_and(Node::is_container);
if container {
let mut at = Some(path.clone());
while let Some(p) = at {
self.collapsed.remove(&p);
at = p.parent();
}
self.expanded.insert(path.clone());
}
container
}
fn close_branch(&mut self, path: &Path) -> bool {
let container = self.node.at(path).is_some_and(Node::is_container);
if container {
self.expanded
.retain(|open| !open.segments().starts_with(path.segments()));
self.collapsed.insert(path.clone());
}
container
}
}
fn child(path: &Path, segment: PathSegment) -> Path {
let mut path = path.clone();
path.push(segment);
path
}
fn fields(node: &Node) -> Vec<(PathSegment, &Node)> {
match &node.value {
Value::Map(entries) => entries
.iter()
.map(|(key, value)| (PathSegment::Key(key.clone()), value))
.collect(),
Value::Seq(items) => items
.iter()
.enumerate()
.map(|(index, item)| (PathSegment::Index(index), item))
.collect(),
_ => Vec::new(),
}
}
impl Renderable for RecordView<'_> {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
self.to_table(console).rich_render(console, options)
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
self.to_table(console).measure(console, options)
}
}