Skip to main content

automapper_validation/display/
format_impl.rs

1//! Whole-report renderings built on [`IssueDisplay`](super::IssueDisplay).
2
3use crate::ValidationReport;
4
5use super::{IssueDisplay, IssueNarrator, IssueView};
6
7/// One finding per line, for a text area or a log.
8///
9/// `[ERROR] text` — `Severity`'s own `Display` (ERROR/WARN/INFO), then the
10/// narrated text. Issues with no location in this view are marked so the reader
11/// is not left wondering why.
12pub fn lines(
13    report: &ValidationReport,
14    view: &dyn IssueView,
15    narrator: &dyn IssueNarrator,
16) -> String {
17    report
18        .issues
19        .iter()
20        .map(|issue| {
21            let d = IssueDisplay::new(issue, view, narrator);
22            match d.location {
23                Some(_) => format!("[{}] {}", d.severity, d.text),
24                None => format!("[{}] {} (kein {}-Feld)", d.severity, d.text, view.name()),
25            }
26        })
27        .collect::<Vec<_>>()
28        .join("\n")
29}
30
31/// Tab-separated: `severity ⇥ location ⇥ code ⇥ text`. For export and for an
32/// LLM that wants a compact rendering.
33pub fn table(
34    report: &ValidationReport,
35    view: &dyn IssueView,
36    narrator: &dyn IssueNarrator,
37) -> String {
38    report
39        .issues
40        .iter()
41        .map(|issue| {
42            let d = IssueDisplay::new(issue, view, narrator);
43            format!(
44                "{}\t{}\t{}\t{}",
45                d.severity,
46                d.location.as_deref().unwrap_or("-"),
47                d.code,
48                d.text
49            )
50        })
51        .collect::<Vec<_>>()
52        .join("\n")
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use crate::display::{EdifactView, TechnicalNarrator};
59    use crate::{IssueKind, Severity, ValidationIssue, ValidationLevel};
60
61    fn two_issue_report() -> ValidationReport {
62        let mut report = ValidationReport::new("UTILMD", ValidationLevel::Full);
63        report.add_issue(
64            ValidationIssue::new(
65                Severity::Error,
66                IssueKind::MissingRequiredField {
67                    field_name: "LOC+Z16".into(),
68                },
69            )
70            .with_field_path("SG4/SG5/LOC/C517/3225"),
71        );
72        report.add_issue(ValidationIssue::new(
73            Severity::Warning,
74            IssueKind::UntSegmentCountMismatch {
75                declared: 30,
76                actual: 31,
77            },
78        ));
79        report
80    }
81
82    #[test]
83    fn lines_marks_issues_with_no_location_in_this_view() {
84        let report = two_issue_report();
85        let rendered = lines(&report, &EdifactView, &TechnicalNarrator);
86
87        assert_eq!(
88            rendered,
89            "[ERROR] Required field 'LOC+Z16' at SG4/SG5/LOC/C517/3225 is missing\n\
90             [WARN] UNT segment count mismatch: declared 30, actual 31 (kein EDIFACT-Feld)"
91        );
92    }
93
94    #[test]
95    fn table_is_tab_separated_severity_location_code_text() {
96        let report = two_issue_report();
97        let rendered = table(&report, &EdifactView, &TechnicalNarrator);
98        let rows: Vec<&str> = rendered.split('\n').collect();
99
100        assert_eq!(
101            rows[0],
102            "ERROR\tSG4/SG5/LOC/C517/3225\tAHB001\tRequired field 'LOC+Z16' at SG4/SG5/LOC/C517/3225 is missing"
103        );
104        assert_eq!(
105            rows[1],
106            "WARN\t-\tSTR007\tUNT segment count mismatch: declared 30, actual 31"
107        );
108    }
109}