1use crate::columns_width::{Column, ColumnsWidth};
7use ion::{Dictionary, Ion, Row, Section, Value};
8use std::fmt::{self, Write};
9use std::str::FromStr;
10
11#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
13pub struct FormatOptions {
14 pub dictionary: DictionaryOptions,
16 pub section: SectionOptions,
18 pub document: DocumentOptions,
20 pub table: Option<TableOptions>,
22}
23
24#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
26pub struct DictionaryOptions {
27 pub field: FieldStyle,
29}
30
31#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
33pub struct SectionOptions {
34 pub spacing: SectionSpacing,
36}
37
38#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
40pub struct DocumentOptions {
41 pub spacing: DocumentSpacing,
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum TableOptions {
48 AddHeaderSeparatorWhenMissing,
50}
51
52#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
54pub enum FieldStyle {
55 Singleline,
57 #[default]
59 Multiline,
60}
61
62#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
64pub enum SectionSpacing {
65 NewLine,
67 #[default]
69 AdditionalNewLine,
70}
71
72#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
74pub enum DocumentSpacing {
75 #[default]
77 EndNewLine,
78 AdditionalEndNewLine,
80}
81
82impl FromStr for FieldStyle {
83 type Err = String;
84
85 fn from_str(value: &str) -> Result<Self, Self::Err> {
86 match value {
87 "singleline" => Ok(Self::Singleline),
88 "multiline" => Ok(Self::Multiline),
89 _ => Err(format!(
90 "Unsupported `dictionary-field` style `{value}`. Expected `singleline` or `multiline`."
91 )),
92 }
93 }
94}
95
96impl FromStr for SectionSpacing {
97 type Err = String;
98
99 fn from_str(value: &str) -> Result<Self, Self::Err> {
100 match value {
101 "newline" => Ok(Self::NewLine),
102 "additional-newline" => Ok(Self::AdditionalNewLine),
103 _ => Err(format!(
104 "Unsupported `section-spacing` style `{value}`. Expected `newline` or `additional-newline`."
105 )),
106 }
107 }
108}
109
110impl FromStr for DocumentSpacing {
111 type Err = String;
112
113 fn from_str(value: &str) -> Result<Self, Self::Err> {
114 match value {
115 "end-newline" => Ok(Self::EndNewLine),
116 "additional-end-newline" => Ok(Self::AdditionalEndNewLine),
117 _ => Err(format!(
118 "Unsupported `document-spacing` style `{value}`. Expected `end-newline` or `additional-end-newline`."
119 )),
120 }
121 }
122}
123
124impl FromStr for TableOptions {
125 type Err = String;
126
127 fn from_str(value: &str) -> Result<Self, Self::Err> {
128 match value {
129 "add-header-separator-when-missing" => Ok(Self::AddHeaderSeparatorWhenMissing),
130 _ => Err(format!(
131 "Unsupported `table` style `{value}`. Expected `add-header-separator-when-missing`."
132 )),
133 }
134 }
135}
136
137#[derive(Clone, Debug)]
142pub struct IonDisplay<'a> {
143 ion: &'a Ion,
144 options: FormatOptions,
145}
146
147impl<'a> IonDisplay<'a> {
148 #[must_use]
150 pub fn new(ion: &'a Ion, options: FormatOptions) -> Self {
151 Self { ion, options }
152 }
153}
154
155#[derive(Clone, Debug)]
160pub struct DictionaryDisplay<'a> {
161 dictionary: &'a Dictionary,
162 field: FieldStyle,
163}
164
165impl<'a> DictionaryDisplay<'a> {
166 #[must_use]
168 pub fn new(dictionary: &'a Dictionary, field: FieldStyle) -> Self {
169 Self { dictionary, field }
170 }
171}
172
173#[derive(Clone, Debug)]
175pub struct DictionaryFieldDisplay<'a> {
176 key: &'a str,
177 value: &'a Value,
178 options: FieldStyle,
179}
180
181impl<'a> DictionaryFieldDisplay<'a> {
182 #[must_use]
184 pub fn new(key: &'a str, value: &'a Value, field: FieldStyle) -> Self {
185 Self {
186 key,
187 value,
188 options: field,
189 }
190 }
191}
192
193#[derive(Clone, Debug)]
194struct SectionDisplay<'a> {
195 columns_width: ColumnsWidth,
196 name: &'a str,
197 section: &'a Section,
198 options: FormatOptions,
199}
200
201#[derive(Clone, Debug)]
202struct RowsDisplay<'a> {
203 columns_width: &'a ColumnsWidth,
204 rows: &'a Vec<Row>,
205 table: Option<TableOptions>,
206}
207
208#[derive(Clone, Debug)]
209struct RowDisplay<'a> {
210 columns_width: &'a ColumnsWidth,
211 row: &'a Row,
212 row_type: RowTypeDisplay,
213}
214
215#[derive(Clone, Copy, Debug, Eq, PartialEq)]
216pub(crate) enum RowTypeDisplay {
217 Header,
218 Separator,
219 Data,
220}
221
222impl<'a> SectionDisplay<'a> {
223 fn new(name: &'a str, section: &'a Section, options: FormatOptions) -> Self {
224 let columns_width = section
225 .rows
226 .iter()
227 .map(|row| row.iter())
228 .collect::<ColumnsWidth>();
229
230 Self {
231 columns_width,
232 name,
233 section,
234 options,
235 }
236 }
237}
238
239#[must_use]
241pub fn display_with_options(ion: &Ion, options: FormatOptions) -> IonDisplay<'_> {
242 IonDisplay::new(ion, options)
243}
244
245#[must_use]
247pub fn format_ion_with_options(ion: &Ion, options: FormatOptions) -> String {
248 display_with_options(ion, options).to_string()
249}
250
251impl fmt::Display for IonDisplay<'_> {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 self.ion
254 .iter()
255 .map(|(name, section)| SectionDisplay::new(name, section, self.options))
256 .try_for_each(|section| writeln!(f, "{section}"))?;
257
258 if self.ion.iter().next().is_some()
259 && self.options.document.spacing == DocumentSpacing::AdditionalEndNewLine
260 {
261 writeln!(f)?;
262 }
263
264 Ok(())
265 }
266}
267
268impl fmt::Display for SectionDisplay<'_> {
269 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 writeln!(f, "[{}]", self.name)?;
271 DictionaryDisplay::new(&self.section.dictionary, self.options.dictionary.field).fmt(f)?;
272
273 if self.section.rows.is_empty() {
274 return Ok(());
275 }
276
277 if !self.section.dictionary.is_empty()
278 && self.options.section.spacing == SectionSpacing::AdditionalNewLine
279 {
280 writeln!(f)?;
281 }
282
283 RowsDisplay {
284 columns_width: &self.columns_width,
285 rows: &self.section.rows,
286 table: self.options.table,
287 }
288 .fmt(f)
289 }
290}
291
292impl fmt::Display for DictionaryDisplay<'_> {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 self.dictionary
295 .iter()
296 .try_for_each(|(key, value)| DictionaryFieldDisplay::new(key, value, self.field).fmt(f))
297 }
298}
299
300impl fmt::Display for DictionaryFieldDisplay<'_> {
301 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302 display_dictionary(f, self.value, self.key, self.options, 0)
303 }
304}
305
306fn display_dictionary(
307 f: &mut fmt::Formatter<'_>,
308 value: &Value,
309 key: &str,
310 options: FieldStyle,
311 depth: usize,
312) -> Result<(), fmt::Error> {
313 let current_inden = depth * 4;
314
315 match value {
316 Value::Dictionary(dictionary) if options == FieldStyle::Multiline => {
317 write!(f, "{0:1$}{2} = ", "", current_inden, key)?;
318 write_dictionary_multiline_value_at_depth(f, dictionary, depth, options)?;
319 }
320 Value::String(text) if options == FieldStyle::Multiline && text.contains('\n') => {
321 write_dictionary_multiline_string(f, key, text, depth)?;
322 }
323 Value::String(_) => {
324 write!(f, "{0:1$}{2} = \"{3}\"", "", current_inden, key, value)?;
325 }
326 _ => {
327 write!(f, "{0:1$}{2} = {3}", "", current_inden, key, value)?;
328 }
329 }
330
331 writeln!(f)
332}
333
334impl fmt::Display for RowsDisplay<'_> {
335 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336 if self.rows.is_empty() {
337 return Ok(());
338 }
339
340 let skip_items = if matches!(
344 self.table,
345 Some(TableOptions::AddHeaderSeparatorWhenMissing)
346 ) && matches!(self.columns_width.row_type(0), RowTypeDisplay::Data)
347 {
348 let header_row = &self.rows[0];
349 RowDisplay {
350 columns_width: self.columns_width,
351 row: header_row,
352 row_type: RowTypeDisplay::Header,
353 }
354 .fmt(f)?;
355 RowDisplay {
356 columns_width: self.columns_width,
357 row: header_row,
358 row_type: RowTypeDisplay::Separator,
359 }
360 .fmt(f)?;
361
362 1
363 } else {
364 0
365 };
366
367 self.rows
368 .iter()
369 .enumerate()
370 .skip(skip_items)
371 .try_for_each(|(row_idx, row)| {
372 RowDisplay {
373 columns_width: self.columns_width,
374 row,
375 row_type: self.columns_width.row_type(row_idx),
376 }
377 .fmt(f)
378 })?;
379
380 Ok(())
381 }
382}
383
384impl fmt::Display for RowDisplay<'_> {
385 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386 match self.row_type {
387 RowTypeDisplay::Header => header(f, self),
388 RowTypeDisplay::Separator => header_separator(f, self),
389 RowTypeDisplay::Data => write_data(f, self),
390 }
391 }
392}
393
394fn write_dictionary_multiline_string(
395 f: &mut fmt::Formatter<'_>,
396 key: &str,
397 text: &str,
398 depth: usize,
399) -> fmt::Result {
400 write!(f, "{0:1$}{2} = \"", "", depth * 4, key)?;
401
402 for ch in text.chars() {
403 match ch {
404 '\\' => f.write_str("\\\\")?,
405 '"' => f.write_str("\\\"")?,
406 _ => f.write_char(ch)?,
407 }
408 }
409 write!(f, "\"")
410}
411
412fn write_dictionary_multiline_value_at_depth(
413 f: &mut fmt::Formatter<'_>,
414 dictionary: &Dictionary,
415 depth: usize,
416 options: FieldStyle,
417) -> fmt::Result {
418 writeln!(f, "{{")?;
419
420 for (key, value) in dictionary {
421 display_dictionary(f, value, key, options, depth + 1)?;
422 }
423
424 write!(f, "{0:1$}}}", "", depth * 4)
425}
426
427fn header(f: &mut fmt::Formatter<'_>, columns: &RowDisplay<'_>) -> fmt::Result {
428 let mut columns_iter = columns.row.iter().enumerate();
429
430 if let Some((idx, ion_value)) = columns_iter.next() {
431 let column = columns.columns_width.column(idx);
432 let text = format!("{ion_value}");
433 let (left_alignment, right_alignment) = center_header_column_alignment(column, &text);
434
435 write_header_first_column(f, &text, left_alignment, right_alignment)?;
436
437 for (idx, ion_value) in columns_iter {
438 let column = columns.columns_width.column(idx);
439 let text = format!("{ion_value}");
440 let (left_alignment, right_alignment) = center_header_column_alignment(column, &text);
441
442 write_header_next_columns(f, &text, left_alignment, right_alignment)?;
443 }
444
445 writeln!(f)?;
446 }
447
448 Ok(())
449}
450
451fn center_header_column_alignment(column: Column, text: &str) -> (usize, usize) {
452 let spaces_width = column.width.checked_sub(text.len()).unwrap_or(text.len());
453 let even_width = spaces_width / 2;
454 let (modulo_left, modulo_right) = if text.len() & 0x1 == 0x1 {
455 (0, spaces_width % 2)
456 } else {
457 (spaces_width % 2, 0)
458 };
459
460 (even_width + modulo_left, even_width + modulo_right)
461}
462
463fn header_separator(f: &mut fmt::Formatter<'_>, columns: &RowDisplay<'_>) -> fmt::Result {
464 let mut header_iter = columns.row.iter().enumerate().map(|(index, _)| index);
465
466 if let Some(idx) = header_iter.next() {
467 write_separator_first_column(f, columns.columns_width.width(idx) + 2)?;
468
469 for idx in header_iter {
470 write_separator_next_columns(f, columns.columns_width.width(idx) + 2)?;
471 }
472
473 writeln!(f)?;
474 }
475
476 Ok(())
477}
478
479fn write_data(f: &mut fmt::Formatter<'_>, columns: &RowDisplay<'_>) -> fmt::Result {
480 let mut values_iter = columns.row.iter().enumerate();
481
482 if let Some((idx, ion_value)) = values_iter.next() {
483 let column = columns.columns_width.column(idx);
484 write_data_first_column(f, column, ion_value)?;
485
486 for (idx, ion_value) in values_iter {
487 let column = columns.columns_width.column(idx);
488 write_data_next_columns(f, column, ion_value)?;
489 }
490
491 writeln!(f)?;
492 }
493
494 Ok(())
495}
496
497fn write_data_first_column(
498 f: &mut fmt::Formatter<'_>,
499 column: Column,
500 ion_value: &Value,
501) -> fmt::Result {
502 let text = format!("{ion_value}");
503 write_data_cell(f, column, &text, "| ")
504}
505
506fn write_data_next_columns(
507 f: &mut fmt::Formatter<'_>,
508 column: Column,
509 ion_value: &Value,
510) -> fmt::Result {
511 let text = format!("{ion_value}");
512 write_data_cell(f, column, &text, " ")
513}
514
515fn write_header_first_column(
516 f: &mut fmt::Formatter<'_>,
517 text: &str,
518 left_alignment: usize,
519 right_alignment: usize,
520) -> fmt::Result {
521 write_header_cell(f, text, left_alignment, right_alignment, "| ")
522}
523
524fn write_header_next_columns(
525 f: &mut fmt::Formatter<'_>,
526 text: &str,
527 left_alignment: usize,
528 right_alignment: usize,
529) -> fmt::Result {
530 write_header_cell(f, text, left_alignment, right_alignment, " ")
531}
532
533fn write_header_cell(
534 f: &mut fmt::Formatter<'_>,
535 text: &str,
536 left_alignment: usize,
537 right_alignment: usize,
538 prefix: &str,
539) -> fmt::Result {
540 f.write_str(prefix)?;
541
542 write_repeated(f, ' ', left_alignment)?;
543 f.write_str(text)?;
544 write_repeated(f, ' ', right_alignment)?;
545 f.write_str(" |")
546}
547
548fn write_separator_first_column(f: &mut fmt::Formatter<'_>, width: usize) -> fmt::Result {
549 f.write_char('|')?;
550 write_separator_cell(f, width)
551}
552
553fn write_separator_next_columns(f: &mut fmt::Formatter<'_>, width: usize) -> fmt::Result {
554 write_separator_cell(f, width)
555}
556
557fn write_separator_cell(f: &mut fmt::Formatter<'_>, width: usize) -> fmt::Result {
558 write_repeated(f, '-', width)?;
559 f.write_char('|')
560}
561
562fn write_data_cell(
563 f: &mut fmt::Formatter<'_>,
564 column: Column,
565 text: &str,
566 prefix: &str,
567) -> fmt::Result {
568 f.write_str(prefix)?;
569
570 let padding = column.width.saturating_sub(text.len());
571 if column.is_number() {
572 write_repeated(f, ' ', padding)?;
573 f.write_str(text)?;
574 } else {
575 f.write_str(text)?;
576 write_repeated(f, ' ', padding)?;
577 }
578
579 f.write_str(" |")
580}
581
582fn write_repeated(f: &mut fmt::Formatter<'_>, ch: char, count: usize) -> fmt::Result {
583 for _ in 0..count {
584 f.write_char(ch)?;
585 }
586
587 Ok(())
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593 use crate::columns_width::{Column, ColumnType};
594 use indoc::indoc;
595 use ion::Dictionary;
596 use pretty_assertions::assert_eq;
597 use std::sync::LazyLock;
598 use test_case::test_case;
599
600 #[derive(Debug)]
601 struct IonFormatTestCase {
602 description: &'static str,
603 raw: &'static str,
604 options: FormatOptions,
605 expected: &'static str,
606 }
607
608 #[derive(Debug)]
609 struct DictionaryDisplayTestCase {
610 description: &'static str,
611 dictionary: Dictionary,
612 field: FieldStyle,
613 expected: &'static str,
614 }
615
616 #[derive(Debug)]
617 struct DictionaryFieldDisplayTestCase {
618 description: &'static str,
619 key: &'static str,
620 value: Value,
621 field: FieldStyle,
622 expected: &'static str,
623 }
624
625 #[derive(Debug)]
626 struct WriteDictionaryMultilineStringTestCase {
627 description: &'static str,
628 key: &'static str,
629 text: &'static str,
630 expected: &'static str,
631 }
632
633 #[derive(Debug)]
634 struct RowDisplayTestCase {
635 description: &'static str,
636 row: Row,
637 row_type: RowTypeDisplay,
638 columns_width: ColumnsWidth,
639 expected: &'static str,
640 }
641
642 fn string(value: &str) -> Value {
643 Value::String(value.into())
644 }
645
646 fn dictionary(entries: impl IntoIterator<Item = (&'static str, Value)>) -> Dictionary {
647 let mut dictionary = Dictionary::new();
648 for (key, value) in entries {
649 dictionary.insert(key.into(), value);
650 }
651 dictionary
652 }
653
654 #[test]
655 fn section_spacing_default_is_additional_newline() {
656 assert_eq!(SectionSpacing::AdditionalNewLine, SectionSpacing::default());
657 assert_eq!(
658 SectionSpacing::AdditionalNewLine,
659 FormatOptions::default().section.spacing
660 );
661 }
662
663 #[test]
664 fn document_spacing_default_is_newline() {
665 assert_eq!(DocumentSpacing::EndNewLine, DocumentSpacing::default());
666 assert_eq!(
667 DocumentSpacing::EndNewLine,
668 FormatOptions::default().document.spacing
669 );
670 }
671
672 static ION_FORMAT_CASE: LazyLock<IonFormatTestCase> = LazyLock::new(|| IonFormatTestCase {
673 description: "formats ion document",
674 raw: indoc! {r#"
675 [ALPHA]
676 name = "foo"
677
678 [BETA]
679 | num | text |
680 |-----|------|
681 | 1 | A |
682 | 22 | B |
683 "#},
684 options: FormatOptions {
685 dictionary: DictionaryOptions {
686 field: FieldStyle::Singleline,
687 },
688 section: SectionOptions {
689 spacing: SectionSpacing::AdditionalNewLine,
690 },
691 document: DocumentOptions {
692 spacing: DocumentSpacing::EndNewLine,
693 },
694 table: None,
695 },
696 expected: indoc! {r#"
697 [ALPHA]
698 name = "foo"
699
700 [BETA]
701 | num | text |
702 |-----|------|
703 | 1 | A |
704 | 22 | B |
705
706 "#},
707 });
708 static ION_FORMAT_NON_STRING_DICTIONARY_CASE: LazyLock<IonFormatTestCase> =
709 LazyLock::new(|| IonFormatTestCase {
710 description: "formats non string dictionary value",
711 raw: indoc! {r"
712 [ALPHA]
713 value = 7
714 "},
715 options: FormatOptions {
716 dictionary: DictionaryOptions {
717 field: FieldStyle::Singleline,
718 },
719 section: SectionOptions {
720 spacing: SectionSpacing::AdditionalNewLine,
721 },
722 document: DocumentOptions {
723 spacing: DocumentSpacing::EndNewLine,
724 },
725 table: None,
726 },
727 expected: indoc! {r"
728 [ALPHA]
729 value = 7
730
731 "},
732 });
733 const TEST_1A_BASE_CASE: IonFormatTestCase = IonFormatTestCase {
734 description: "TEST_1A base input with singleline dictionary, single newline section, single newline document",
735 raw: indoc! {r#"
736 [Data]
737 select = "
738 SELECT column
739 FROM table t1
740 INNER JOIN t2
741 ON t1.id = t2.
742 WHERE t1.userid = {{ user_id }}
743 ORDER BY name ASC
744 "
745 | col1 |
746 |--------|
747 | name1 |
748 | name2 |
749 "#},
750 options: FormatOptions {
751 dictionary: DictionaryOptions {
752 field: FieldStyle::Singleline,
753 },
754 section: SectionOptions {
755 spacing: SectionSpacing::NewLine,
756 },
757 document: DocumentOptions {
758 spacing: DocumentSpacing::EndNewLine,
759 },
760 table: None,
761 },
762 expected: indoc! {r#"
763 [Data]
764 select = "\n SELECT column\n FROM table t1\n INNER JOIN t2\n ON t1.id = t2.\n WHERE t1.userid = {{ user_id }}\n ORDER BY name ASC\n"
765 | col1 |
766 |-------|
767 | name1 |
768 | name2 |
769
770 "#},
771 };
772 const TEST_1B_DICTIONARY_MULTILINE_CASE: IonFormatTestCase = IonFormatTestCase {
773 description: "TEST_1B same input as TEST_1A with multiline dictionary field style",
774 options: FormatOptions {
775 dictionary: DictionaryOptions {
776 field: FieldStyle::Multiline,
777 },
778 ..TEST_1A_BASE_CASE.options
779 },
780 expected: indoc! {r#"
781 [Data]
782 select = "
783 SELECT column
784 FROM table t1
785 INNER JOIN t2
786 ON t1.id = t2.
787 WHERE t1.userid = {{ user_id }}
788 ORDER BY name ASC
789 "
790 | col1 |
791 |-------|
792 | name1 |
793 | name2 |
794
795 "#},
796 ..TEST_1A_BASE_CASE
797 };
798 const TEST_1C_SECTION_ADDITIONAL_NEWLINE_CASE: IonFormatTestCase = IonFormatTestCase {
799 description: "TEST_1C same input as TEST_1A with additional newline between dictionary and table",
800 options: FormatOptions {
801 section: SectionOptions {
802 spacing: SectionSpacing::AdditionalNewLine,
803 },
804 ..TEST_1A_BASE_CASE.options
805 },
806 expected: indoc! {r#"
807 [Data]
808 select = "\n SELECT column\n FROM table t1\n INNER JOIN t2\n ON t1.id = t2.\n WHERE t1.userid = {{ user_id }}\n ORDER BY name ASC\n"
809
810 | col1 |
811 |-------|
812 | name1 |
813 | name2 |
814
815 "#},
816 ..TEST_1A_BASE_CASE
817 };
818 const TEST_1D_DOCUMENT_ADDITIONAL_NEWLINE_CASE: IonFormatTestCase = IonFormatTestCase {
819 description: "TEST_1D same input as TEST_1A with additional newline at document end",
820 options: FormatOptions {
821 document: DocumentOptions {
822 spacing: DocumentSpacing::AdditionalEndNewLine,
823 },
824 table: None,
825 ..TEST_1A_BASE_CASE.options
826 },
827 expected: indoc! {r#"
828 [Data]
829 select = "\n SELECT column\n FROM table t1\n INNER JOIN t2\n ON t1.id = t2.\n WHERE t1.userid = {{ user_id }}\n ORDER BY name ASC\n"
830 | col1 |
831 |-------|
832 | name1 |
833 | name2 |
834
835
836 "#},
837 ..TEST_1A_BASE_CASE
838 };
839 const TEST_1E_DEFAULT_BEHAVIOR_CASE: IonFormatTestCase = IonFormatTestCase {
840 description: "TEST_1E same input as TEST_1A with current default option combination",
841 options: FormatOptions {
842 dictionary: DictionaryOptions {
843 field: FieldStyle::Multiline,
844 },
845 section: SectionOptions {
846 spacing: SectionSpacing::AdditionalNewLine,
847 },
848 document: DocumentOptions {
849 spacing: DocumentSpacing::EndNewLine,
850 },
851 table: None,
852 },
853 expected: indoc! {r#"
854 [Data]
855 select = "
856 SELECT column
857 FROM table t1
858 INNER JOIN t2
859 ON t1.id = t2.
860 WHERE t1.userid = {{ user_id }}
861 ORDER BY name ASC
862 "
863
864 | col1 |
865 |-------|
866 | name1 |
867 | name2 |
868
869 "#},
870 ..TEST_1A_BASE_CASE
871 };
872 const TEST_2_WITHOUT_HEADER_TABLE: IonFormatTestCase = IonFormatTestCase {
873 description: "TEST_2 table rows without explicit header separator are formatted consistently",
874 raw: indoc! {r"
875 [WITHOUT_HEADER]
876 | 1 | alpha | PL | 11.2 | ok |
877 | 2 | beta | DE | 9 | hold |
878 | 3 | gamma | UK | 13.75 | ok |
879 "},
880 options: FormatOptions {
881 dictionary: DictionaryOptions {
882 field: FieldStyle::Singleline,
883 },
884 section: SectionOptions {
885 spacing: SectionSpacing::AdditionalNewLine,
886 },
887 document: DocumentOptions {
888 spacing: DocumentSpacing::EndNewLine,
889 },
890 table: None,
891 },
892 expected: indoc! {r"
893 [WITHOUT_HEADER]
894 | 1 | alpha | PL | 11.2 | ok |
895 | 2 | beta | DE | 9 | hold |
896 | 3 | gamma | UK | 13.75 | ok |
897
898 "},
899 };
900 const TEST_2A_ADD_HEADER_SEPARATOR_WHEN_MISSING: IonFormatTestCase = IonFormatTestCase {
901 description: "TEST_2A synthesizes a header separator when configured",
902 raw: indoc! {r"
903 [WITHOUT_HEADER]
904 | id | name | country | amount | status |
905 | 1 | alpha | PL | 11.2 | ok |
906 | 2 | beta | DE | 9 | hold |
907 | 3 | gamma | UK | 13.75 | ok |
908 "},
909 options: FormatOptions {
910 dictionary: DictionaryOptions {
911 field: FieldStyle::Singleline,
912 },
913 section: SectionOptions {
914 spacing: SectionSpacing::AdditionalNewLine,
915 },
916 document: DocumentOptions {
917 spacing: DocumentSpacing::EndNewLine,
918 },
919 table: Some(TableOptions::AddHeaderSeparatorWhenMissing),
920 },
921 expected: indoc! {r"
922 [WITHOUT_HEADER]
923 | id | name | country | amount | status |
924 |----|-------|---------|--------|--------|
925 | 1 | alpha | PL | 11.2 | ok |
926 | 2 | beta | DE | 9 | hold |
927 | 3 | gamma | UK | 13.75 | ok |
928
929 "},
930 };
931 const TEST_3A_NESTED_DICTIONARY: IonFormatTestCase = IonFormatTestCase {
932 description: "TEST_3A formats dictionary with string, array, and nested dictionary values",
933 raw: indoc! {r#"
934 [CONTRACT]
935 country = "Poland"
936 markets = ["PL", "DE", "UK"]
937 75042 = {
938 view = "SV"
939 loc = ["M", "B"]
940 dist = { beach_km = 4.1 }
941 }
942 "#},
943 options: FormatOptions {
944 dictionary: DictionaryOptions {
945 field: FieldStyle::Singleline,
946 },
947 section: SectionOptions {
948 spacing: SectionSpacing::AdditionalNewLine,
949 },
950 document: DocumentOptions {
951 spacing: DocumentSpacing::EndNewLine,
952 },
953 table: None,
954 },
955 #[cfg(not(feature = "dictionary-indexmap"))]
956 expected: indoc! {r#"
957 [CONTRACT]
958 75042 = { dist = { beach_km = 4.1 }, loc = [ "M", "B" ], view = "SV" }
959 country = "Poland"
960 markets = [ "PL", "DE", "UK" ]
961
962 "#},
963 #[cfg(feature = "dictionary-indexmap")]
964 expected: indoc! {r#"
965 [CONTRACT]
966 country = "Poland"
967 markets = [ "PL", "DE", "UK" ]
968 75042 = { view = "SV", loc = [ "M", "B" ], dist = { beach_km = 4.1 } }
969
970 "#},
971 };
972 const TEST_3B_NESTED_DICTIONARY: IonFormatTestCase = IonFormatTestCase {
973 description: "TEST_3B same input as TEST_3A with multiline dictionary field style",
974 options: FormatOptions {
975 dictionary: DictionaryOptions {
976 field: FieldStyle::Multiline,
977 },
978 ..TEST_3A_NESTED_DICTIONARY.options
979 },
980 #[cfg(not(feature = "dictionary-indexmap"))]
981 expected: indoc! {r#"
982 [CONTRACT]
983 75042 = {
984 dist = {
985 beach_km = 4.1
986 }
987 loc = [ "M", "B" ]
988 view = "SV"
989 }
990 country = "Poland"
991 markets = [ "PL", "DE", "UK" ]
992
993 "#},
994 #[cfg(feature = "dictionary-indexmap")]
995 expected: indoc! {r#"
996 [CONTRACT]
997 country = "Poland"
998 markets = [ "PL", "DE", "UK" ]
999 75042 = {
1000 view = "SV"
1001 loc = [ "M", "B" ]
1002 dist = {
1003 beach_km = 4.1
1004 }
1005 }
1006
1007 "#},
1008 ..TEST_3A_NESTED_DICTIONARY
1009 };
1010 const TEST_3C_NESTED_DICTIONARY: IonFormatTestCase = IonFormatTestCase {
1011 description: "TEST_3C same input as TEST_3A with default dictionary field style",
1012 options: FormatOptions {
1013 dictionary: DictionaryOptions {
1014 field: FieldStyle::Multiline,
1015 },
1016 document: DocumentOptions {
1017 spacing: DocumentSpacing::AdditionalEndNewLine,
1018 },
1019 table: None,
1020 ..TEST_3A_NESTED_DICTIONARY.options
1021 },
1022 #[cfg(not(feature = "dictionary-indexmap"))]
1023 expected: indoc! {r#"
1024 [CONTRACT]
1025 75042 = {
1026 dist = {
1027 beach_km = 4.1
1028 }
1029 loc = [ "M", "B" ]
1030 view = "SV"
1031 }
1032 country = "Poland"
1033 markets = [ "PL", "DE", "UK" ]
1034
1035
1036 "#},
1037 #[cfg(feature = "dictionary-indexmap")]
1038 expected: indoc! {r#"
1039 [CONTRACT]
1040 country = "Poland"
1041 markets = [ "PL", "DE", "UK" ]
1042 75042 = {
1043 view = "SV"
1044 loc = [ "M", "B" ]
1045 dist = {
1046 beach_km = 4.1
1047 }
1048 }
1049
1050
1051 "#},
1052 ..TEST_3A_NESTED_DICTIONARY
1053 };
1054 const TEST_4A_DEEP_NESTED_DICTIONARY: IonFormatTestCase = IonFormatTestCase {
1055 description: "TEST_4A formats deep nested dictionary field in singleline mode",
1056 raw: indoc! {r#"
1057 [CONFIG]
1058 config = {
1059 zeta = {
1060 release = {
1061 minor = 2
1062 major = 1
1063 }
1064 flags = [true, false]
1065 select = "
1066 SELECT column
1067 FROM table t4
1068 INNER JOIN t6
1069 ON t4.id = t6.id
1070 WHERE t4.userid = {{ user_id }}
1071 ORDER BY name ASC
1072 "
1073 }
1074 alpha = "pkg"
1075 select = "
1076 SELECT column
1077 FROM table t1
1078 INNER JOIN t2
1079 ON t1.id = t2.something
1080 WHERE t1.userid = {{ user_id }}
1081 ORDER BY name ASC
1082 "
1083 }
1084 "#},
1085 options: FormatOptions {
1086 dictionary: DictionaryOptions {
1087 field: FieldStyle::Singleline,
1088 },
1089 section: SectionOptions {
1090 spacing: SectionSpacing::AdditionalNewLine,
1091 },
1092 document: DocumentOptions {
1093 spacing: DocumentSpacing::EndNewLine,
1094 },
1095 table: None,
1096 },
1097 #[cfg(not(feature = "dictionary-indexmap"))]
1098 expected: indoc! {r#"
1099 [CONFIG]
1100 config = { alpha = "pkg", select = "\n SELECT column\n FROM table t1\n INNER JOIN t2\n ON t1.id = t2.something\n WHERE t1.userid = {{ user_id }}\n ORDER BY name ASC\n ", zeta = { flags = [ true, false ], release = { major = 1, minor = 2 }, select = "\n SELECT column\n FROM table t4\n INNER JOIN t6\n ON t4.id = t6.id\n WHERE t4.userid = {{ user_id }}\n ORDER BY name ASC\n " } }
1101
1102 "#},
1103 #[cfg(feature = "dictionary-indexmap")]
1104 expected: indoc! {r#"
1105 [CONFIG]
1106 config = { zeta = { release = { minor = 2, major = 1 }, flags = [ true, false ], select = "\n SELECT column\n FROM table t4\n INNER JOIN t6\n ON t4.id = t6.id\n WHERE t4.userid = {{ user_id }}\n ORDER BY name ASC\n " }, alpha = "pkg", select = "\n SELECT column\n FROM table t1\n INNER JOIN t2\n ON t1.id = t2.something\n WHERE t1.userid = {{ user_id }}\n ORDER BY name ASC\n " }
1107
1108 "#},
1109 };
1110 const TEST_4B_DEEP_NESTED_DICTIONARY: IonFormatTestCase = IonFormatTestCase {
1111 description: "TEST_4B same input as TEST_4A in multiline mode",
1112 options: FormatOptions {
1113 dictionary: DictionaryOptions {
1114 field: FieldStyle::Multiline,
1115 },
1116 section: SectionOptions {
1117 spacing: SectionSpacing::AdditionalNewLine,
1118 },
1119 document: DocumentOptions {
1120 spacing: DocumentSpacing::EndNewLine,
1121 },
1122 table: None,
1123 },
1124 #[cfg(not(feature = "dictionary-indexmap"))]
1125 expected: indoc! {r#"
1126 [CONFIG]
1127 config = {
1128 alpha = "pkg"
1129 select = "
1130 SELECT column
1131 FROM table t1
1132 INNER JOIN t2
1133 ON t1.id = t2.something
1134 WHERE t1.userid = {{ user_id }}
1135 ORDER BY name ASC
1136 "
1137 zeta = {
1138 flags = [ true, false ]
1139 release = {
1140 major = 1
1141 minor = 2
1142 }
1143 select = "
1144 SELECT column
1145 FROM table t4
1146 INNER JOIN t6
1147 ON t4.id = t6.id
1148 WHERE t4.userid = {{ user_id }}
1149 ORDER BY name ASC
1150 "
1151 }
1152 }
1153
1154 "#},
1155 #[cfg(feature = "dictionary-indexmap")]
1156 expected: indoc! {r#"
1157 [CONFIG]
1158 config = {
1159 zeta = {
1160 release = {
1161 minor = 2
1162 major = 1
1163 }
1164 flags = [ true, false ]
1165 select = "
1166 SELECT column
1167 FROM table t4
1168 INNER JOIN t6
1169 ON t4.id = t6.id
1170 WHERE t4.userid = {{ user_id }}
1171 ORDER BY name ASC
1172 "
1173 }
1174 alpha = "pkg"
1175 select = "
1176 SELECT column
1177 FROM table t1
1178 INNER JOIN t2
1179 ON t1.id = t2.something
1180 WHERE t1.userid = {{ user_id }}
1181 ORDER BY name ASC
1182 "
1183 }
1184
1185 "#},
1186 ..TEST_4A_DEEP_NESTED_DICTIONARY
1187 };
1188 const TEST_5A_TABLE_ONLY_WITH_DEFAULT_SECTION_SPACING_CASE: IonFormatTestCase =
1189 IonFormatTestCase {
1190 description: "TEST_5A table-only input does not add section spacing when dictionary is empty",
1191 raw: indoc! {r"
1192 [TABLE]
1193 | c |
1194 |---|
1195 | 1 |
1196 "},
1197 options: FormatOptions {
1198 dictionary: DictionaryOptions {
1199 field: FieldStyle::Singleline,
1200 },
1201 section: SectionOptions {
1202 spacing: SectionSpacing::AdditionalNewLine,
1203 },
1204 document: DocumentOptions {
1205 spacing: DocumentSpacing::EndNewLine,
1206 },
1207 table: None,
1208 },
1209 expected: indoc! {r"
1210 [TABLE]
1211 | c |
1212 |---|
1213 | 1 |
1214
1215 "},
1216 };
1217 #[test_case(&*ION_FORMAT_CASE)]
1218 #[test_case(&*ION_FORMAT_NON_STRING_DICTIONARY_CASE)]
1219 #[test_case(&TEST_1A_BASE_CASE)]
1220 #[test_case(&TEST_1B_DICTIONARY_MULTILINE_CASE)]
1221 #[test_case(&TEST_1C_SECTION_ADDITIONAL_NEWLINE_CASE)]
1222 #[test_case(&TEST_1D_DOCUMENT_ADDITIONAL_NEWLINE_CASE)]
1223 #[test_case(&TEST_1E_DEFAULT_BEHAVIOR_CASE)]
1224 #[test_case(&TEST_2_WITHOUT_HEADER_TABLE)]
1225 #[test_case(&TEST_2A_ADD_HEADER_SEPARATOR_WHEN_MISSING)]
1226 #[test_case(&TEST_3A_NESTED_DICTIONARY)]
1227 #[test_case(&TEST_3B_NESTED_DICTIONARY)]
1228 #[test_case(&TEST_3C_NESTED_DICTIONARY)]
1229 #[test_case(&TEST_4A_DEEP_NESTED_DICTIONARY)]
1230 #[test_case(&TEST_4B_DEEP_NESTED_DICTIONARY)]
1231 #[test_case(&TEST_5A_TABLE_ONLY_WITH_DEFAULT_SECTION_SPACING_CASE)]
1232 fn format_ion_document(case: &IonFormatTestCase) {
1233 let ion = case.raw.parse::<Ion>().unwrap();
1234 assert_eq!(
1235 case.expected,
1236 format_ion_with_options(&ion, case.options),
1237 "{}",
1238 case.description
1239 );
1240 assert_eq!(
1241 case.expected,
1242 display_with_options(&ion, case.options).to_string(),
1243 "{}",
1244 case.description
1245 );
1246 }
1247
1248 static DICTIONARY_DISPLAY_SINGLELINE_CASE: LazyLock<DictionaryDisplayTestCase> =
1249 LazyLock::new(|| DictionaryDisplayTestCase {
1250 description: "dictionary display with singleline style",
1251 dictionary: dictionary([("a", string("foo")), ("query", string("\nSELECT 1\n"))]),
1252 field: FieldStyle::Singleline,
1253 expected: "a = \"foo\"\nquery = \"\\nSELECT 1\\n\"\n",
1254 });
1255 static DICTIONARY_DISPLAY_MULTILINE_CASE: LazyLock<DictionaryDisplayTestCase> =
1256 LazyLock::new(|| DictionaryDisplayTestCase {
1257 description: "dictionary display with multiline style",
1258 dictionary: dictionary([("a", string("foo")), ("query", string("\nSELECT 1\n"))]),
1259 field: FieldStyle::Multiline,
1260 expected: "a = \"foo\"\nquery = \"\nSELECT 1\n\"\n",
1261 });
1262
1263 #[test_case(&*DICTIONARY_DISPLAY_SINGLELINE_CASE)]
1264 #[test_case(&*DICTIONARY_DISPLAY_MULTILINE_CASE)]
1265 fn display_dictionary(case: &DictionaryDisplayTestCase) {
1266 assert_eq!(
1267 case.expected,
1268 DictionaryDisplay::new(&case.dictionary, case.field).to_string(),
1269 "{}",
1270 case.description
1271 );
1272 }
1273
1274 static DICTIONARY_FIELD_NON_STRING_CASE: LazyLock<DictionaryFieldDisplayTestCase> =
1275 LazyLock::new(|| DictionaryFieldDisplayTestCase {
1276 description: "dictionary field non string value",
1277 key: "count",
1278 value: Value::Integer(7),
1279 field: FieldStyle::Singleline,
1280 expected: "count = 7\n",
1281 });
1282 static DICTIONARY_FIELD_MULTILINE_SINGLELINE_CASE: LazyLock<DictionaryFieldDisplayTestCase> =
1283 LazyLock::new(|| DictionaryFieldDisplayTestCase {
1284 description: "dictionary field multiline value with singleline style",
1285 key: "query",
1286 value: string("\nSELECT 1\n"),
1287 field: FieldStyle::Singleline,
1288 expected: "query = \"\\nSELECT 1\\n\"\n",
1289 });
1290 static DICTIONARY_FIELD_MULTILINE_MULTILINE_CASE: LazyLock<DictionaryFieldDisplayTestCase> =
1291 LazyLock::new(|| DictionaryFieldDisplayTestCase {
1292 description: "dictionary field multiline value with multiline style",
1293 key: "query",
1294 value: string("\nSELECT 1\n"),
1295 field: FieldStyle::Multiline,
1296 expected: "query = \"\nSELECT 1\n\"\n",
1297 });
1298 static DICTIONARY_FIELD_DICTIONARY_MULTILINE_CASE: LazyLock<DictionaryFieldDisplayTestCase> =
1299 LazyLock::new(|| {
1300 let value = Value::Dictionary(dictionary([
1301 ("view", string("SV")),
1302 ("loc", Value::Array(vec![string("M"), string("B")])),
1303 (
1304 "dist",
1305 Value::Dictionary(dictionary([("beach_km", Value::Float(4.1))])),
1306 ),
1307 ]));
1308 DictionaryFieldDisplayTestCase {
1309 description: "dictionary field dictionary value with multiline style",
1310 key: "75042",
1311 value,
1312 field: FieldStyle::Multiline,
1313 expected: if cfg!(feature = "dictionary-indexmap") {
1314 indoc! {r#"
1315 75042 = {
1316 view = "SV"
1317 loc = [ "M", "B" ]
1318 dist = {
1319 beach_km = 4.1
1320 }
1321 }
1322 "#}
1323 } else {
1324 indoc! {r#"
1325 75042 = {
1326 dist = {
1327 beach_km = 4.1
1328 }
1329 loc = [ "M", "B" ]
1330 view = "SV"
1331 }
1332 "#}
1333 },
1334 }
1335 });
1336 #[test_case(&*DICTIONARY_FIELD_NON_STRING_CASE)]
1337 #[test_case(&*DICTIONARY_FIELD_MULTILINE_SINGLELINE_CASE)]
1338 #[test_case(&*DICTIONARY_FIELD_MULTILINE_MULTILINE_CASE)]
1339 #[test_case(&*DICTIONARY_FIELD_DICTIONARY_MULTILINE_CASE)]
1340 fn display_dictionary_field(case: &DictionaryFieldDisplayTestCase) {
1341 assert_eq!(
1342 case.expected,
1343 DictionaryFieldDisplay::new(case.key, &case.value, case.field).to_string(),
1344 "{}",
1345 case.description
1346 );
1347 }
1348
1349 fn render_dictionary_multiline_string(key: &str, text: &str) -> String {
1350 struct DictionaryMultilineStringDisplay<'a> {
1351 key: &'a str,
1352 text: &'a str,
1353 }
1354
1355 impl std::fmt::Display for DictionaryMultilineStringDisplay<'_> {
1356 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1357 write_dictionary_multiline_string(f, self.key, self.text, 0)
1358 }
1359 }
1360
1361 DictionaryMultilineStringDisplay { key, text }.to_string()
1362 }
1363
1364 const WRITE_MULTILINE_DICTIONARY_STRING_EMPTY_CASE: WriteDictionaryMultilineStringTestCase =
1365 WriteDictionaryMultilineStringTestCase {
1366 description: "writes empty multiline dictionary string",
1367 key: "query",
1368 text: "",
1369 expected: "query = \"\"",
1370 };
1371 const WRITE_MULTILINE_DICTIONARY_STRING_MULTILINE_CASE: WriteDictionaryMultilineStringTestCase =
1372 WriteDictionaryMultilineStringTestCase {
1373 description: "preserves multiline text and trailing newline",
1374 key: "query",
1375 text: "\nSELECT 1\nFROM dual\n",
1376 expected: "query = \"\nSELECT 1\nFROM dual\n\"",
1377 };
1378 const WRITE_MULTILINE_DICTIONARY_STRING_ESCAPES_QUOTE_CASE:
1379 WriteDictionaryMultilineStringTestCase = WriteDictionaryMultilineStringTestCase {
1380 description: "escapes quotes in multiline dictionary string",
1381 key: "query",
1382 text: "value \"quoted\"",
1383 expected: "query = \"value \\\"quoted\\\"\"",
1384 };
1385 const WRITE_MULTILINE_DICTIONARY_STRING_ESCAPES_BACKSLASH_CASE:
1386 WriteDictionaryMultilineStringTestCase = WriteDictionaryMultilineStringTestCase {
1387 description: "escapes backslashes in multiline dictionary string",
1388 key: "query",
1389 text: r"C:\work\ion",
1390 expected: "query = \"C:\\\\work\\\\ion\"",
1391 };
1392 const WRITE_MULTILINE_DICTIONARY_STRING_ESCAPES_QUOTE_AND_BACKSLASH_CASE:
1393 WriteDictionaryMultilineStringTestCase = WriteDictionaryMultilineStringTestCase {
1394 description: "escapes quotes and backslashes while preserving newlines",
1395 key: "query",
1396 text: "path=\"C:\\work\"\n-- done",
1397 expected: "query = \"path=\\\"C:\\\\work\\\"\n-- done\"",
1398 };
1399
1400 #[test_case(&WRITE_MULTILINE_DICTIONARY_STRING_EMPTY_CASE)]
1401 #[test_case(&WRITE_MULTILINE_DICTIONARY_STRING_MULTILINE_CASE)]
1402 #[test_case(&WRITE_MULTILINE_DICTIONARY_STRING_ESCAPES_QUOTE_CASE)]
1403 #[test_case(&WRITE_MULTILINE_DICTIONARY_STRING_ESCAPES_BACKSLASH_CASE)]
1404 #[test_case(&WRITE_MULTILINE_DICTIONARY_STRING_ESCAPES_QUOTE_AND_BACKSLASH_CASE)]
1405 fn write_dictionary_multiline_string_cases(case: &WriteDictionaryMultilineStringTestCase) {
1406 assert_eq!(
1407 case.expected,
1408 render_dictionary_multiline_string(case.key, case.text),
1409 "{}",
1410 case.description
1411 );
1412 }
1413
1414 static HEADER_ROW_DISPLAY_CASE: LazyLock<RowDisplayTestCase> =
1415 LazyLock::new(|| RowDisplayTestCase {
1416 description: "header row",
1417 row: vec![string("num"), string("total")],
1418 row_type: RowTypeDisplay::Header,
1419 columns_width: ColumnsWidth::new(vec![
1420 Column {
1421 width: 3,
1422 typ: ColumnType::Text,
1423 },
1424 Column {
1425 width: 5,
1426 typ: ColumnType::Text,
1427 },
1428 ]),
1429 expected: "| num | total |\n",
1430 });
1431 static SEPARATOR_ROW_DISPLAY_CASE: LazyLock<RowDisplayTestCase> =
1432 LazyLock::new(|| RowDisplayTestCase {
1433 description: "separator row",
1434 row: vec![string("num"), string("total")],
1435 row_type: RowTypeDisplay::Separator,
1436 columns_width: ColumnsWidth::new(vec![
1437 Column {
1438 width: 3,
1439 typ: ColumnType::Text,
1440 },
1441 Column {
1442 width: 5,
1443 typ: ColumnType::Text,
1444 },
1445 ]),
1446 expected: "|-----|-------|\n",
1447 });
1448 static DATA_ROW_DISPLAY_CASE: LazyLock<RowDisplayTestCase> =
1449 LazyLock::new(|| RowDisplayTestCase {
1450 description: "data row",
1451 row: vec![Value::Integer(12), string("A")],
1452 row_type: RowTypeDisplay::Data,
1453 columns_width: ColumnsWidth::new(vec![
1454 Column {
1455 width: 3,
1456 typ: ColumnType::Number,
1457 },
1458 Column {
1459 width: 5,
1460 typ: ColumnType::Text,
1461 },
1462 ]),
1463 expected: "| 12 | A |\n",
1464 });
1465 static HEADER_ROW_DISPLAY_EVEN_TEXT_CASE: LazyLock<RowDisplayTestCase> =
1466 LazyLock::new(|| RowDisplayTestCase {
1467 description: "header row with even text",
1468 row: vec![string("ABCD"), string("EF")],
1469 row_type: RowTypeDisplay::Header,
1470 columns_width: ColumnsWidth::new(vec![
1471 Column {
1472 width: 5,
1473 typ: ColumnType::Text,
1474 },
1475 Column {
1476 width: 4,
1477 typ: ColumnType::Text,
1478 },
1479 ]),
1480 expected: "| ABCD | EF |\n",
1481 });
1482 static DATA_ROW_DISPLAY_TEXT_THEN_NUMBER_CASE: LazyLock<RowDisplayTestCase> =
1483 LazyLock::new(|| RowDisplayTestCase {
1484 description: "data row with text first and number next",
1485 row: vec![string("A"), Value::Integer(7)],
1486 row_type: RowTypeDisplay::Data,
1487 columns_width: ColumnsWidth::new(vec![
1488 Column {
1489 width: 3,
1490 typ: ColumnType::Text,
1491 },
1492 Column {
1493 width: 4,
1494 typ: ColumnType::Number,
1495 },
1496 ]),
1497 expected: "| A | 7 |\n",
1498 });
1499 static EMPTY_HEADER_ROW_DISPLAY_CASE: LazyLock<RowDisplayTestCase> =
1500 LazyLock::new(|| RowDisplayTestCase {
1501 description: "empty header row",
1502 row: vec![],
1503 row_type: RowTypeDisplay::Header,
1504 columns_width: ColumnsWidth::new(vec![]),
1505 expected: "",
1506 });
1507 static EMPTY_SEPARATOR_ROW_DISPLAY_CASE: LazyLock<RowDisplayTestCase> =
1508 LazyLock::new(|| RowDisplayTestCase {
1509 description: "empty separator row",
1510 row: vec![],
1511 row_type: RowTypeDisplay::Separator,
1512 columns_width: ColumnsWidth::new(vec![]),
1513 expected: "",
1514 });
1515 static EMPTY_DATA_ROW_DISPLAY_CASE: LazyLock<RowDisplayTestCase> =
1516 LazyLock::new(|| RowDisplayTestCase {
1517 description: "empty data row",
1518 row: vec![],
1519 row_type: RowTypeDisplay::Data,
1520 columns_width: ColumnsWidth::new(vec![]),
1521 expected: "",
1522 });
1523
1524 #[test_case(&*HEADER_ROW_DISPLAY_CASE)]
1525 #[test_case(&*SEPARATOR_ROW_DISPLAY_CASE)]
1526 #[test_case(&*DATA_ROW_DISPLAY_CASE)]
1527 #[test_case(&*HEADER_ROW_DISPLAY_EVEN_TEXT_CASE)]
1528 #[test_case(&*DATA_ROW_DISPLAY_TEXT_THEN_NUMBER_CASE)]
1529 #[test_case(&*EMPTY_HEADER_ROW_DISPLAY_CASE)]
1530 #[test_case(&*EMPTY_SEPARATOR_ROW_DISPLAY_CASE)]
1531 #[test_case(&*EMPTY_DATA_ROW_DISPLAY_CASE)]
1532 fn display_row(case: &RowDisplayTestCase) {
1533 let row_display = RowDisplay {
1534 columns_width: &case.columns_width,
1535 row: &case.row,
1536 row_type: case.row_type,
1537 };
1538 assert_eq!(
1539 case.expected,
1540 row_display.to_string(),
1541 "{}",
1542 case.description
1543 );
1544 }
1545}