mod format_impl;
mod narrator;
mod views;
pub use narrator::TechnicalNarrator;
pub use views::{Bo4eView, EdifactView};
pub mod format {
pub use super::format_impl::{lines, table};
}
use crate::{SegmentPosition, Severity, ValidationCategory, ValidationIssue};
pub trait IssueView {
fn name(&self) -> &str;
fn location(&self, issue: &ValidationIssue) -> Option<String>;
}
pub trait IssueNarrator {
fn describe(&self, issue: &ValidationIssue, location: Option<&str>) -> String;
}
#[derive(Debug, Clone)]
pub struct IssueDisplay<'a> {
pub severity: Severity,
pub category: ValidationCategory,
pub code: &'static str,
pub text: String,
pub location: Option<String>,
pub segment_position: Option<SegmentPosition>,
pub rule: Option<&'a str>,
pub actual_value: Option<&'a str>,
pub expected_value: Option<&'a str>,
}
impl<'a> IssueDisplay<'a> {
pub fn new(
issue: &'a ValidationIssue,
view: &dyn IssueView,
narrator: &dyn IssueNarrator,
) -> Self {
let location = view.location(issue);
Self {
severity: issue.severity,
category: issue.category(),
code: issue.code(),
text: narrator.describe(issue, location.as_deref()),
location,
segment_position: issue.segment_position,
rule: issue.rule.as_deref(),
actual_value: issue.actual_value.as_deref(),
expected_value: issue.expected_value.as_deref(),
}
}
}
pub fn report_to_json(
report: &crate::ValidationReport,
view: &dyn IssueView,
narrator: &dyn IssueNarrator,
) -> serde_json::Value {
let mut value = serde_json::to_value(report).unwrap_or(serde_json::Value::Null);
if let Some(issues) = value.get_mut("issues").and_then(|i| i.as_array_mut()) {
for (json, issue) in issues.iter_mut().zip(report.issues.iter()) {
if let Some(obj) = json.as_object_mut() {
let location = view.location(issue);
obj.insert(
"message".into(),
serde_json::Value::String(narrator.describe(issue, location.as_deref())),
);
obj.insert(
"code".into(),
serde_json::Value::String(issue.code().into()),
);
obj.insert(
"category".into(),
serde_json::to_value(issue.category()).unwrap_or(serde_json::Value::Null),
);
}
}
}
value
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{IssueKind, ValidationLevel, ValidationReport};
#[test]
fn report_to_json_carries_message_code_and_category_for_every_issue() {
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::CodeNotAllowedForPid {
value: "Z99".into(),
allowed: vec!["Z01".into(), "Z02".into()],
},
));
let json = report_to_json(&report, &EdifactView, &TechnicalNarrator);
let issues = json["issues"].as_array().expect("issues array");
assert_eq!(issues.len(), 2);
for issue in issues {
assert!(issue["message"].is_string(), "{issue}");
assert!(issue["code"].is_string(), "{issue}");
assert!(issue["category"].is_string(), "{issue}");
assert!(issue["kind"].is_object(), "{issue}");
}
let with_path = &issues[0];
assert_eq!(with_path["field_path"], "SG4/SG5/LOC/C517/3225");
assert_eq!(with_path["severity"], "Error");
assert_eq!(with_path["category"], "Ahb");
assert_eq!(with_path["code"], "AHB001");
assert_eq!(
with_path["message"],
"Required field 'LOC+Z16' at SG4/SG5/LOC/C517/3225 is missing"
);
assert_eq!(with_path["kind"]["type"], "missingRequiredField");
let without_path = &issues[1];
assert_eq!(without_path["field_path"], serde_json::Value::Null);
assert!(without_path["message"].is_string());
assert!(without_path["code"].is_string());
assert!(without_path["category"].is_string());
assert_eq!(without_path["severity"], "Warning");
assert_eq!(without_path["category"], "Code");
assert_eq!(without_path["code"], "COD002");
assert_eq!(without_path["kind"]["type"], "codeNotAllowedForPid");
}
}