1extern crate linked_hash_map;
6extern crate regex;
7extern crate serde;
8
9#[macro_use]
10extern crate serde_derive;
11extern crate serde_yaml;
12
13#[macro_use]
14pub mod utils;
15pub mod csv;
16pub mod types;
17pub mod yaml;
18
19#[cfg(feature = "spreadsheets")]
20pub mod cal;
21
22#[cfg(feature = "python")]
23pub mod py;
24
25use indexmap::IndexSet;
26use std::cmp;
27use types::*;
28
29#[allow(unused_imports)]
30use utils::StripMargin;
31
32#[test]
33fn can_extract_headers() {
34 let hdrs = vec![
35 linkedhashmap![s!("foo") => s!("ggg"), s!("bar") => s!("fred"), s!("nop") => s!("no")], linkedhashmap![s!("foo") => s!("seventy"), s!("bar") => s!("barry"), s!("nop") => s!("no"), s!("aaa") => s!("ddd")], linkedhashmap![s!("bar") => s!("col has no foo"), s!("fff") => s!("ffsd")],
38 ];
39
40 let expected = indexset![s!("bar"), s!("foo"), s!("nop"), s!("aaa"), s!("fff")];
41 let result = collect_headers(&hdrs);
42 assert!(expected == result);
43}
44
45pub fn collect_headers(data: &[TableRow<String, String>]) -> IndexSet<String> {
46 data.iter().flat_map(|hm| hm.keys().cloned()).collect()
47}
48
49#[test]
50fn can_mk_header() {
51 let hdr = mk_md_header(&vec![(s!("bard"), 5), (s!("other"), 8)]);
52
53 let expected = "
55 ||bard | other |
56 ||-----|--------|"
57 .strip_margin();
58 assert!(hdr == expected);
59}
60
61pub fn mk_md_header(heading_data: &[(String, usize)]) -> String {
68 let heading: String = heading_data.iter().fold(String::from("|"), |res, h| {
69 format!("{}{: ^width$}|", res, h.0, width = h.1)
70 });
71 let dashed: String = heading_data.iter().fold(String::from("|"), |res, h| {
72 format!("{}{:-^width$}|", res, "-", width = h.1)
73 });
74
75 format!("{}\n{}", heading, dashed)
76}
77
78#[test]
79fn can_mk_data() {
80 let tbl_md = mk_md_data(
81 &vec![(s!("foo"), 5), (s!("bar"), 8)],
82 &vec![
83 linkedhashmap![s!("foo") => s!("ggg"), s!("bar") => s!("fred"), s!("nop") => s!("no")],
84 linkedhashmap![s!("foo") => s!("seventy"), s!("bar") => s!("barry"), s!("nop") => s!("no")],
85 linkedhashmap![s!("bar") => s!("col has no foo")],
86 ],
87 &None,
88 );
89
90 let expected = "
92 || ggg | fred |
93 ||seventy| barry |
94 || |col has no foo|"
95 .strip_margin();
96
97 println!("{}\n{}", tbl_md, expected);
98
99 assert!(tbl_md == expected);
100}
101
102#[test]
103fn can_mk_data_limiting_headers() {
104 let tbl_md = mk_md_data(
105 &vec![(s!("foo"), 5), (s!("bar"), 8)],
106 &vec![
107 linkedhashmap![s!("foo") => s!("ggg"), s!("bar") => s!("fred"), s!("nop") => s!("no")],
108 linkedhashmap![s!("foo") => s!("seventy"), s!("bar") => s!("barry"), s!("nop") => s!("no")],
109 linkedhashmap![s!("bar") => s!("col has no foo")],
110 ],
111 &None,
112 );
113
114 let expected = "
116 || ggg | fred |
117 ||seventy| barry |
118 || |col has no foo|"
119 .strip_margin();
120
121 println!("{}\n{}", tbl_md, expected);
122
123 assert!(tbl_md == expected);
124}
125
126pub fn mk_md_data(
146 heading_data: &[(String, usize)],
147 data: &[TableRow<String, String>],
148 render_options: &Option<RenderOptions>,
149) -> String {
150 let filters: Option<Vec<KVFilter>> = render_options.clone().and_then(|ro| ro.filters);
151
152 let iter: Box<dyn Iterator<Item = &TableRow<String, String>>> = match filters {
153 None => Box::new(data.iter()),
154 Some(vfilts) => Box::new(
155 data.iter()
156 .filter(move |row| filter_tablerows(row, &vfilts)),
157 ),
158 };
159
160 let ret: Vec<String> = iter
161 .map(|hm| {
162 heading_data.iter().fold(String::from("|"), |res, k| {
163 let s = match hm.get(&k.0) {
164 Some(x) => x.to_string(),
165 None => "".into(),
166 };
167
168 format!("{}{: ^width$}|", res, s, width = k.1)
169 })
170 })
171 .collect::<Vec<String>>();
172
173 ret.join("\n")
175}
176
177fn filter_tablerows(row: &TableRow<String, String>, vfilters: &Vec<KVFilter>) -> bool {
182 vfilters.iter().all(|f| tablerow_filter(row, f))
183}
184
185fn tablerow_filter(row: &TableRow<String, String>, filt: &KVFilter) -> bool {
193 row.keys()
194 .filter(|k| {
195 filt.key_re.is_match(k)
196 && match row.get(*k) {
197 Some(v) => filt.value_re.is_match(v),
198 None => false,
199 }
200 })
201 .collect::<Vec<_>>()
202 .len()
203 > 0
204}
205
206#[test]
207fn can_make_table() {
208 let tbl_md = mk_table(
209 &vec![
210 linkedhashmap![s!("foo") => s!("ggg"), s!("bar") => s!("fred"), s!("nop") => s!("no")],
211 linkedhashmap![s!("foo") => s!("seventy"), s!("bar") => s!("barry"), s!("nop") => s!("no")],
212 linkedhashmap![s!("bar") => s!("col has no foo")],
213 ],
214 &Some(RenderOptions {
215 headings: Some(vec![s!("foo"), s!("bar")]),
216 ..Default::default()
217 }),
218 );
219
220 let expected = "
222 || foo | bar |
223 ||-------|--------------|
224 || ggg | fred |
225 ||seventy| barry |
226 || |col has no foo|"
227 .strip_margin();
228
229 assert!(tbl_md == expected);
230}
231
232#[test]
233fn can_make_table_all_cols() {
234 let tbl_md = mk_table(
235 &vec![
236 linkedhashmap![s!("foo") => s!("ggg"), s!("bar") => s!("fred"), s!("nop") => s!("no")],
237 linkedhashmap![s!("foo") => s!("seventy"), s!("bar") => s!("barry"), s!("nop") => s!("no")],
238 linkedhashmap![s!("bar") => s!("col has no foo")],
239 ],
240 &None,
241 );
242
243 let expected = "
245 || foo | bar |nop|
246 ||-------|--------------|---|
247 || ggg | fred |no |
248 ||seventy| barry |no |
249 || |col has no foo| |"
250 .strip_margin();
251
252 println!("{}\n{}", tbl_md, expected);
253
254 assert!(tbl_md == expected);
255}
256
257pub fn mk_table(
266 data: &[TableRow<String, String>],
267 render_options: &Option<RenderOptions>,
268) -> String {
269 let headings = match render_options {
272 Some(RenderOptions {
273 headings: Some(h), ..
274 }) => h.clone(),
275 _ => collect_headers(data).into_iter().collect(),
276 };
277
278 let heading_data: Vec<(String, usize)> = headings
279 .iter()
280 .map(|h| {
281 (
282 h.clone(),
283 data.iter().fold(h.len(), |max, hm| {
284 cmp::max(
285 max,
286 match hm.get(h) {
287 Some(v) => v.to_string().len(),
288 None => 0,
289 },
290 )
291 }),
292 )
293 })
294 .collect::<Vec<(String, usize)>>();
295
296 format!(
297 "{}\n{}",
298 mk_md_header(&heading_data),
299 mk_md_data(&heading_data, data, render_options)
300 )
301}
302
303#[test]
304fn can_mk_table_with_1_filter() {
305 let tbl_md = mk_table(
306 &vec![
307 linkedhashmap![s!("foo") => s!("ggg"), s!("bar") => s!("fred"), s!("nop") => s!("no")],
308 linkedhashmap![s!("foo") => s!("seventy"), s!("bar") => s!("barry"), s!("nop") => s!("no")],
309 linkedhashmap![s!("bar") => s!("col has no foo")],
310 ],
311 &Some(RenderOptions {
312 headings: Some(vec![s!("foo"), s!("bar")]),
313 filters: Some(vec![KVFilter::new(s!("foo"), s!("ggg"))]),
314
315 ..Default::default()
316 }),
317 );
318
319 let expected = "
321 || foo | bar |
322 ||-------|--------------|
323 || ggg | fred |"
324 .strip_margin();
325
326 println!("{}\n{}", tbl_md, expected);
327
328 assert!(tbl_md == expected);
329}
330
331#[test]
332fn can_mk_table_with_2_filter() {
333 let tbl_md = mk_table(
334 &vec![
335 linkedhashmap![s!("foo") => s!("ggg"), s!("bar") => s!("fred"), s!("nop") => s!("no")],
336 linkedhashmap![s!("foo") => s!("ggg"), s!("bar") => s!("barry"), s!("nop") => s!("no")],
337 linkedhashmap![s!("bar") => s!("col has no foo")],
338 ],
339 &Some(RenderOptions {
340 headings: Some(vec![s!("foo"), s!("bar")]),
341 filters: Some(vec![
342 KVFilter::new(s!("foo"), s!("ggg")),
343 KVFilter::new(s!("bar"), s!("barry")),
344 ]),
345
346 ..Default::default()
347 }),
348 );
349
350 let expected = "
352 ||foo| bar |
353 ||---|--------------|
354 ||ggg| barry |"
355 .strip_margin();
356
357 println!("{}\n{}", tbl_md, expected);
358
359 assert!(tbl_md == expected);
360}
361
362#[test]
367fn can_mk_table_with_value_regex() {
368 let tbl_md = mk_table(
369 &vec![
370 linkedhashmap![s!("foo") => s!("ggg"), s!("bar") => s!("fred"), s!("nop") => s!("no")],
371 linkedhashmap![s!("foo") => s!("ggg"), s!("bar") => s!("abc"), s!("nop") => s!("has an r here")],
372 linkedhashmap![s!("bar") => s!("col has no foo")],
373 ],
374 &Some(RenderOptions {
375 headings: Some(vec![s!("foo"), s!("bar")]),
376 filters: Some(vec![KVFilter::new(s!(".*"), s!(".*r.*"))]),
377
378 ..Default::default()
379 }),
380 );
381
382 let expected = "
384 ||foo| bar |
385 ||---|--------------|
386 ||ggg| fred |
387 ||ggg| abc |"
388 .strip_margin();
389
390 println!("{}\n{}", tbl_md, expected);
391
392 assert!(tbl_md == expected);
393}
394
395pub fn named_table_to_md(
401 table: &Result<NamedTable<String, String>, MadatoError>,
402 print_name: bool,
403 render_options: &Option<RenderOptions>,
404) -> String {
405 match table {
406 Err(e) => format!("**Table errored**: {}", e),
407 Ok((name, table_data)) => {
408 if print_name {
409 format!("**{}**\n{}", name, mk_table(&table_data, render_options))
410 } else {
411 mk_table(&table_data, render_options)
412 }
413 }
414 }
415}