Coverage Report

Created: 2026-01-25 06:45

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/tabled-0.20.0/src/tables/extended.rs
Line
Count
Source
1
//! This module contains an [`ExtendedTable`] structure which is useful in cases where
2
//! a structure has a lot of fields.
3
4
use crate::grid::util::string::get_line_width;
5
use crate::Tabled;
6
use std::cell::RefCell;
7
use std::fmt::{self, Debug, Display};
8
use 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)]
53
pub struct ExtendedTable {
54
    fields: Vec<String>,
55
    records: Vec<Vec<String>>,
56
    template: Rc<RefCell<dyn Fn(usize) -> String>>,
57
}
58
59
impl ExtendedTable {
60
    /// Creates a new instance of `ExtendedTable`
61
0
    pub fn new<T>(iter: impl IntoIterator<Item = T>) -> Self
62
0
    where
63
0
        T: Tabled,
64
    {
65
0
        let data = iter
66
0
            .into_iter()
67
0
            .map(|i| {
68
0
                i.fields()
69
0
                    .into_iter()
70
0
                    .map(|s| s.escape_debug().to_string())
71
0
                    .collect()
72
0
            })
73
0
            .collect();
74
0
        let header = T::headers()
75
0
            .into_iter()
76
0
            .map(|s| s.escape_debug().to_string())
77
0
            .collect();
78
79
0
        Self {
80
0
            records: data,
81
0
            fields: header,
82
0
            template: Rc::new(RefCell::new(record_template)),
83
0
        }
84
0
    }
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
0
    pub fn truncate(&mut self, max: usize, suffix: &str) -> bool {
94
        // -[ RECORD 0 ]-
95
0
        let teplate_width = self.records.len().to_string().len() + 13;
96
0
        let min_width = teplate_width;
97
0
        if max < min_width {
98
0
            return false;
99
0
        }
100
101
0
        let suffix_width = get_line_width(suffix);
102
0
        if max < suffix_width {
103
0
            return false;
104
0
        }
105
106
0
        let max = max - suffix_width;
107
108
0
        let fields_max_width = self
109
0
            .fields
110
0
            .iter()
111
0
            .map(|s| get_line_width(s))
112
0
            .max()
113
0
            .unwrap_or_default();
114
115
        // 3 is a space for ' | '
116
0
        let fields_affected = max < fields_max_width + 3;
117
0
        if fields_affected {
118
0
            if max < 3 {
119
0
                return false;
120
0
            }
121
122
0
            let max = max - 3;
123
124
0
            if max < suffix_width {
125
0
                return false;
126
0
            }
127
128
0
            let max = max - suffix_width;
129
130
0
            truncate_fields(&mut self.fields, max, suffix);
131
0
            truncate_records(&mut self.records, 0, suffix);
132
0
        } else {
133
0
            let max = max - fields_max_width - 3 - suffix_width;
134
0
            truncate_records(&mut self.records, max, suffix);
135
0
        }
136
137
0
        true
138
0
    }
139
140
    /// Sets the template for a record.
141
0
    pub fn template<F>(mut self, template: F) -> Self
142
0
    where
143
0
        F: Fn(usize) -> String + 'static,
144
    {
145
0
        self.template = Rc::new(RefCell::new(template));
146
0
        self
147
0
    }
148
}
149
150
impl From<Vec<Vec<String>>> for ExtendedTable {
151
0
    fn from(mut data: Vec<Vec<String>>) -> Self {
152
0
        if data.is_empty() {
153
0
            return Self {
154
0
                fields: vec![],
155
0
                records: vec![],
156
0
                template: Rc::new(RefCell::new(record_template)),
157
0
            };
158
0
        }
159
160
0
        let fields = data.remove(0);
161
162
0
        Self {
163
0
            fields,
164
0
            records: data,
165
0
            template: Rc::new(RefCell::new(record_template)),
166
0
        }
167
0
    }
168
}
169
170
impl Display for ExtendedTable {
171
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172
0
        if self.records.is_empty() {
173
0
            return Ok(());
174
0
        }
175
176
        // It's possible that field|header can be a multiline string so
177
        // we escape it and trim \" chars.
178
0
        let fields = self.fields.iter().collect::<Vec<_>>();
179
180
0
        let max_field_width = fields
181
0
            .iter()
182
0
            .map(|s| get_line_width(s))
183
0
            .max()
184
0
            .unwrap_or_default();
185
186
0
        let max_values_length = self
187
0
            .records
188
0
            .iter()
189
0
            .map(|record| record.iter().map(|s| get_line_width(s)).max())
190
0
            .max()
191
0
            .unwrap_or_default()
192
0
            .unwrap_or_default();
193
194
0
        for (i, records) in self.records.iter().enumerate() {
195
0
            write_header_template(f, &self.template, i, max_field_width, max_values_length)?;
196
197
0
            for (value, field) in records.iter().zip(fields.iter()) {
198
0
                writeln!(f)?;
199
0
                write_record(f, field, value, max_field_width)?;
200
            }
201
202
0
            let is_last_record = i + 1 == self.records.len();
203
0
            if !is_last_record {
204
0
                writeln!(f)?;
205
0
            }
206
        }
207
208
0
        Ok(())
209
0
    }
