use crate::{IssueKind, UnresolvedConditions, ValidationIssue};
use super::IssueNarrator;
pub struct TechnicalNarrator;
fn at(location: Option<&str>) -> String {
location.map_or_else(String::new, |l| format!(" at {l}"))
}
fn ids(list: &[u32]) -> String {
list.iter()
.map(|id| format!("[{id}]"))
.collect::<Vec<_>>()
.join(", ")
}
fn unresolved_detail(u: &UnresolvedConditions) -> String {
if u.is_empty() {
return String::new();
}
let mut parts = Vec::new();
if !u.external.is_empty() {
parts.push(format!(
"external conditions require provider: {}",
ids(&u.external)
));
}
if !u.undetermined.is_empty() {
parts.push(format!(
"conditions could not be determined from message data: {}",
ids(&u.undetermined)
));
}
if !u.missing.is_empty() {
parts.push(format!("missing conditions: {}", ids(&u.missing)));
}
format!(" ({})", parts.join("; "))
}
impl IssueNarrator for TechnicalNarrator {
fn describe(&self, issue: &ValidationIssue, location: Option<&str>) -> String {
match &issue.kind {
IssueKind::MissingRequiredField { field_name } => {
format!("Required field '{field_name}'{} is missing", at(location))
}
IssueKind::FieldConditionNotSatisfied { field_name } => format!(
"Field '{field_name}'{} is present but does not satisfy condition: {}",
at(location),
issue.rule.as_deref().unwrap_or_default()
),
IssueKind::ConditionUnknown { field_name, unresolved } => format!(
"Condition for field '{field_name}' could not be fully evaluated{}",
unresolved_detail(unresolved)
),
IssueKind::PackageCardinality { package_id, present, min, max, codes } => format!(
"Package [{package_id}P{min}..{max}]{}: {present} code(s) present (allowed {min}..{max}). \
Codes in package: [{}]",
at(location),
codes.join(", ")
),
IssueKind::CodeNotAllowedForPid { value, allowed } => format!(
"Code '{value}' is not allowed for this PID. Allowed: [{}]",
allowed.join(", ")
),
IssueKind::UntSegmentCountMismatch { declared, actual } => {
format!("UNT segment count mismatch: declared {declared}, actual {actual}")
}
IssueKind::UntCountNotVerifiable { unh_count } => format!(
"UNT validation requires per-message segments, found {unh_count} UNH segments"
),
IssueKind::StructureDiagnostic { detail, .. } => detail.clone(),
}
}
}
#[cfg(test)]
mod tests {
use crate::display::{Bo4eView, EdifactView, IssueNarrator, IssueView, TechnicalNarrator};
use crate::{IssueKind, Severity, UnresolvedConditions, ValidationIssue};
use mig_assembly::StructureDiagnosticKind;
fn missing_field_issue() -> ValidationIssue {
ValidationIssue::new(
Severity::Error,
IssueKind::MissingRequiredField {
field_name: "Merkmal, Code".into(),
},
)
.with_field_path("SG4/SG8/SG10/CCI/C240/7037")
.with_bo4e_path("stammdaten.Marktlokation.haushaltskunde")
}
#[test]
fn edifact_view_narrates_with_the_edifact_path() {
let issue = missing_field_issue();
let loc = EdifactView.location(&issue);
assert_eq!(loc.as_deref(), Some("SG4/SG8/SG10/CCI/C240/7037"));
assert_eq!(
TechnicalNarrator.describe(&issue, loc.as_deref()),
"Required field 'Merkmal, Code' at SG4/SG8/SG10/CCI/C240/7037 is missing"
);
}
#[test]
fn bo4e_view_narrates_with_the_bo4e_path() {
let issue = missing_field_issue();
let loc = Bo4eView.location(&issue);
assert_eq!(
loc.as_deref(),
Some("stammdaten.Marktlokation.haushaltskunde")
);
assert_eq!(
TechnicalNarrator.describe(&issue, loc.as_deref()),
"Required field 'Merkmal, Code' at stammdaten.Marktlokation.haushaltskunde is missing"
);
}
#[test]
fn bo4e_view_has_no_location_for_envelope_issues() {
let issue = ValidationIssue::new(
Severity::Error,
IssueKind::UntSegmentCountMismatch {
declared: 30,
actual: 31,
},
)
.with_field_path("UNT/0074");
assert_eq!(Bo4eView.location(&issue), None);
assert_eq!(
TechnicalNarrator.describe(&issue, None),
"UNT segment count mismatch: declared 30, actual 31"
);
}
#[test]
fn field_condition_not_satisfied_matches_the_legacy_message() {
let issue = ValidationIssue::new(
Severity::Error,
IssueKind::FieldConditionNotSatisfied {
field_name: "Merkmal, Code".into(),
},
)
.with_field_path("SG4/SG8/SG10/CCI/C240/7037")
.with_rule("Muss [182]");
let loc = EdifactView.location(&issue);
assert_eq!(
TechnicalNarrator.describe(&issue, loc.as_deref()),
"Field 'Merkmal, Code' at SG4/SG8/SG10/CCI/C240/7037 is present but does not satisfy condition: Muss [182]"
);
}
#[test]
fn condition_unknown_joins_multiple_id_groups_with_semicolons() {
let issue = ValidationIssue::new(
Severity::Info,
IssueKind::ConditionUnknown {
field_name: "Merkmal, Code".into(),
unresolved: UnresolvedConditions {
external: vec![182],
undetermined: vec![6],
missing: vec![570],
},
},
)
.with_field_path("SG4/SG8/SG10/CCI/C240/7037");
let loc = EdifactView.location(&issue);
assert_eq!(
TechnicalNarrator.describe(&issue, loc.as_deref()),
"Condition for field 'Merkmal, Code' could not be fully evaluated (external conditions require provider: [182]; conditions could not be determined from message data: [6]; missing conditions: [570])"
);
}
#[test]
fn package_cardinality_matches_the_legacy_message() {
let issue = ValidationIssue::new(
Severity::Error,
IssueKind::PackageCardinality {
package_id: "1".into(),
present: 2,
min: 0,
max: 1,
codes: vec!["A".into(), "B".into()],
},
)
.with_field_path("SG4/SG8/SG10");
let loc = EdifactView.location(&issue);
assert_eq!(
TechnicalNarrator.describe(&issue, loc.as_deref()),
"Package [1P0..1] at SG4/SG8/SG10: 2 code(s) present (allowed 0..1). Codes in package: [A, B]"
);
}
#[test]
fn code_not_allowed_for_pid_matches_the_legacy_message() {
let issue = ValidationIssue::new(
Severity::Warning,
IssueKind::CodeNotAllowedForPid {
value: "Z99".into(),
allowed: vec!["Z01".into(), "Z02".into(), "Z98".into()],
},
);
assert_eq!(
TechnicalNarrator.describe(&issue, None),
"Code 'Z99' is not allowed for this PID. Allowed: [Z01, Z02, Z98]"
);
}
#[test]
fn unt_count_not_verifiable_matches_the_legacy_message() {
let issue = ValidationIssue::new(
Severity::Warning,
IssueKind::UntCountNotVerifiable { unh_count: 3 },
);
assert_eq!(
TechnicalNarrator.describe(&issue, None),
"UNT validation requires per-message segments, found 3 UNH segments"
);
}
#[test]
fn structure_diagnostic_passes_its_detail_through_verbatim() {
let detail = "Segment 'FTX' at position 15 is not defined in the PID-filtered MIG; \
the assembler advanced past it";
let issue = ValidationIssue::new(
Severity::Warning,
IssueKind::StructureDiagnostic {
kind: StructureDiagnosticKind::UnexpectedSegment,
segment_id: "FTX".into(),
position: 15,
detail: detail.into(),
},
);
assert_eq!(TechnicalNarrator.describe(&issue, None), detail);
}
}