/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 | } |