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
11pub 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 pub fn header_ref(mut self, name: &impl ToString) -> Self {
62 self.rows.push(Row::Header(name.to_string()));
63 self
64 }
65
66 pub fn add_separator(mut self) -> Self {
68 self.rows.push(Row::Separator);
69 self
70 }
71
72 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 md.push('|');
272 for _col in values {
273 md.push_str(":--:|");
274 }
275 md.push('\n');
276
277 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 let table = MultiPartTable::new().header_ref(&"foo");
338
339 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 let table = MultiPartTable::new()
365 .header_ref(&"foo")
366 .nv_ref(&"bizz", &"buzz");
367
368 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 let table = MultiPartTable::new()
394 .header_ref(&"foo")
395 .nv_ref(&"bizz", &"buzz")
396 .nv_ref(&"bar", &"baz");
397
398 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 let table = MultiPartTable::new()
427 .header_ref(&"foo")
428 .nv(&"bizz", "buzz".to_string());
429
430 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 let table = MultiPartTable::new()
456 .header_ref(&"foo")
457 .nv(&"bizz", "buzz")
458 .nv(&"bar", "baz");
459
460 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 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 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}