Skip to main content

icann_rdap_client/md/
table.rs

1use std::cmp::max;
2
3use icann_rdap_common::prelude::Remark;
4
5use super::{string::StringUtil, MdHeaderText, MdOptions, MdParams, ToMd};
6
7pub(crate) trait ToMpTable {
8    fn add_to_mptable(&self, table: MultiPartTable, params: MdParams) -> MultiPartTable;
9}
10
11/// A datastructure to hold various row types for a markdown table.
12///
13/// This datastructure has the following types of rows:
14/// * header - just the left most column which is centered and bolded text
15/// * name/value - first column is the name and the second column is data.
16///
17/// For name/value rows, the name is right justified. Name/value rows may also
18/// have unordered (bulleted) lists. In markdown, there is no such thing as a
19/// multiline row, so this creates multiple rows where the name is left blank.
20pub struct MultiPartTable {
21    rows: Vec<Row>,
22    value_highlights: Vec<String>,
23}
24
25enum Row {
26    Header(String),
27    Separator,
28    NameValue((String, String)),
29    MultiValue(Vec<String>),
30}
31
32impl Default for MultiPartTable {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl MultiPartTable {
39    pub fn new() -> Self {
40        Self {
41            rows: vec![],
42            value_highlights: vec![],
43        }
44    }
45
46    pub fn new_with_value_highlights(value_highlights: Vec<String>) -> Self {
47        Self {
48            rows: vec![],
49            value_highlights,
50        }
51    }
52
53    pub fn new_with_value_highlights_from_remarks(remarks: &[Remark]) -> Self {
54        Self {
55            rows: vec![],
56            value_highlights: get_value_highlights(remarks),
57        }
58    }
59
60    /// Add a header row.
61    pub fn header_ref(mut self, name: &impl ToString) -> Self {
62        self.rows.push(Row::Header(name.to_string()));
63        self
64    }
65
66    /// Adds a separator line.
67    pub fn add_separator(mut self) -> Self {
68        self.rows.push(Row::Separator);
69        self
70    }
71
72    /// Add a name/value row.
73    pub fn nv_ref(mut self, name: &impl ToString, value: &impl ToString) -> Self {
74        self.rows.push(Row::NameValue((
75            name.to_string(),
76            value.to_string().replace_md_chars(),
77        )));
78        self
79    }
80
81    /// Add a name/value row.
82    pub fn nv(mut self, name: &impl ToString, value: impl ToString) -> Self {
83        self.rows.push(Row::NameValue((
84            name.to_string(),
85            value.to_string().replace_md_chars(),
86        )));
87        self
88    }
89
90    /// Add a name/value row without processing whitespace or markdown characters.
91    pub fn nv_raw(mut self, name: &impl ToString, value: impl ToString) -> Self {
92        self.rows
93            .push(Row::NameValue((name.to_string(), value.to_string())));
94        self
95    }
96
97    /// Add a name/value row with unordered list.
98    pub fn nv_ul_ref(mut self, name: &impl ToString, value: Vec<&impl ToString>) -> Self {
99        value.iter().enumerate().for_each(|(i, v)| {
100            if i == 0 {
101                self.rows.push(Row::NameValue((
102                    name.to_string(),
103                    format!("* {}", v.to_string().replace_md_chars()),
104                )))
105            } else {
106                self.rows.push(Row::NameValue((
107                    String::default(),
108                    format!("* {}", v.to_string().replace_md_chars()),
109                )))
110            }
111        });
112        self
113    }
114
115    /// Add a name/value row with unordered list.
116    pub fn nv_ul(mut self, name: &impl ToString, value: Vec<impl ToString>) -> Self {
117        value.iter().enumerate().for_each(|(i, v)| {
118            if i == 0 {
119                self.rows.push(Row::NameValue((
120                    name.to_string(),
121                    format!("* {}", v.to_string().replace_md_chars()),
122                )))
123            } else {
124                self.rows.push(Row::NameValue((
125                    String::default(),
126                    format!("* {}", v.to_string().replace_md_chars()),
127                )))
128            }
129        });
130        self
131    }
132
133    /// Add a name/value row using a default if value is None.
134    pub fn and_nv_ref<T: ToString>(mut self, name: &impl ToString, value: &Option<T>) -> Self {
135        self.rows.push(Row::NameValue((
136            name.to_string(),
137            value
138                .as_ref()
139                .map(|s| s.to_string())
140                .unwrap_or_default()
141                .replace_md_chars(),
142        )));
143        self
144    }
145
146    /// Add a name/value row if the value is Some(T).
147    pub fn and_nv_ref_maybe<T: ToString>(self, name: &impl ToString, value: &Option<T>) -> Self {
148        if let Some(value) = value {
149            self.nv_ref(name, &value.to_string())
150        } else {
151            self
152        }
153    }
154
155    /// Add a name/value row with unordered list if the value is Some.
156    pub fn and_nv_ul_ref(self, name: &impl ToString, value: Option<Vec<&impl ToString>>) -> Self {
157        if let Some(value) = value {
158            self.nv_ul_ref(name, value)
159        } else {
160            self
161        }
162    }
163
164    /// Add a name/value row with unordered list if the value is Some.
165    pub fn and_nv_ul(self, name: &impl ToString, value: Option<Vec<impl ToString>>) -> Self {
166        if let Some(value) = value {
167            self.nv_ul(name, value)
168        } else {
169            self
170        }
171    }
172
173    /// A summary row is a special type of name/value row that has an unordered (bulleted) list
174    /// that is output in a tree structure (max 3 levels).
175    pub fn summary(mut self, header_text: MdHeaderText) -> Self {
176        self.rows.push(Row::NameValue((
177            "Summary".to_string(),
178            header_text.to_string().replace_md_chars().to_string(),
179        )));
180        // note that termimad has limits on list depth, so we can't go too crazy.
181        // however, this seems perfectly reasonable for must RDAP use cases.
182        for level1 in header_text.children {
183            self.rows.push(Row::NameValue((
184                "".to_string(),
185                format!("* {}", level1.to_string().replace_md_chars()),
186            )));
187            for level2 in level1.children {
188                self.rows.push(Row::NameValue((
189                    "".to_string(),
190                    format!("  * {}", level2.to_string().replace_md_chars()),
191                )));
192            }
193        }
194        self
195    }
196
197    /// Adds a multivalue row.
198    pub fn multi(mut self, values: Vec<String>) -> Self {
199        self.rows.push(Row::MultiValue(
200            values.iter().map(|s| s.replace_md_chars()).collect(),
201        ));
202        self
203    }
204
205    /// Adds a multivalue row.
206    pub fn multi_ref(mut self, values: &[&str]) -> Self {
207        self.rows.push(Row::MultiValue(
208            values.iter().map(|s| s.replace_md_chars()).collect(),
209        ));
210        self
211    }
212
213    /// Adds a multivalue row without processing whitespace or markdown characters.
214    pub fn multi_raw(mut self, values: Vec<String>) -> Self {
215        self.rows.push(Row::MultiValue(
216            values.iter().map(|s| s.to_owned()).collect(),
217        ));
218        self
219    }
220
221    /// Adds a multivalue row without processing whitespace or markdown characters.
222    pub fn multi_raw_ref(mut self, values: &[&str]) -> Self {
223        self.rows.push(Row::MultiValue(
224            values.iter().map(|s| s.to_string()).collect(),
225        ));
226        self
227    }
228
229    pub fn to_md_table(&self, options: &MdOptions) -> String {
230        let mut md = String::new();
231
232        let col_type_width = max(
233            self.rows
234                .iter()
235                .map(|row| match row {
236                    Row::Header(header) => header.len(),
237                    Row::NameValue((name, _value)) => name.len(),
238                    Row::MultiValue(_) => 1,
239                    Row::Separator => 4,
240                })
241                .max()
242                .unwrap_or(1),
243            1,
244        );
245
246        self.rows
247            .iter()
248            .scan(true, |state, x| {
249                let new_state = match x {
250                    Row::Header(name) => {
251                        md.push_str(&format!(
252                            "|:-:|\n|{}|\n",
253                            name.to_center_bold(col_type_width, options)
254                        ));
255                        true
256                    }
257                    Row::Separator => true,
258                    Row::NameValue((name, value)) => {
259                        if *state {
260                            md.push_str("|-:|:-|\n");
261                        };
262                        md.push_str(&format!(
263                            "|{}|{}|\n",
264                            &name.to_right(col_type_width, options),
265                            highlight_value(value, &self.value_highlights, options)
266                        ));
267                        false
268                    }
269                    Row::MultiValue(values) => {
270                        // column formatting
271                        md.push('|');
272                        for _col in values {
273                            md.push_str(":--:|");
274                        }
275                        md.push('\n');
276
277                        // the actual data
278                        md.push('|');
279                        for col in values {
280                            md.push_str(&format!("{col}|"));
281                        }
282                        md.push('\n');
283                        true
284                    }
285                };
286                *state = new_state;
287                Some(new_state)
288            })
289            .last();
290
291        md.push_str("|\n\n");
292        md
293    }
294}
295
296impl ToMd for MultiPartTable {
297    fn to_md(&self, params: super::MdParams) -> String {
298        self.to_md_table(params.options)
299    }
300}
301
302fn highlight_value(value: &str, values_to_highlight: &[String], options: &MdOptions) -> String {
303    let mut value = value.to_string();
304    for hl in values_to_highlight {
305        value = value.replace(hl, &hl.to_em(options));
306    }
307    value
308}
309
310pub(crate) fn get_value_highlights(remarks: &[Remark]) -> Vec<String> {
311    let mut highlights = vec![];
312    for remark in remarks {
313        if let Some(keys) = &remark.simple_redaction_keys {
314            for key in keys {
315                highlights.push(key.clone());
316            }
317            if let Some(title) = &remark.title {
318                highlights.push(title.clone())
319            }
320        }
321    }
322    highlights
323}
324
325#[cfg(test)]
326#[allow(non_snake_case)]
327mod tests {
328    use icann_rdap_common::{httpdata::HttpData, prelude::ToResponse, response::Rfc9083Error};
329
330    use crate::{md::ToMd, rdap::rr::RequestData};
331
332    use super::MultiPartTable;
333
334    #[test]
335    fn GIVEN_header_WHEN_to_md_THEN_header_format_and_header() {
336        // GIVEN
337        let table = MultiPartTable::new().header_ref(&"foo");
338
339        // WHEN
340        let req_data = RequestData {
341            req_number: 0,
342            req_target: true,
343        };
344        let rdap_response = Rfc9083Error::response_obj()
345            .error_code(500)
346            .build()
347            .to_response();
348        let actual = table.to_md(crate::md::MdParams {
349            heading_level: 0,
350            root: &rdap_response,
351            http_data: &HttpData::example().build(),
352            options: &crate::md::MdOptions::plain_text(),
353            req_data: &req_data,
354            show_rfc9537_redactions: false,
355            highlight_simple_redactions: false,
356        });
357
358        assert_eq!(actual, "|:-:|\n|__foo__|\n|\n\n")
359    }
360
361    #[test]
362    fn GIVEN_header_and_data_ref_WHEN_to_md_THEN_header_format_and_header() {
363        // GIVEN
364        let table = MultiPartTable::new()
365            .header_ref(&"foo")
366            .nv_ref(&"bizz", &"buzz");
367
368        // WHEN
369        let req_data = RequestData {
370            req_number: 0,
371            req_target: true,
372        };
373        let rdap_response = Rfc9083Error::response_obj()
374            .error_code(500)
375            .build()
376            .to_response();
377        let actual = table.to_md(crate::md::MdParams {
378            heading_level: 0,
379            root: &rdap_response,
380            http_data: &HttpData::example().build(),
381            options: &crate::md::MdOptions::plain_text(),
382            req_data: &req_data,
383            show_rfc9537_redactions: false,
384            highlight_simple_redactions: false,
385        });
386
387        assert_eq!(actual, "|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n|\n\n")
388    }
389
390    #[test]
391    fn GIVEN_header_and_2_data_ref_WHEN_to_md_THEN_header_format_and_header() {
392        // GIVEN
393        let table = MultiPartTable::new()
394            .header_ref(&"foo")
395            .nv_ref(&"bizz", &"buzz")
396            .nv_ref(&"bar", &"baz");
397
398        // WHEN
399        let req_data = RequestData {
400            req_number: 0,
401            req_target: true,
402        };
403        let rdap_response = Rfc9083Error::response_obj()
404            .error_code(500)
405            .build()
406            .to_response();
407        let actual = table.to_md(crate::md::MdParams {
408            heading_level: 0,
409            root: &rdap_response,
410            http_data: &HttpData::example().build(),
411            options: &crate::md::MdOptions::plain_text(),
412            req_data: &req_data,
413            show_rfc9537_redactions: false,
414            highlight_simple_redactions: false,
415        });
416
417        assert_eq!(
418            actual,
419            "|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n| bar|baz|\n|\n\n"
420        )
421    }
422
423    #[test]
424    fn GIVEN_header_and_data_WHEN_to_md_THEN_header_format_and_header() {
425        // GIVEN
426        let table = MultiPartTable::new()
427            .header_ref(&"foo")
428            .nv(&"bizz", "buzz".to_string());
429
430        // WHEN
431        let req_data = RequestData {
432            req_number: 0,
433            req_target: true,
434        };
435        let rdap_response = Rfc9083Error::response_obj()
436            .error_code(500)
437            .build()
438            .to_response();
439        let actual = table.to_md(crate::md::MdParams {
440            heading_level: 0,
441            root: &rdap_response,
442            http_data: &HttpData::example().build(),
443            options: &crate::md::MdOptions::plain_text(),
444            req_data: &req_data,
445            show_rfc9537_redactions: false,
446            highlight_simple_redactions: false,
447        });
448
449        assert_eq!(actual, "|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n|\n\n")
450    }
451
452    #[test]
453    fn GIVEN_header_and_2_data_WHEN_to_md_THEN_header_format_and_header() {
454        // GIVEN
455        let table = MultiPartTable::new()
456            .header_ref(&"foo")
457            .nv(&"bizz", "buzz")
458            .nv(&"bar", "baz");
459
460        // WHEN
461        let req_data = RequestData {
462            req_number: 0,
463            req_target: true,
464        };
465        let rdap_response = Rfc9083Error::response_obj()
466            .error_code(500)
467            .build()
468            .to_response();
469        let actual = table.to_md(crate::md::MdParams {
470            heading_level: 0,
471            root: &rdap_response,
472            http_data: &HttpData::example().build(),
473            options: &crate::md::MdOptions::plain_text(),
474            req_data: &req_data,
475            show_rfc9537_redactions: false,
476            highlight_simple_redactions: false,
477        });
478
479        assert_eq!(
480            actual,
481            "|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n| bar|baz|\n|\n\n"
482        )
483    }
484
485    #[test]
486    fn GIVEN_header_and_2_data_ref_twice_WHEN_to_md_THEN_header_format_and_header() {
487        // GIVEN
488        let table = MultiPartTable::new()
489            .header_ref(&"foo")
490            .nv_ref(&"bizz", &"buzz")
491            .nv_ref(&"bar", &"baz")
492            .header_ref(&"foo")
493            .nv_ref(&"bizz", &"buzz")
494            .nv_ref(&"bar", &"baz");
495
496        // WHEN
497        let req_data = RequestData {
498            req_number: 0,
499            req_target: true,
500        };
501        let rdap_response = Rfc9083Error::response_obj()
502            .error_code(500)
503            .build()
504            .to_response();
505        let actual = table.to_md(crate::md::MdParams {
506            heading_level: 0,
507            root: &rdap_response,
508            http_data: &HttpData::example().build(),
509            options: &crate::md::MdOptions::plain_text(),
510            req_data: &req_data,
511            show_rfc9537_redactions: false,
512            highlight_simple_redactions: false,
513        });
514
515        assert_eq!(
516            actual,
517            "|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n| bar|baz|\n|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n| bar|baz|\n|\n\n"
518        )
519    }
520}