Skip to main content

madato/
lib.rs

1// #![feature(
2//     slice_patterns, extern_prelude, serde_impl
3// )]
4
5extern 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")], // foo bar nop
36        linkedhashmap![s!("foo") => s!("seventy"), s!("bar") => s!("barry"), s!("nop") => s!("no"), s!("aaa") => s!("ddd")], //
37        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    // the | below is the margin
54    let expected = "
55   ||bard | other  |
56   ||-----|--------|"
57        .strip_margin();
58    assert!(hdr == expected);
59}
60
61/// Returns a String of the heading and 2nd line of a markdown table.
62///
63/// # Arguments
64///
65/// `headings` - vector of headings (column titles over the table) and their sizes
66///
67pub 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    // the | below is the margin
91    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    // the | below is the margin
115    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
126/// Takes an ordered list of tuples; (heading_data) (key, column_width) and a slice of TableRows, the cell values
127/// The TableRow could carry more data than the keys provided. That is, only hm.get(key) will appear in the output.
128///
129/// returns a string of Markdown rows; `\n` separated in the form, layed out in the
130/// width as per the heading_data.
131///
132///
133/// ```text
134/// | val1 | val3 | val4 | val5 |
135/// ...
136/// | val1 | val3 | val4 | val5 |
137/// ```
138///
139/// # Arguments
140///
141/// `heading_data` - Name of column, and the width to use for each row.
142/// `data`         - Vector of TableRows
143/// `render_options` - Set of "config" that drives filtering, ordering, output.
144///
145pub 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    // make a new String of all the concatenated fields
174    ret.join("\n")
175}
176
177///
178/// For every filter im the Vec of filters, return true, immediately
179/// if the tablerow passes the filter. (ignore all other filters)
180///
181fn filter_tablerows(row: &TableRow<String, String>, vfilters: &Vec<KVFilter>) -> bool {
182    vfilters.iter().all(|f| tablerow_filter(row, f))
183}
184
185///
186/// Per row filter. Takes a regex and the row.
187/// If the "regex" for a key and a value returns one or more
188/// matches (a key - to a cell), then this row is "kept". (returns true)
189///
190/// If the regex pair in KVFilter returns no matches across all cells the this
191/// row is filtered out (return false)
192fn 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    // the | below is the margin
221    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    // the | below is the margin
244    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
257/// Takes an ordered list of headings and a Vector of TableRows, the cell values
258/// and produces a formatted Markdown Table.
259///
260/// # Arguments
261///
262/// `data`           - Slice of TableRows
263/// `render_options` - Set of "config" that drives filtering, ordering, output.
264///
265pub fn mk_table(
266    data: &[TableRow<String, String>],
267    render_options: &Option<RenderOptions>,
268) -> String {
269    // for each heading, find the "widest" heading, or value
270
271    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    // the | below is the margin
320    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    // the | below is the margin
351    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///
363/// We want to see if the regexp finds values in other "not" aligned cells
364/// and because a "heading" filter is applied (afte the fact) the cell that has
365/// an 'r' in it, doesn't come in the output.
366#[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    // the | below is the margin
383    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
395///
396/// From Spreadsheets, or keyed YAML files, the table could be named.
397/// When we generate the Markdown, we optionally may want the title of the
398/// table at the beginning.
399///
400pub 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}