Skip to main content

rich_ext/data/
table.rs

1//! Records as a table: one row per record, one column per key.
2
3use 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
10/// How nested values are cut in a cell when no `max_string` is set.
11const INLINE_LIMIT: usize = 40;
12
13/// Overrides for a [`TableView`].
14#[derive(Clone, Debug, Default)]
15pub struct TableOptions {
16    /// Show only these columns, in this order.
17    pub columns: Option<Vec<String>>,
18    /// Header text per column key (default: the key).
19    pub headers: HashMap<String, String>,
20    /// Justification per column key (default: right for all-number
21    /// columns, left otherwise).
22    pub justify: HashMap<String, Justify>,
23    /// A title above the table (plain text, not markup).
24    pub title: Option<String>,
25    /// Show at most this many rows, then `… N more`.
26    pub max_rows: Option<usize>,
27    /// Cut strings (and inline nested values) to this many characters.
28    pub max_string: Option<usize>,
29}
30
31impl TableOptions {
32    pub fn new() -> Self {
33        Self::default()
34    }
35    /// Select and order the columns.
36    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    /// Rename a column's header.
41    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    /// Justify a column.
46    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/// A sequence of records as a table.
65///
66/// Columns are the union of the records' keys in first-seen order; a record
67/// that is not a map fills a `value` column. Missing cells stay empty,
68/// nested values show as compact JSON, numbers are right-justified and nulls
69/// dim. A map renders as one record; a scalar as a one-cell table.
70///
71/// ```
72/// use rich::Console;
73/// use rich_ext::data::{from_serialize, TableView};
74/// use serde_json::json;
75///
76/// let rows = from_serialize(&json!([{"name": "a", "port": 80}, {"name": "b"}])).unwrap();
77/// let out = Console::builder().width(30).build().render_export(&TableView::new(&rows));
78/// assert!(out.contains("│ a    │   80 │"), "{out}");
79/// ```
80#[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    /// Replace all options.
94    pub fn options(mut self, options: TableOptions) -> Self {
95        self.options = options;
96        self
97    }
98    /// See [`TableOptions::columns`].
99    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    /// See [`TableOptions::header`].
104    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    /// See [`TableOptions::justify`].
109    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    /// The core table.
134    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
228/// A value in a table cell: unquoted scalars, compact JSON for containers.
229pub(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}