1use std::borrow::Cow;
4use std::collections::HashMap;
5
6use rich::{Console, ConsoleOptions, Justify, Renderable, Segment, Table, Text};
7
8use super::{escape_controls, scalar_text, style, truncate_chars, Node, Value};
9
10const INLINE_LIMIT: usize = 40;
12
13#[derive(Clone, Debug, Default)]
15pub struct TableOptions {
16 pub columns: Option<Vec<String>>,
18 pub headers: HashMap<String, String>,
20 pub justify: HashMap<String, Justify>,
23 pub title: Option<String>,
25 pub max_rows: Option<usize>,
27 pub max_string: Option<usize>,
29}
30
31impl TableOptions {
32 pub fn new() -> Self {
33 Self::default()
34 }
35 pub fn columns<S: Into<String>>(mut self, columns: impl IntoIterator<Item = S>) -> Self {
37 self.columns = Some(columns.into_iter().map(Into::into).collect());
38 self
39 }
40 pub fn header(mut self, column: impl Into<String>, header: impl Into<String>) -> Self {
42 self.headers.insert(column.into(), header.into());
43 self
44 }
45 pub fn justify(mut self, column: impl Into<String>, justify: Justify) -> Self {
47 self.justify.insert(column.into(), justify);
48 self
49 }
50 pub fn title(mut self, title: impl Into<String>) -> Self {
51 self.title = Some(title.into());
52 self
53 }
54 pub fn max_rows(mut self, max_rows: usize) -> Self {
55 self.max_rows = Some(max_rows);
56 self
57 }
58 pub fn max_string(mut self, max_string: usize) -> Self {
59 self.max_string = Some(max_string);
60 self
61 }
62}
63
64#[derive(Clone, Debug)]
81pub struct TableView<'a> {
82 node: Cow<'a, Node>,
83 options: TableOptions,
84}
85
86impl<'a> TableView<'a> {
87 pub fn new(node: impl Into<Cow<'a, Node>>) -> Self {
88 TableView {
89 node: node.into(),
90 options: TableOptions::default(),
91 }
92 }
93 pub fn options(mut self, options: TableOptions) -> Self {
95 self.options = options;
96 self
97 }
98 pub fn columns<S: Into<String>>(mut self, columns: impl IntoIterator<Item = S>) -> Self {
100 self.options = self.options.columns(columns);
101 self
102 }
103 pub fn header(mut self, column: impl Into<String>, header: impl Into<String>) -> Self {
105 self.options = self.options.header(column, header);
106 self
107 }
108 pub fn justify(mut self, column: impl Into<String>, justify: Justify) -> Self {
110 self.options = self.options.justify(column, justify);
111 self
112 }
113 pub fn title(mut self, title: impl Into<String>) -> Self {
114 self.options = self.options.title(title);
115 self
116 }
117 pub fn max_rows(mut self, max_rows: usize) -> Self {
118 self.options = self.options.max_rows(max_rows);
119 self
120 }
121 pub fn max_string(mut self, max_string: usize) -> Self {
122 self.options = self.options.max_string(max_string);
123 self
124 }
125
126 fn records(&self) -> Vec<&Node> {
127 match &self.node.value {
128 Value::Seq(items) => items.iter().collect(),
129 _ => vec![&*self.node],
130 }
131 }
132
133 pub fn to_table(&self, console: &Console) -> Table {
135 let records = self.records();
136 let columns: Vec<String> = match &self.options.columns {
137 Some(columns) => columns.clone(),
138 None => {
139 let mut columns: Vec<String> = Vec::new();
140 let mut seen = std::collections::HashSet::new();
141 for record in &records {
142 match &record.value {
143 Value::Map(entries) => {
144 for (key, _) in entries {
145 if seen.insert(key.clone()) {
146 columns.push(key.clone());
147 }
148 }
149 }
150 _ => {
151 if seen.insert("value".to_string()) {
152 columns.push("value".to_string());
153 }
154 }
155 }
156 }
157 columns
158 }
159 };
160 let shown = self
161 .options
162 .max_rows
163 .unwrap_or(usize::MAX)
164 .min(records.len());
165
166 let mut table = Table::new();
167 if let Some(title) = &self.options.title {
168 table = table.title(rich::markup::escape(title));
169 }
170 if shown < records.len() {
171 table = table.caption(format!("… {} more", records.len() - shown));
172 }
173 for column in &columns {
174 let justify = self
175 .options
176 .justify
177 .get(column)
178 .copied()
179 .unwrap_or_else(|| {
180 let mut values = records
181 .iter()
182 .filter_map(|r| cell(r, column))
183 .filter(|n| n.value != Value::Null)
184 .peekable();
185 let numeric = values.peek().is_some()
186 && values.all(|n| {
187 matches!(n.value, Value::Int(_) | Value::UInt(_) | Value::Float(_))
188 });
189 if numeric {
190 Justify::Right
191 } else {
192 Justify::Left
193 }
194 });
195 let header = self
196 .options
197 .headers
198 .get(column)
199 .map_or(column.as_str(), String::as_str);
200 table.add_column_text(Text::new(escape_controls(header)), justify);
201 }
202 for record in records.iter().take(shown) {
203 let row = columns
204 .iter()
205 .map(|column| match cell(record, column) {
206 Some(node) => cell_text(console, node, self.options.max_string),
207 None => Text::new(""),
208 })
209 .collect();
210 table.add_row_text(row);
211 }
212 table
213 }
214}
215
216fn cell<'n>(record: &'n Node, column: &str) -> Option<&'n Node> {
217 match &record.value {
218 Value::Map(entries) => entries
219 .iter()
220 .rev()
221 .find(|(k, _)| k == column)
222 .map(|(_, v)| v),
223 _ if column == "value" => Some(record),
224 _ => None,
225 }
226}
227
228pub(crate) fn cell_text(console: &Console, node: &Node, max_string: Option<usize>) -> Text {
230 match &node.value {
231 Value::Null => Text::styled("null", style(console, "data.null")),
232 Value::Seq(_) | Value::Map(_) => {
233 let json = node.to_json().to_string();
234 let limit = max_string.unwrap_or(INLINE_LIMIT);
235 Text::styled(
236 escape_controls(&truncate_chars(&json, limit)),
237 style(console, "data.summary"),
238 )
239 }
240 Value::String(s) => {
241 let shown = max_string.map_or(Cow::Borrowed(s.as_str()), |n| truncate_chars(s, n));
242 Text::new(escape_controls(&shown))
243 }
244 other => {
245 let (text, key) = scalar_text(other, false);
246 Text::styled(text, style(console, key))
247 }
248 }
249}
250
251impl Renderable for TableView<'_> {
252 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
253 self.to_table(console).rich_render(console, options)
254 }
255}