210
}
211
212
impl Debug for ExtendedTable {
213
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214
0
        f.debug_struct("ExtendedTable")
215
0
            .field("fields", &self.fields)
216
0
            .field("records", &self.records)
217
0
            .finish_non_exhaustive()
218
0
    }
219
}
220
221
0
fn truncate_records(records: &mut Vec<Vec<String>>, max_width: usize, suffix: &str) {
222
0
    for fields in records {
223
0
        truncate_fields(fields, max_width, suffix);
224
0
    }
225
0
}
226
227
0
fn truncate_fields(records: &mut Vec<String>, max_width: usize, suffix: &str) {
228
0
    for text in records {
229
0
        truncate(text, max_width, suffix);
230
0
    }
231
0
}
232
233
0
fn write_header_template(
234
0
    f: &mut fmt::Formatter<'_>,
235
0
    template: &Rc<RefCell<dyn Fn(usize) -> String>>,
236
0
    index: usize,
237
0
    max_field_width: usize,
238
0
    max_values_length: usize,
239
0
) -> fmt::Result {
240
0
    let record_template = template.borrow()(index);
241
0
    let mut template = format!("-{record_template}-");
242
0
    let default_template_length = template.len();
243
244
    // 3 - is responsible for ' | ' formatting
245
0
    let max_line_width = std::cmp::max(
246
0
        max_field_width + 3 + max_values_length,
247
0
        default_template_length,
248
    );
249
0
    let rest_to_print = max_line_width - default_template_length;
250
0
    if rest_to_print > 0 {
251
        // + 1 is a space after field name and we get a next pos so its +2
252
0
        if max_field_width + 2 > default_template_length {
253
0
            let part1 = (max_field_width + 1) - default_template_length;
254
0
            let part2 = rest_to_print - part1 - 1;
255
0
256
0
            template.extend(
257
0
                std::iter::repeat_n('-', part1)
258
0
                    .chain(std::iter::once('+'))
259
0
                    .chain(std::iter::repeat_n('-', part2)),
260
0
            );
261
0
        } else {
262
0
            template.extend(std::iter::repeat_n('-', rest_to_print));
263
0
        }
264
0
    }
265
266
0
    write!(f, "{template}")?;
267
268
0
    Ok(())
269
0
}
270
271
0
fn write_record(
272
0
    f: &mut fmt::Formatter<'_>,
273
0
    field: &str,
274
0
    value: &str,
275
0
    max_field_width: usize,
276
0
) -> fmt::Result {
277
0
    write!(f, "{field:max_field_width$} | {value}")
278
0
}
279
280
0
fn truncate(text: &mut String, max: usize, suffix: &str) {
281
0
    let original_len = text.len();
282
283
0
    if max == 0 || text.is_empty() {
284
0
        *text = String::new();
285
0
    } else {
286
0
        *text = crate::util::string::cut_str(text, max).into_owned();
287
0
    }
288
289
0
    let cut_was_done = text.len() < original_len;
290
0
    if !suffix.is_empty() && cut_was_done {
291
0
        text.push_str(suffix);
292
0
    }
293
0
}
294
295
0
fn record_template(index: usize) -> String {
296
0
    format!("[ RECORD {index} ]")
297
0
}