automapper-validation 0.12.0

AHB condition expression parsing, evaluation, and EDIFACT validation
Documentation
//! Whole-report renderings built on [`IssueDisplay`](super::IssueDisplay).

use crate::ValidationReport;

use super::{IssueDisplay, IssueNarrator, IssueView};

/// One finding per line, for a text area or a log.
///
/// `[ERROR] text` — `Severity`'s own `Display` (ERROR/WARN/INFO), then the
/// narrated text. Issues with no location in this view are marked so the reader
/// is not left wondering why.
pub fn lines(
    report: &ValidationReport,
    view: &dyn IssueView,
    narrator: &dyn IssueNarrator,
) -> String {
    report
        .issues
        .iter()
        .map(|issue| {
            let d = IssueDisplay::new(issue, view, narrator);
            match d.location {
                Some(_) => format!("[{}] {}", d.severity, d.text),
                None => format!("[{}] {} (kein {}-Feld)", d.severity, d.text, view.name()),
            }
        })
        .collect::<Vec<_>>()
        .join("\n")
}

/// Tab-separated: `severity ⇥ location ⇥ code ⇥ text`. For export and for an
/// LLM that wants a compact rendering.
pub fn table(
    report: &ValidationReport,
    view: &dyn IssueView,
    narrator: &dyn IssueNarrator,
) -> String {
    report
        .issues
        .iter()
        .map(|issue| {
            let d = IssueDisplay::new(issue, view, narrator);
            format!(
                "{}\t{}\t{}\t{}",
                d.severity,
                d.location.as_deref().unwrap_or("-"),
                d.code,
                d.text
            )
        })
        .collect::<Vec<_>>()
        .join("\n")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::display::{EdifactView, TechnicalNarrator};
    use crate::{IssueKind, Severity, ValidationIssue, ValidationLevel};

    fn two_issue_report() -> ValidationReport {
        let mut report = ValidationReport::new("UTILMD", ValidationLevel::Full);
        report.add_issue(
            ValidationIssue::new(
                Severity::Error,
                IssueKind::MissingRequiredField {
                    field_name: "LOC+Z16".into(),
                },
            )
            .with_field_path("SG4/SG5/LOC/C517/3225"),
        );
        report.add_issue(ValidationIssue::new(
            Severity::Warning,
            IssueKind::UntSegmentCountMismatch {
                declared: 30,
                actual: 31,
            },
        ));
        report
    }

    #[test]
    fn lines_marks_issues_with_no_location_in_this_view() {
        let report = two_issue_report();
        let rendered = lines(&report, &EdifactView, &TechnicalNarrator);

        assert_eq!(
            rendered,
            "[ERROR] Required field 'LOC+Z16' at SG4/SG5/LOC/C517/3225 is missing\n\
             [WARN] UNT segment count mismatch: declared 30, actual 31 (kein EDIFACT-Feld)"
        );
    }

    #[test]
    fn table_is_tab_separated_severity_location_code_text() {
        let report = two_issue_report();
        let rendered = table(&report, &EdifactView, &TechnicalNarrator);
        let rows: Vec<&str> = rendered.split('\n').collect();

        assert_eq!(
            rows[0],
            "ERROR\tSG4/SG5/LOC/C517/3225\tAHB001\tRequired field 'LOC+Z16' at SG4/SG5/LOC/C517/3225 is missing"
        );
        assert_eq!(
            rows[1],
            "WARN\t-\tSTR007\tUNT segment count mismatch: declared 30, actual 31"
        );
    }
}