tabled/tables/
extended.rs

1//! This module contains an [`ExtendedTable`] structure which is useful in cases where
2//! a structure has a lot of fields.
3
4use crate::grid::util::string::get_line_width;
5use crate::Tabled;
6use std::cell::RefCell;
7use std::fmt::{self, Debug, Display};
8use std::rc::Rc;
9
10/// `ExtendedTable` display data in a 'expanded display mode' from postgresql.
11/// It may be useful for a large data sets with a lot of fields.
12///
13/// See 'Examples' in <https://www.postgresql.org/docs/current/app-psql.html>.
14///
15/// It escapes strings to resolve a multi-line ones.
16/// Because of that ANSI sequences will be not be rendered too so colores will not be showed.
17///
18#[cfg_attr(feature = "derive", doc = "```")]
19#[cfg_attr(not(feature = "derive"), doc = "```ignore")]
20/// use tabled::{Tabled, tables::ExtendedTable};
21///
22/// #[derive(Tabled)]
23/// struct Language {
24///     name: &'static str,
25///     designed_by: &'static str,
26///     invented_year: usize,
27/// }
28///
29/// let languages = vec![
30///     Language{ name: "C", designed_by: "Dennis Ritchie", invented_year: 1972 },
31///     Language{ name: "Rust", designed_by: "Graydon Hoare", invented_year: 2010 },
32///     Language{ name: "Go", designed_by: "Rob Pike", invented_year: 2009 },
33/// ];
34///
35/// let table = ExtendedTable::new(languages).to_string();
36///
37/// let expected = "-[ RECORD 0 ]-+---------------\n\
38///                 name          | C\n\
39///                 designed_by   | Dennis Ritchie\n\
40///                 invented_year | 1972\n\
41///                 -[ RECORD 1 ]-+---------------\n\
42///                 name          | Rust\n\
43///                 designed_by   | Graydon Hoare\n\
44///                 invented_year | 2010\n\
45///                 -[ RECORD 2 ]-+---------------\n\
46///                 name          | Go\n\
47///                 designed_by   | Rob Pike\n\
48///                 invented_year | 2009";
49///
50/// assert_eq!(table, expected);
51/// ```
52#[derive(Clone)]
53pub struct ExtendedTable {
54    fields: Vec<String>,
55    records: Vec<Vec<String>>,
56    template: Rc<RefCell<dyn Fn(usize) -> String>>,
57}
58
59impl ExtendedTable {
60    /// Creates a new instance of `ExtendedTable`
61    pub fn new<T>(iter: impl IntoIterator<Item = T>) -> Self
62    where
63        T: Tabled,
64    {
65        let data = iter
66            .into_iter()
67            .map(|i| {
68                i.fields()
69                    .into_iter()
70                    .map(|s| s.escape_debug().to_string())
71                    .collect()
72            })
73            .collect();
74        let header = T::headers()
75            .into_iter()
76            .map(|s| s.escape_debug().to_string())
77            .collect();
78
79        Self {
80            records: data,
81            fields: header,
82            template: Rc::new(RefCell::new(record_template)),
83        }
84    }
85
86    /// Truncates table to a set width value for a table.
87    /// It returns a success inticator, where `false` means it's not possible to set the table width,
88    /// because of the given arguments.
89    ///
90    /// It tries to not affect fields, but if there's no enough space all records will be deleted and fields will be cut.
91    ///
92    /// The minimum width is 14.
93    pub fn truncate(&mut self, max: usize, suffix: &str) -> bool {
94        // -[ RECORD 0 ]-
95        let teplate_width = self.records.len().to_string().len() + 13;
96        let min_width = teplate_width;
97        if max < min_width {
98            return false;
99        }
100
101        let suffix_width = get_line_width(suffix);
102        if max < suffix_width {
103            return false;
104        }
105
106        let max = max - suffix_width;
107
108        let fields_max_width = self
109            .fields
110            .iter()
111            .map(|s| get_line_width(s))
112            .max()
113            .unwrap_or_default();
114
115        // 3 is a space for ' | '
116        let fields_affected = max < fields_max_width + 3;
117        if fields_affected {
118            if max < 3 {
119                return false;
120            }
121
122            let max = max - 3;
123
124            if max < suffix_width {
125                return false;
126            }
127
128            let max = max - suffix_width;
129
130            truncate_fields(&mut self.fields, max, suffix);
131            truncate_records(&mut self.records, 0, suffix);
132        } else {
133            let max = max - fields_max_width - 3 - suffix_width;
134            truncate_records(&mut self.records, max, suffix);
135        }
136
137        true
138    }
139
140    /// Sets the template for a record.
141    pub fn template<F>(mut self, template: F) -> Self
142    where
143        F: Fn(usize) -> String + 'static,
144    {
145        self.template = Rc::new(RefCell::new(template));
146        self
147    }
148}
149
150impl From<Vec<Vec<String>>> for ExtendedTable {
151    fn from(mut data: Vec<Vec<String>>) -> Self {
152        if data.is_empty() {
153            return Self {
154                fields: vec![],
155                records: vec![],
156                template: Rc::new(RefCell::new(record_template)),
157            };
158        }
159
160        let fields = data.remove(0);
161
162        Self {
163            fields,
164            records: data,
165            template: Rc::new(RefCell::new(record_template)),
166        }
167    }
168}
169
170impl Display for ExtendedTable {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        if self.records.is_empty() {
173            return Ok(());
174        }
175
176        // It's possible that field|header can be a multiline string so
177        // we escape it and trim \" chars.
178        let fields = self.fields.iter().collect::<Vec<_>>();
179
180        let max_field_width = fields
181            .iter()
182            .map(|s| get_line_width(s))
183            .max()
184            .unwrap_or_default();
185
186        let max_values_length = self
187            .records
188            .iter()
189            .map(|record| record.iter().map(|s| get_line_width(s)).max())
190            .max()
191            .unwrap_or_default()
192            .unwrap_or_default();
193
194        for (i, records) in self.records.iter().enumerate() {
195            write_header_template(f, &self.template, i, max_field_width, max_values_length)?;
196
197            for (value, field) in records.iter().zip(fields.iter()) {
198                writeln!(f)?;
199                write_record(f, field, value, max_field_width)?;
200            }
201
202            let is_last_record = i + 1 == self.records.len();
203            if !is_last_record {
204                writeln!(f)?;
205            }
206        }
207
208        Ok(())
209    }
210}
211
212impl Debug for ExtendedTable {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        f.debug_struct("ExtendedTable")
215            .field("fields", &self.fields)
216            .field("records", &self.records)
217            .finish_non_exhaustive()
218    }
219}
220
221fn truncate_records(records: &mut Vec<Vec<String>>, max_width: usize, suffix: &str) {
222    for fields in records {
223        truncate_fields(fields, max_width, suffix);
224    }
225}
226
227fn truncate_fields(records: &mut Vec<String>, max_width: usize, suffix: &str) {
228    for text in records {
229        truncate(text, max_width, suffix);
230    }
231}
232
233fn write_header_template(
234    f: &mut fmt::Formatter<'_>,
235    template: &Rc<RefCell<dyn Fn(usize) -> String>>,
236    index: usize,
237    max_field_width: usize,
238    max_values_length: usize,
239) -> fmt::Result {
240    let record_template = template.borrow()(index);
241    let mut template = format!("-{record_template}-");
242    let default_template_length = template.len();
243
244    // 3 - is responsible for ' | ' formatting
245    let max_line_width = std::cmp::max(
246        max_field_width + 3 + max_values_length,
247        default_template_length,
248    );
249    let rest_to_print = max_line_width - default_template_length;
250    if rest_to_print > 0 {
251        // + 1 is a space after field name and we get a next pos so its +2
252        if max_field_width + 2 > default_template_length {
253            let part1 = (max_field_width + 1) - default_template_length;
254            let part2 = rest_to_print - part1 - 1;
255
256            template.extend(
257                std::iter::repeat_n('-', part1)
258                    .chain(std::iter::once('+'))
259                    .chain(std::iter::repeat_n('-', part2)),
260            );
261        } else {
262            template.extend(std::iter::repeat_n('-', rest_to_print));
263        }
264    }
265
266    write!(f, "{template}")?;
267
268    Ok(())
269}
270
271fn write_record(
272    f: &mut fmt::Formatter<'_>,
273    field: &str,
274    value: &str,
275    max_field_width: usize,
276) -> fmt::Result {
277    write!(f, "{field:max_field_width$} | {value}")
278}
279
280fn truncate(text: &mut String, max: usize, suffix: &str) {
281    let original_len = text.len();
282
283    if max == 0 || text.is_empty() {
284        *text = String::new();
285    } else {
286        *text = crate::util::string::cut_str(text, max).into_owned();
287    }
288
289    let cut_was_done = text.len() < original_len;
290    if !suffix.is_empty() && cut_was_done {
291        text.push_str(suffix);
292    }
293}
294
295fn record_template(index: usize) -> String {
296    format!("[ RECORD {index} ]")
297}