use std::borrow::Cow;
use std::collections::HashMap;
use rich::{Console, ConsoleOptions, Justify, Renderable, Segment, Table, Text};
use super::{escape_controls, scalar_text, style, truncate_chars, Node, Value};
const INLINE_LIMIT: usize = 40;
#[derive(Clone, Debug, Default)]
pub struct TableOptions {
pub columns: Option<Vec<String>>,
pub headers: HashMap<String, String>,
pub justify: HashMap<String, Justify>,
pub title: Option<String>,
pub max_rows: Option<usize>,
pub max_string: Option<usize>,
}
impl TableOptions {
pub fn new() -> Self {
Self::default()
}
pub fn columns<S: Into<String>>(mut self, columns: impl IntoIterator<Item = S>) -> Self {
self.columns = Some(columns.into_iter().map(Into::into).collect());
self
}
pub fn header(mut self, column: impl Into<String>, header: impl Into<String>) -> Self {
self.headers.insert(column.into(), header.into());
self
}
pub fn justify(mut self, column: impl Into<String>, justify: Justify) -> Self {
self.justify.insert(column.into(), justify);
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn max_rows(mut self, max_rows: usize) -> Self {
self.max_rows = Some(max_rows);
self
}
pub fn max_string(mut self, max_string: usize) -> Self {
self.max_string = Some(max_string);
self
}
}
#[derive(Clone, Debug)]
pub struct TableView<'a> {
node: Cow<'a, Node>,
options: TableOptions,
}
impl<'a> TableView<'a> {
pub fn new(node: impl Into<Cow<'a, Node>>) -> Self {
TableView {
node: node.into(),
options: TableOptions::default(),
}
}
pub fn options(mut self, options: TableOptions) -> Self {
self.options = options;
self
}
pub fn columns<S: Into<String>>(mut self, columns: impl IntoIterator<Item = S>) -> Self {
self.options = self.options.columns(columns);
self
}
pub fn header(mut self, column: impl Into<String>, header: impl Into<String>) -> Self {
self.options = self.options.header(column, header);
self
}
pub fn justify(mut self, column: impl Into<String>, justify: Justify) -> Self {
self.options = self.options.justify(column, justify);
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.options = self.options.title(title);
self
}
pub fn max_rows(mut self, max_rows: usize) -> Self {
self.options = self.options.max_rows(max_rows);
self
}
pub fn max_string(mut self, max_string: usize) -> Self {
self.options = self.options.max_string(max_string);
self
}
fn records(&self) -> Vec<&Node> {
match &self.node.value {
Value::Seq(items) => items.iter().collect(),
_ => vec![&*self.node],
}
}
pub fn to_table(&self, console: &Console) -> Table {
let records = self.records();
let columns: Vec<String> = match &self.options.columns {
Some(columns) => columns.clone(),
None => {
let mut columns: Vec<String> = Vec::new();
let mut seen = std::collections::HashSet::new();
for record in &records {
match &record.value {
Value::Map(entries) => {
for (key, _) in entries {
if seen.insert(key.clone()) {
columns.push(key.clone());
}
}
}
_ => {
if seen.insert("value".to_string()) {
columns.push("value".to_string());
}
}
}
}
columns
}
};
let shown = self
.options
.max_rows
.unwrap_or(usize::MAX)
.min(records.len());
let mut table = Table::new();
if let Some(title) = &self.options.title {
table = table.title(rich::markup::escape(title));
}
if shown < records.len() {
table = table.caption(format!("… {} more", records.len() - shown));
}
for column in &columns {
let justify = self
.options
.justify
.get(column)
.copied()
.unwrap_or_else(|| {
let mut values = records
.iter()
.filter_map(|r| cell(r, column))
.filter(|n| n.value != Value::Null)
.peekable();
let numeric = values.peek().is_some()
&& values.all(|n| {
matches!(n.value, Value::Int(_) | Value::UInt(_) | Value::Float(_))
});
if numeric {
Justify::Right
} else {
Justify::Left
}
});
let header = self
.options
.headers
.get(column)
.map_or(column.as_str(), String::as_str);
table.add_column_text(Text::new(escape_controls(header)), justify);
}
for record in records.iter().take(shown) {
let row = columns
.iter()
.map(|column| match cell(record, column) {
Some(node) => cell_text(console, node, self.options.max_string),
None => Text::new(""),
})
.collect();
table.add_row_text(row);
}
table
}
}
fn cell<'n>(record: &'n Node, column: &str) -> Option<&'n Node> {
match &record.value {
Value::Map(entries) => entries
.iter()
.rev()
.find(|(k, _)| k == column)
.map(|(_, v)| v),
_ if column == "value" => Some(record),
_ => None,
}
}
pub(crate) fn cell_text(console: &Console, node: &Node, max_string: Option<usize>) -> Text {
match &node.value {
Value::Null => Text::styled("null", style(console, "data.null")),
Value::Seq(_) | Value::Map(_) => {
let json = node.to_json().to_string();
let limit = max_string.unwrap_or(INLINE_LIMIT);
Text::styled(
escape_controls(&truncate_chars(&json, limit)),
style(console, "data.summary"),
)
}
Value::String(s) => {
let shown = max_string.map_or(Cow::Borrowed(s.as_str()), |n| truncate_chars(s, n));
Text::new(escape_controls(&shown))
}
other => {
let (text, key) = scalar_text(other, false);
Text::styled(text, style(console, key))
}
}
}
impl Renderable for TableView<'_> {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
self.to_table(console).rich_render(console, options)
}
}