use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Severity {
Info,
Warning,
Error,
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Severity::Info => write!(f, "INFO"),
Severity::Warning => write!(f, "WARN"),
Severity::Error => write!(f, "ERROR"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ValidationCategory {
Structure,
Format,
Code,
Ahb,
}
impl std::fmt::Display for ValidationCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ValidationCategory::Structure => write!(f, "Structure"),
ValidationCategory::Format => write!(f, "Format"),
ValidationCategory::Code => write!(f, "Code"),
ValidationCategory::Ahb => write!(f, "AHB"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SegmentPosition {
pub segment_number: u32,
pub byte_offset: usize,
pub message_number: u32,
}
impl From<edifact_primitives::SegmentPosition> for SegmentPosition {
fn from(pos: edifact_primitives::SegmentPosition) -> Self {
Self {
segment_number: pos.segment_number,
byte_offset: pos.byte_offset,
message_number: pos.message_number,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationIssue {
pub severity: Severity,
pub category: ValidationCategory,
pub code: String,
pub message: String,
pub segment_position: Option<SegmentPosition>,
pub field_path: Option<String>,
pub rule: Option<String>,
pub actual_value: Option<String>,
pub expected_value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bo4e_path: Option<String>,
}
impl ValidationIssue {
pub fn new(
severity: Severity,
category: ValidationCategory,
code: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
severity,
category,
code: code.into(),
message: message.into(),
segment_position: None,
field_path: None,
rule: None,
actual_value: None,
expected_value: None,
bo4e_path: None,
}
}
pub fn with_position(mut self, position: impl Into<SegmentPosition>) -> Self {
self.segment_position = Some(position.into());
self
}
pub fn with_field_path(mut self, path: impl Into<String>) -> Self {
self.field_path = Some(path.into());
self
}
pub fn with_rule(mut self, rule: impl Into<String>) -> Self {
self.rule = Some(rule.into());
self
}
pub fn with_actual(mut self, value: impl Into<String>) -> Self {
self.actual_value = Some(value.into());
self
}
pub fn with_expected(mut self, value: impl Into<String>) -> Self {
self.expected_value = Some(value.into());
self
}
pub fn with_bo4e_path(mut self, path: impl Into<String>) -> Self {
self.bo4e_path = Some(path.into());
self
}
pub fn is_error(&self) -> bool {
self.severity == Severity::Error
}
pub fn is_warning(&self) -> bool {
self.severity == Severity::Warning
}
}
impl std::fmt::Display for ValidationIssue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}: {}", self.severity, self.code, self.message)?;
if let Some(ref path) = self.field_path {
write!(f, " at {path}")?;
}
if let Some(ref pos) = self.segment_position {
write!(
f,
" (segment #{}, byte {})",
pos.segment_number, pos.byte_offset
)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_severity_ordering() {
assert!(Severity::Info < Severity::Warning);
assert!(Severity::Warning < Severity::Error);
}
#[test]
fn test_issue_builder() {
let issue = ValidationIssue::new(
Severity::Error,
ValidationCategory::Ahb,
"AHB001",
"Required field missing",
)
.with_field_path("SG2/NAD/C082/3039")
.with_rule("Muss [182] ∧ [152]")
.with_position(SegmentPosition {
segment_number: 5,
byte_offset: 234,
message_number: 1,
});
assert!(issue.is_error());
assert!(!issue.is_warning());
assert_eq!(issue.code, "AHB001");
assert_eq!(issue.field_path.as_deref(), Some("SG2/NAD/C082/3039"));
assert_eq!(issue.rule.as_deref(), Some("Muss [182] ∧ [152]"));
assert_eq!(issue.segment_position.unwrap().segment_number, 5);
}
#[test]
fn test_issue_display() {
let issue = ValidationIssue::new(
Severity::Error,
ValidationCategory::Ahb,
"AHB001",
"Required field missing",
)
.with_field_path("NAD");
let display = format!("{issue}");
assert!(display.contains("[ERROR]"));
assert!(display.contains("AHB001"));
assert!(display.contains("Required field missing"));
assert!(display.contains("at NAD"));
}
#[test]
fn test_issue_serialization() {
let issue = ValidationIssue::new(
Severity::Warning,
ValidationCategory::Code,
"COD002",
"Code not allowed for PID",
);
let json = serde_json::to_string_pretty(&issue).unwrap();
assert!(!json.contains("bo4e_path"));
let deserialized: ValidationIssue = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.code, "COD002");
assert_eq!(deserialized.severity, Severity::Warning);
assert!(deserialized.bo4e_path.is_none());
}
#[test]
fn test_bo4e_path_builder_and_serialization() {
let issue = ValidationIssue::new(
Severity::Error,
ValidationCategory::Ahb,
"AHB001",
"Required field missing",
)
.with_field_path("SG4/SG5/LOC/C517/3225")
.with_bo4e_path("stammdaten.Marktlokation.marktlokationsId");
assert_eq!(
issue.bo4e_path.as_deref(),
Some("stammdaten.Marktlokation.marktlokationsId")
);
let json = serde_json::to_string_pretty(&issue).unwrap();
assert!(json.contains("bo4e_path"));
assert!(json.contains("stammdaten.Marktlokation.marktlokationsId"));
let deserialized: ValidationIssue = serde_json::from_str(&json).unwrap();
assert_eq!(
deserialized.bo4e_path.as_deref(),
Some("stammdaten.Marktlokation.marktlokationsId")
);
}
#[test]
fn test_category_display() {
assert_eq!(format!("{}", ValidationCategory::Structure), "Structure");
assert_eq!(format!("{}", ValidationCategory::Ahb), "AHB");
}
#[test]
fn test_position_from_edifact_primitives() {
let edifact_pos = edifact_primitives::SegmentPosition::new(3, 100, 1);
let pos: SegmentPosition = edifact_pos.into();
assert_eq!(pos.segment_number, 3);
assert_eq!(pos.byte_offset, 100);
assert_eq!(pos.message_number, 1);
}
}