use crate::error::Insignificant;
use crate::validator::{ValidationRuleContext, Validator, report_error};
use crate::{EdifactError, Segment, ValidationIssue, ValidationReport, ValidationSeverity};
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Status {
Mandatory,
Conditional,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ReprKind {
Alphabetic,
Numeric,
Alphanumeric,
}
impl ReprKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Alphabetic => "a",
Self::Numeric => "n",
Self::Alphanumeric => "an",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Repr {
kind: ReprKind,
max: u16,
fixed: bool,
}
impl Repr {
#[must_use]
pub const fn an(max: u16) -> Self {
Self {
kind: ReprKind::Alphanumeric,
max,
fixed: true,
}
}
#[must_use]
pub const fn an_up_to(max: u16) -> Self {
Self {
kind: ReprKind::Alphanumeric,
max,
fixed: false,
}
}
#[must_use]
pub const fn a(max: u16) -> Self {
Self {
kind: ReprKind::Alphabetic,
max,
fixed: true,
}
}
#[must_use]
pub const fn a_up_to(max: u16) -> Self {
Self {
kind: ReprKind::Alphabetic,
max,
fixed: false,
}
}
#[must_use]
pub const fn n(max: u16) -> Self {
Self {
kind: ReprKind::Numeric,
max,
fixed: true,
}
}
#[must_use]
pub const fn n_up_to(max: u16) -> Self {
Self {
kind: ReprKind::Numeric,
max,
fixed: false,
}
}
#[must_use]
pub const fn kind(self) -> ReprKind {
self.kind
}
#[must_use]
pub const fn max_length(self) -> u16 {
self.max
}
#[must_use]
pub const fn min_length(self) -> u16 {
if self.fixed { self.max } else { 1 }
}
#[must_use]
pub const fn is_fixed(self) -> bool {
self.fixed
}
#[must_use]
pub fn measure(self, value: &str) -> usize {
match self.kind {
ReprKind::Numeric => {
let mantissa = value
.split_once(['E', 'e'])
.map_or(value, |(mantissa, _exponent)| mantissa);
mantissa
.chars()
.filter(|c| !matches!(c, '-' | '.' | ','))
.count()
}
_ => value.chars().count(),
}
}
#[must_use]
pub fn permits_characters(self, value: &str) -> bool {
match self.kind {
ReprKind::Alphanumeric => true,
ReprKind::Alphabetic => !value.chars().any(|c| c.is_ascii_digit()),
ReprKind::Numeric => is_iso6093_numeric(value),
}
}
}
impl std::fmt::Display for Repr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.fixed {
write!(f, "{}{}", self.kind.as_str(), self.max)
} else {
write!(f, "{}..{}", self.kind.as_str(), self.max)
}
}
}
fn is_iso6093_numeric(value: &str) -> bool {
let (mantissa, exponent) = match value.split_once(['E', 'e']) {
Some((mantissa, exponent)) => (mantissa, Some(exponent)),
None => (value, None),
};
if let Some(exponent) = exponent {
let digits = exponent.strip_prefix('-').unwrap_or(exponent);
if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
return false;
}
}
let digits = mantissa.strip_prefix('-').unwrap_or(mantissa);
if digits.is_empty() {
return false;
}
match digits.split_once(['.', ',']) {
Some((integer, fraction)) => {
!fraction.is_empty()
&& fraction.chars().all(|c| c.is_ascii_digit())
&& integer.chars().all(|c| c.is_ascii_digit())
}
None => digits.chars().all(|c| c.is_ascii_digit()),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ReprRequirement {
primary: Repr,
alternative: Option<Repr>,
}
impl ReprRequirement {
const fn single(repr: Repr) -> Self {
Self {
primary: repr,
alternative: None,
}
}
fn permits_characters(&self, value: &str) -> bool {
self.primary.permits_characters(value)
|| self
.alternative
.is_some_and(|repr| repr.permits_characters(value))
}
fn permits_length(&self, value: &str) -> bool {
let fits = |repr: Repr| {
let length = repr.measure(value);
length >= usize::from(repr.min_length()) && length <= usize::from(repr.max_length())
};
fits(self.primary) || self.alternative.is_some_and(fits)
}
fn is_too_short(&self, value: &str) -> bool {
let short = |repr: Repr| repr.measure(value) < usize::from(repr.min_length());
short(self.primary) && self.alternative.is_none_or(short)
}
fn measure(&self, value: &str) -> usize {
self.primary.measure(value)
}
}
impl std::fmt::Display for ReprRequirement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.alternative {
Some(alternative) => write!(f, "{} or {alternative}", self.primary),
None => write!(f, "{}", self.primary),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ComponentRef {
position: u8,
data_element: &'static str,
status: Status,
repeat_count: u8,
repr: Option<Repr>,
repr_from_v4: Option<Repr>,
}
impl ComponentRef {
#[must_use]
pub const fn new(position: u8, data_element: &'static str, status: Status) -> Self {
assert!(
position != 0,
"ComponentRef position must be >= 1 (one-based)"
);
Self {
position,
data_element,
status,
repeat_count: 1,
repr: None,
repr_from_v4: None,
}
}
#[must_use]
pub const fn repeated(
position: u8,
data_element: &'static str,
status: Status,
repeat_count: u8,
) -> Self {
assert!(
position != 0,
"ComponentRef position must be >= 1 (one-based)"
);
assert!(
repeat_count != 0,
"ComponentRef repeat_count must be >= 1; use `new` for a component that does not repeat"
);
Self {
position,
data_element,
status,
repeat_count,
repr: None,
repr_from_v4: None,
}
}
#[must_use]
#[inline]
pub const fn repeat_count(&self) -> u8 {
self.repeat_count
}
#[must_use]
#[inline]
pub const fn position(&self) -> u8 {
self.position
}
#[must_use]
#[inline]
pub const fn data_element(&self) -> &'static str {
self.data_element
}
#[must_use]
#[inline]
pub const fn status(&self) -> Status {
self.status
}
#[must_use]
pub const fn with_repr(mut self, repr: Repr) -> Self {
self.repr = Some(repr);
self
}
#[must_use]
#[inline]
pub const fn repr(&self) -> Option<Repr> {
self.repr
}
#[must_use]
pub const fn with_repr_by_syntax_version(mut self, up_to_v3: Repr, from_v4: Repr) -> Self {
self.repr = Some(up_to_v3);
self.repr_from_v4 = Some(from_v4);
self
}
#[must_use]
#[inline]
pub const fn repr_from_v4(&self) -> Option<Repr> {
self.repr_from_v4
}
}
#[derive(Debug, Clone, Copy)]
pub struct ElementRef {
position: u8,
data_element: &'static str,
status: Status,
max_repeat: u8,
components: &'static [ComponentRef],
repr: Option<Repr>,
repr_from_v4: Option<Repr>,
}
impl ElementRef {
#[must_use]
pub const fn new(
position: u8,
data_element: &'static str,
status: Status,
max_repeat: u8,
) -> Self {
assert!(
position != 0,
"ElementRef position must be >= 1 (one-based)"
);
Self {
position,
data_element,
status,
max_repeat,
components: &[],
repr: None,
repr_from_v4: None,
}
}
#[must_use]
pub const fn composite(
position: u8,
data_element: &'static str,
status: Status,
max_repeat: u8,
components: &'static [ComponentRef],
) -> Self {
assert!(
position != 0,
"ElementRef position must be >= 1 (one-based)"
);
Self {
position,
data_element,
status,
max_repeat,
components,
repr: None,
repr_from_v4: None,
}
}
#[must_use]
#[inline]
pub const fn position(&self) -> u8 {
self.position
}
#[must_use]
#[inline]
pub const fn data_element(&self) -> &'static str {
self.data_element
}
#[must_use]
#[inline]
pub const fn status(&self) -> Status {
self.status
}
#[must_use]
#[inline]
pub const fn max_repeat(&self) -> u8 {
self.max_repeat
}
#[must_use]
#[inline]
pub const fn components(&self) -> &'static [ComponentRef] {
self.components
}
#[must_use]
pub const fn with_repr(mut self, repr: Repr) -> Self {
self.repr = Some(repr);
self
}
#[must_use]
#[inline]
pub const fn repr(&self) -> Option<Repr> {
self.repr
}
#[must_use]
pub const fn with_repr_by_syntax_version(mut self, up_to_v3: Repr, from_v4: Repr) -> Self {
self.repr = Some(up_to_v3);
self.repr_from_v4 = Some(from_v4);
self
}
#[must_use]
#[inline]
pub const fn repr_from_v4(&self) -> Option<Repr> {
self.repr_from_v4
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct SegmentDefinition {
pub tag: &'static str,
pub name: &'static str,
pub elements: &'static [ElementRef],
}
const fn const_str_eq(a: &str, b: &str) -> bool {
let (a, b) = (a.as_bytes(), b.as_bytes());
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ElementPath {
pub element: usize,
pub component: Option<usize>,
}
impl ElementPath {
#[must_use]
#[inline]
pub const fn element(element: usize) -> Self {
Self {
element,
component: None,
}
}
#[must_use]
#[inline]
pub const fn component(element: usize, component: usize) -> Self {
Self {
element,
component: Some(component),
}
}
#[must_use]
#[inline]
pub const fn component_index(&self) -> usize {
match self.component {
Some(c) => c,
None => 0,
}
}
}
pub trait SegmentLayout {
fn layout_tag(&self) -> &str;
fn resolve_code(&self, data_element: &str) -> Result<ElementPath, EdifactError>;
fn slots(&self) -> Vec<LayoutSlot>;
fn audit(&self, segments: &[crate::Segment<'_>]) -> LayoutAudit {
audit_layout(self.layout_tag(), &self.slots(), segments)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LayoutSlot {
pub element: usize,
pub component: Option<usize>,
pub data_element: String,
pub status: Status,
pub element_status: Status,
}
impl LayoutSlot {
#[must_use]
pub fn component_index(&self) -> usize {
self.component.unwrap_or(0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum LayoutFinding {
UndeclaredElement {
element: usize,
span: crate::Span,
},
UndeclaredComponent {
element: usize,
component: usize,
span: crate::Span,
},
MandatoryNeverPopulated {
slot: LayoutSlot,
},
NeverObserved {
slot: LayoutSlot,
},
}
#[derive(Debug, Clone, Default)]
pub struct LayoutAudit {
tag: String,
segments_examined: usize,
findings: Vec<LayoutFinding>,
}
impl LayoutAudit {
#[must_use]
pub fn tag(&self) -> &str {
&self.tag
}
#[must_use]
pub fn segments_examined(&self) -> usize {
self.segments_examined
}
#[must_use]
pub fn findings(&self) -> &[LayoutFinding] {
&self.findings
}
pub fn contradictions(&self) -> impl Iterator<Item = &LayoutFinding> {
self.findings
.iter()
.filter(|f| !matches!(f, LayoutFinding::NeverObserved { .. }))
}
#[must_use]
pub fn has_contradictions(&self) -> bool {
self.contradictions().next().is_some()
}
pub fn unconfirmed(&self) -> impl Iterator<Item = &LayoutSlot> {
self.findings.iter().filter_map(|f| match f {
LayoutFinding::NeverObserved { slot } => Some(slot),
_ => None,
})
}
}
impl std::fmt::Display for LayoutAudit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(
f,
"{}: {} segment(s) examined, {} contradiction(s), {} unconfirmed slot(s)",
self.tag,
self.segments_examined,
self.contradictions().count(),
self.unconfirmed().count(),
)?;
for finding in &self.findings {
match finding {
LayoutFinding::UndeclaredElement { element, span } => writeln!(
f,
" element {element} is populated at bytes {span} but the layout declares no such element",
)?,
LayoutFinding::UndeclaredComponent {
element,
component,
span,
} => writeln!(
f,
" element {element} component {component} is populated at bytes {span} but the layout declares no such component",
)?,
LayoutFinding::MandatoryNeverPopulated { slot } => writeln!(
f,
" {} is declared mandatory but is empty in every segment",
describe_slot(slot),
)?,
LayoutFinding::NeverObserved { slot } => writeln!(
f,
" {} was never populated — this corpus cannot confirm it",
describe_slot(slot),
)?,
}
}
Ok(())
}
}
fn describe_slot(slot: &LayoutSlot) -> String {
match slot.component {
Some(component) => format!(
"DE {} (element {}, component {component})",
slot.data_element, slot.element
),
None => format!("DE {} (element {})", slot.data_element, slot.element),
}
}
pub fn audit_directory<'a, L, F>(lookup: F, segments: &[crate::Segment<'_>]) -> Vec<LayoutAudit>
where
L: SegmentLayout + ?Sized + 'a,
F: Fn(&str) -> Option<&'a L>,
{
let mut seen: Vec<&str> = Vec::new();
for segment in segments {
if !seen.contains(&segment.tag) {
seen.push(segment.tag);
}
}
seen.into_iter()
.filter_map(|tag| lookup(tag).map(|layout| layout.audit(segments)))
.collect()
}
fn audit_layout(tag: &str, slots: &[LayoutSlot], segments: &[crate::Segment<'_>]) -> LayoutAudit {
let mut audit = LayoutAudit {
tag: tag.to_owned(),
segments_examined: 0,
findings: Vec::new(),
};
let declared_elements = slots.iter().map(|s| s.element + 1).max().unwrap_or(0);
let mut declared_components: Vec<usize> = vec![0; declared_elements];
for slot in slots {
let width = slot.component_index() + 1;
if width > declared_components[slot.element] {
declared_components[slot.element] = width;
}
}
let mut populated: Vec<Vec<bool>> = declared_components
.iter()
.map(|width| vec![false; *width])
.collect();
for segment in segments.iter().filter(|s| s.tag == tag) {
audit.segments_examined += 1;
for (element_index, element) in segment.elements.iter().enumerate() {
for occurrence in element.repetitions() {
for (component_index, (value, _)) in occurrence.iter().enumerate() {
if value.is_empty() {
continue;
}
if element_index >= declared_elements {
push_once(
&mut audit.findings,
LayoutFinding::UndeclaredElement {
element: element_index,
span: segment.span,
},
);
continue;
}
if component_index >= declared_components[element_index] {
push_once(
&mut audit.findings,
LayoutFinding::UndeclaredComponent {
element: element_index,
component: component_index,
span: segment.span,
},
);
continue;
}
populated[element_index][component_index] = true;
}
}
}
}
let element_populated: Vec<bool> = populated
.iter()
.map(|components| components.iter().any(|seen| *seen))
.collect();
for slot in slots {
if populated[slot.element][slot.component_index()] {
continue;
}
let required_here = slot.status == Status::Mandatory
&& (slot.component.is_none()
|| slot.element_status == Status::Mandatory
|| element_populated[slot.element]);
audit.findings.push(if required_here {
LayoutFinding::MandatoryNeverPopulated { slot: slot.clone() }
} else {
LayoutFinding::NeverObserved { slot: slot.clone() }
});
}
audit
}
fn push_once(findings: &mut Vec<LayoutFinding>, finding: LayoutFinding) {
let duplicate = findings.iter().any(|existing| match (existing, &finding) {
(
LayoutFinding::UndeclaredElement { element: a, .. },
LayoutFinding::UndeclaredElement { element: b, .. },
) => a == b,
(
LayoutFinding::UndeclaredComponent {
element: a,
component: c,
..
},
LayoutFinding::UndeclaredComponent {
element: b,
component: d,
..
},
) => a == b && c == d,
_ => false,
});
if !duplicate {
findings.push(finding);
}
}
impl SegmentDefinition {
#[must_use]
pub const fn new(
tag: &'static str,
name: &'static str,
elements: &'static [ElementRef],
) -> Self {
Self {
tag,
name,
elements,
}
}
#[must_use]
pub const fn code_positions(&self, data_element: &str) -> usize {
let mut hits = 0;
let mut i = 0;
while i < self.elements.len() {
let el = &self.elements[i];
if const_str_eq(el.data_element, data_element) {
hits += 1;
}
let mut c = 0;
while c < el.components.len() {
if const_str_eq(el.components[c].data_element, data_element) {
hits += 1;
}
c += 1;
}
i += 1;
}
hits
}
#[must_use]
pub const fn element_slot(&self, data_element: &str) -> usize {
assert!(
self.code_positions(data_element) != 0,
"this segment definition declares no such data element identifier — check it against the directory"
);
assert!(
self.code_positions(data_element) == 1,
"this data element identifier is declared at more than one position; address it positionally, or declare the repeat with ComponentRef::repeated"
);
let mut i = 0;
while i < self.elements.len() {
let el = &self.elements[i];
if const_str_eq(el.data_element, data_element) {
return el.position as usize - 1;
}
let mut c = 0;
while c < el.components.len() {
if const_str_eq(el.components[c].data_element, data_element) {
return el.position as usize - 1;
}
c += 1;
}
i += 1;
}
unreachable!()
}
#[must_use]
pub const fn component_slot(&self, data_element: &str) -> usize {
assert!(
self.code_positions(data_element) != 0,
"this segment definition declares no such data element identifier — check it against the directory"
);
assert!(
self.code_positions(data_element) == 1,
"this data element identifier is declared at more than one position; address it positionally, or declare the repeat with ComponentRef::repeated"
);
let mut i = 0;
while i < self.elements.len() {
let el = &self.elements[i];
if const_str_eq(el.data_element, data_element) {
return 0;
}
let mut c = 0;
while c < el.components.len() {
if const_str_eq(el.components[c].data_element, data_element) {
return el.components[c].position as usize - 1;
}
c += 1;
}
i += 1;
}
unreachable!()
}
#[must_use]
pub const fn code_is_component(&self, data_element: &str) -> bool {
let mut i = 0;
while i < self.elements.len() {
let el = &self.elements[i];
let mut c = 0;
while c < el.components.len() {
if const_str_eq(el.components[c].data_element, data_element) {
return true;
}
c += 1;
}
i += 1;
}
false
}
}
impl SegmentLayout for SegmentDefinition {
#[inline]
fn layout_tag(&self) -> &str {
self.tag
}
fn resolve_code(&self, data_element: &str) -> Result<ElementPath, EdifactError> {
let mut hits = 0usize;
let mut found = None;
for el in self.elements {
if el.data_element == data_element {
hits += 1;
found.get_or_insert(ElementPath::element(el.position as usize - 1));
}
for comp in el.components {
if comp.data_element == data_element {
hits += 1;
found.get_or_insert(ElementPath::component(
el.position as usize - 1,
comp.position as usize - 1,
));
}
}
}
resolve_outcome(self.tag, data_element, hits, found)
}
fn slots(&self) -> Vec<LayoutSlot> {
let mut out = Vec::new();
for element in self.elements {
if element.components.is_empty() {
out.push(LayoutSlot {
element: element.position as usize - 1,
component: None,
data_element: element.data_element.to_owned(),
status: element.status,
element_status: element.status,
});
continue;
}
for component in element.components {
out.push(LayoutSlot {
element: element.position as usize - 1,
component: Some(component.position as usize - 1),
data_element: component.data_element.to_owned(),
status: component.status,
element_status: element.status,
});
}
}
out
}
}
fn resolve_outcome(
tag: &str,
data_element: &str,
hits: usize,
found: Option<ElementPath>,
) -> Result<ElementPath, EdifactError> {
match (hits, found) {
(1, Some(path)) => Ok(path),
(0, _) => Err(EdifactError::UnknownDataElement {
tag: tag.to_owned(),
data_element: data_element.to_owned(),
}),
_ => Err(EdifactError::AmbiguousDataElement {
tag: tag.to_owned(),
data_element: data_element.to_owned(),
}),
}
}
#[derive(Debug, Clone)]
pub struct OwnedElementRef {
position: u8,
data_element: String,
status: Status,
max_repeat: u8,
repr: Option<Repr>,
repr_from_v4: Option<Repr>,
components: Vec<OwnedComponentRef>,
}
#[derive(Debug, Clone)]
pub struct OwnedComponentRef {
position: u8,
data_element: String,
status: Status,
repr: Option<Repr>,
repr_from_v4: Option<Repr>,
repeat_count: u8,
}
impl OwnedComponentRef {
pub fn new_unchecked(position: u8, data_element: String, status: Status) -> Self {
assert!(
position != 0,
"OwnedComponentRef::new_unchecked: position must be >= 1 (one-based), got 0"
);
Self {
position,
data_element,
status,
repeat_count: 1,
repr: None,
repr_from_v4: None,
}
}
#[must_use]
pub fn repeated(position: u8, data_element: String, status: Status, repeat_count: u8) -> Self {
assert!(
position != 0,
"OwnedComponentRef::repeated: position must be >= 1 (one-based), got 0"
);
assert!(
repeat_count != 0,
"OwnedComponentRef::repeated: repeat_count must be >= 1"
);
Self {
position,
data_element,
status,
repeat_count,
repr: None,
repr_from_v4: None,
}
}
#[inline]
#[must_use]
pub fn repeat_count(&self) -> u8 {
self.repeat_count
}
pub fn try_new(
position: u8,
data_element: String,
status: Status,
) -> Result<Self, EdifactError> {
if position == 0 {
return Err(EdifactError::InvalidElementPosition);
}
Ok(Self {
position,
data_element,
status,
repeat_count: 1,
repr: None,
repr_from_v4: None,
})
}
#[inline]
pub fn position(&self) -> u8 {
self.position
}
#[inline]
pub fn data_element(&self) -> &str {
&self.data_element
}
#[inline]
pub fn status(&self) -> Status {
self.status
}
#[must_use]
pub fn with_repr(mut self, repr: Repr) -> Self {
self.repr = Some(repr);
self
}
#[inline]
#[must_use]
pub fn repr(&self) -> Option<Repr> {
self.repr
}
#[must_use]
pub fn with_repr_by_syntax_version(mut self, up_to_v3: Repr, from_v4: Repr) -> Self {
self.repr = Some(up_to_v3);
self.repr_from_v4 = Some(from_v4);
self
}
#[inline]
#[must_use]
pub fn repr_from_v4(&self) -> Option<Repr> {
self.repr_from_v4
}
}
#[derive(Debug, Clone)]
pub struct OwnedSegmentDef {
tag: String,
name: String,
elements: Vec<OwnedElementRef>,
}
impl OwnedSegmentDef {
pub fn new_unchecked(tag: String, name: String, elements: Vec<OwnedElementRef>) -> Self {
assert!(
tag.len() == 3 && tag.bytes().all(|b| b.is_ascii_uppercase()),
"OwnedSegmentDef::new_unchecked: tag must be exactly three ASCII uppercase letters, got {tag:?}"
);
Self {
tag,
name,
elements,
}
}
pub fn try_new(
tag: String,
name: String,
elements: Vec<OwnedElementRef>,
) -> Result<Self, EdifactError> {
if tag.len() != 3 || !tag.bytes().all(|b| b.is_ascii_uppercase()) {
return Err(EdifactError::InvalidSegmentTag(tag));
}
Ok(Self {
tag,
name,
elements,
})
}
#[inline]
pub fn tag(&self) -> &str {
&self.tag
}
#[inline]
pub fn name(&self) -> &str {
&self.name
}
#[inline]
pub fn elements(&self) -> &[OwnedElementRef] {
&self.elements
}
#[must_use]
pub fn code_positions(&self, data_element: &str) -> usize {
self.elements
.iter()
.map(|el| {
usize::from(el.data_element == data_element)
+ el.components
.iter()
.filter(|c| c.data_element == data_element)
.count()
})
.sum()
}
}
impl SegmentLayout for OwnedSegmentDef {
#[inline]
fn layout_tag(&self) -> &str {
&self.tag
}
fn resolve_code(&self, data_element: &str) -> Result<ElementPath, EdifactError> {
let mut hits = 0usize;
let mut found = None;
for el in &self.elements {
if el.data_element == data_element {
hits += 1;
found.get_or_insert(ElementPath::element(el.position as usize - 1));
}
for comp in &el.components {
if comp.data_element == data_element {
hits += 1;
found.get_or_insert(ElementPath::component(
el.position as usize - 1,
comp.position as usize - 1,
));
}
}
}
resolve_outcome(&self.tag, data_element, hits, found)
}
fn slots(&self) -> Vec<LayoutSlot> {
let mut out = Vec::new();
for element in &self.elements {
if element.components.is_empty() {
out.push(LayoutSlot {
element: element.position as usize - 1,
component: None,
data_element: element.data_element.clone(),
status: element.status,
element_status: element.status,
});
continue;
}
for component in &element.components {
out.push(LayoutSlot {
element: element.position as usize - 1,
component: Some(component.position as usize - 1),
data_element: component.data_element.clone(),
status: component.status,
element_status: element.status,
});
}
}
out
}
}
impl OwnedElementRef {
pub fn new_unchecked(
position: u8,
data_element: String,
status: Status,
max_repeat: u8,
) -> Self {
assert!(
position != 0,
"OwnedElementRef::new_unchecked: position must be >= 1 (one-based), got 0"
);
Self {
position,
data_element,
status,
max_repeat,
repr: None,
repr_from_v4: None,
components: Vec::new(),
}
}
pub fn try_new(
position: u8,
data_element: String,
status: Status,
max_repeat: u8,
) -> Result<Self, EdifactError> {
if position == 0 {
return Err(EdifactError::InvalidElementPosition);
}
Ok(Self {
position,
data_element,
status,
max_repeat,
repr: None,
repr_from_v4: None,
components: Vec::new(),
})
}
#[must_use]
pub fn with_components(mut self, components: Vec<OwnedComponentRef>) -> Self {
self.components = components;
self
}
#[inline]
pub fn components(&self) -> &[OwnedComponentRef] {
&self.components
}
#[inline]
pub fn position(&self) -> u8 {
self.position
}
#[inline]
pub fn data_element(&self) -> &str {
&self.data_element
}
#[inline]
pub fn status(&self) -> Status {
self.status
}
#[inline]
pub fn max_repeat(&self) -> u8 {
self.max_repeat
}
#[must_use]
pub fn with_repr(mut self, repr: Repr) -> Self {
self.repr = Some(repr);
self
}
#[inline]
#[must_use]
pub fn repr(&self) -> Option<Repr> {
self.repr
}
#[must_use]
pub fn with_repr_by_syntax_version(mut self, up_to_v3: Repr, from_v4: Repr) -> Self {
self.repr = Some(up_to_v3);
self.repr_from_v4 = Some(from_v4);
self
}
#[inline]
#[must_use]
pub fn repr_from_v4(&self) -> Option<Repr> {
self.repr_from_v4
}
}
type SegmentLookupFn = Arc<dyn Fn(&str) -> Option<&'static SegmentDefinition> + Send + Sync>;
type IsCodeValidFn = Arc<dyn Fn(&str, &str) -> bool + Send + Sync>;
type SuggestCodeFn = Arc<dyn Fn(&str, &str) -> Option<&'static str> + Send + Sync>;
type ExpectedComponentsFn = Arc<dyn Fn(&str, usize) -> Option<u8> + Send + Sync>;
type AdditionalStructureRuleRefFn = fn(&Segment<'_>) -> Result<(), EdifactError>;
type AdditionalStructureRuleFn =
Arc<dyn Fn(&Segment<'_>) -> Result<(), EdifactError> + Send + Sync>;
type CodeListRulesFn = Arc<dyn Fn(&str) -> &'static [(usize, usize, &'static str)] + Send + Sync>;
type RequiredSegmentsFn = Arc<dyn Fn(&str) -> &'static [&'static str] + Send + Sync>;
enum SegmentDefRef<'a> {
Static(&'static SegmentDefinition),
Owned(&'a OwnedSegmentDef),
}
impl SegmentDefRef<'_> {
fn max_element_position(&self) -> usize {
match self {
Self::Static(d) => d
.elements
.iter()
.map(|e| e.position as usize)
.max()
.unwrap_or(0),
Self::Owned(d) => d
.elements
.iter()
.map(|e| e.position as usize)
.max()
.unwrap_or(0),
}
}
fn last_mandatory_position(&self) -> usize {
match self {
Self::Static(d) => d
.elements
.iter()
.filter(|e| e.status == Status::Mandatory)
.map(|e| e.position as usize)
.max()
.unwrap_or(0),
Self::Owned(d) => d
.elements
.iter()
.filter(|e| e.status == Status::Mandatory)
.map(|e| e.position as usize)
.max()
.unwrap_or(0),
}
}
fn for_each_mandatory_position<E, F>(&self, mut f: F) -> Result<(), E>
where
F: FnMut(usize, &str) -> Result<(), E>,
{
match self {
Self::Static(d) => {
for e in d.elements.iter().filter(|e| e.status == Status::Mandatory) {
f((e.position as usize).saturating_sub(1), e.data_element)?;
}
}
Self::Owned(d) => {
for e in d.elements.iter().filter(|e| e.status == Status::Mandatory) {
f(
(e.position as usize).saturating_sub(1),
e.data_element.as_str(),
)?;
}
}
}
Ok(())
}
fn for_each_mandatory_component<E, F>(&self, mut f: F) -> Result<(), E>
where
F: FnMut(usize, usize, &str) -> Result<(), E>,
{
match self {
Self::Static(d) => {
for e in d.elements {
for c in e
.components
.iter()
.filter(|c| c.status == Status::Mandatory)
{
f(
(e.position as usize).saturating_sub(1),
(c.position as usize).saturating_sub(1),
c.data_element,
)?;
}
}
}
Self::Owned(d) => {
for e in &d.elements {
for c in e
.components
.iter()
.filter(|c| c.status == Status::Mandatory)
{
f(
(e.position as usize).saturating_sub(1),
(c.position as usize).saturating_sub(1),
c.data_element.as_str(),
)?;
}
}
}
}
Ok(())
}
fn max_repeat_at(&self, index: usize) -> Option<u8> {
let position = u8::try_from(index.checked_add(1)?).ok()?;
match self {
Self::Static(d) => d
.elements
.iter()
.find(|e| e.position == position)
.map(ElementRef::max_repeat),
Self::Owned(d) => d
.elements
.iter()
.find(|e| e.position == position)
.map(OwnedElementRef::max_repeat),
}
}
fn repr_at(
&self,
element: usize,
component: usize,
syntax_version: Option<u8>,
) -> Option<ReprRequirement> {
let position = u8::try_from(element.checked_add(1)?).ok()?;
let component_position = u8::try_from(component.checked_add(1)?).ok()?;
match self {
Self::Static(d) => {
let element = d.elements.iter().find(|e| e.position == position)?;
if element.components.is_empty() {
return if component == 0 {
select_repr(element.repr(), element.repr_from_v4(), syntax_version)
} else {
None
};
}
let component_ref = element.components.iter().find(|c| {
let first = c.position();
let last = first.saturating_add(c.repeat_count().saturating_sub(1));
(first..=last).contains(&component_position)
})?;
select_repr(
component_ref.repr(),
component_ref.repr_from_v4(),
syntax_version,
)
}
Self::Owned(d) => {
let element = d.elements.iter().find(|e| e.position == position)?;
if element.components.is_empty() {
return if component == 0 {
select_repr(
OwnedElementRef::repr(element),
OwnedElementRef::repr_from_v4(element),
syntax_version,
)
} else {
None
};
}
let component_ref = element.components.iter().find(|c| {
let first = c.position();
let last = first.saturating_add(c.repeat_count().saturating_sub(1));
(first..=last).contains(&component_position)
})?;
select_repr(
component_ref.repr(),
component_ref.repr_from_v4(),
syntax_version,
)
}
}
}
fn declared_component_count(&self, index: usize) -> Option<u8> {
let position = u8::try_from(index.checked_add(1)?).ok()?;
let count: u32 = match self {
Self::Static(d) => d
.elements
.iter()
.find(|e| e.position == position)
.map(|e| e.components.iter().map(|c| u32::from(c.repeat_count)).sum())?,
Self::Owned(d) => d
.elements
.iter()
.find(|e| e.position == position)
.map(|e| e.components.iter().map(|c| u32::from(c.repeat_count)).sum())?,
};
if count == 0 {
return None;
}
u8::try_from(count).ok()
}
}
fn detect_syntax_version(segments: &[Segment<'_>]) -> Option<u8> {
segments
.iter()
.find(|s| s.tag == "UNB")
.and_then(|unb| unb.component_str(0, 1))
.and_then(|version| version.parse().ok())
}
fn select_repr(
base: Option<Repr>,
from_v4: Option<Repr>,
syntax_version: Option<u8>,
) -> Option<ReprRequirement> {
match (base, from_v4) {
(_, None) => base.map(ReprRequirement::single),
(None, Some(v4)) => Some(ReprRequirement::single(v4)),
(Some(base), Some(v4)) => Some(match syntax_version {
Some(version) if version >= 4 => ReprRequirement::single(v4),
Some(_) => ReprRequirement::single(base),
None => ReprRequirement {
primary: base,
alternative: Some(v4),
},
}),
}
}
fn default_required_segments(_message_type: &str) -> &'static [&'static str] {
&["UNH", "UNT"]
}
pub(crate) fn base_code_list_rules(tag: &str) -> &'static [(usize, usize, &'static str)] {
match tag {
"BGM" => &[(0, 0, "1001")],
"DTM" => &[(0, 0, "2005")],
"NAD" => &[(0, 0, "3035")],
"QTY" => &[(0, 0, "6063")],
"RFF" => &[(0, 0, "1153")],
"MOA" => &[(0, 0, "5025")],
"PRI" => &[(0, 0, "5125")],
"LOC" => &[(0, 0, "3227")],
_ => &[],
}
}
#[derive(Clone)]
pub struct DirectoryValidator {
directory_id: String,
segment_lookup: SegmentLookupFn,
owned_defs: Option<Arc<Vec<OwnedSegmentDef>>>,
owned_index: Option<Arc<std::collections::HashMap<String, usize>>>,
is_code_valid: IsCodeValidFn,
suggest_code: SuggestCodeFn,
expected_components: ExpectedComponentsFn,
code_list_rules: CodeListRulesFn,
additional_structure_rule: Option<AdditionalStructureRuleFn>,
required_segments: RequiredSegmentsFn,
message_type: Option<String>,
enforce_known_tags: bool,
structure_checks: bool,
code_list_checks: bool,
}
impl std::fmt::Debug for DirectoryValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DirectoryValidator")
.field("directory_id", &self.directory_id)
.field("message_type", &self.message_type)
.field("enforce_known_tags", &self.enforce_known_tags)
.field("structure_checks", &self.structure_checks)
.field("code_list_checks", &self.code_list_checks)
.finish_non_exhaustive()
}
}
impl DirectoryValidator {
pub fn new(
directory_id: &'static str,
segment_lookup: fn(&str) -> Option<&'static SegmentDefinition>,
is_code_valid: fn(&str, &str) -> bool,
suggest_code: fn(&str, &str) -> Option<&'static str>,
expected_components: fn(&str, usize) -> Option<u8>,
additional_structure_rule: Option<AdditionalStructureRuleRefFn>,
) -> Self {
Self {
directory_id: directory_id.to_owned(),
segment_lookup: Arc::new(segment_lookup),
owned_defs: None,
owned_index: None,
is_code_valid: Arc::new(is_code_valid),
suggest_code: Arc::new(suggest_code),
expected_components: Arc::new(expected_components),
code_list_rules: Arc::new(base_code_list_rules),
additional_structure_rule: additional_structure_rule
.map(|f| Arc::new(f) as AdditionalStructureRuleFn),
required_segments: Arc::new(default_required_segments),
message_type: None,
enforce_known_tags: true,
structure_checks: true,
code_list_checks: true,
}
}
pub fn from_definitions(definitions: &'static [SegmentDefinition]) -> Self {
let lookup_map: std::collections::HashMap<&'static str, &'static SegmentDefinition> =
definitions.iter().map(|d| (d.tag, d)).collect();
let lookup_map = Arc::new(lookup_map);
Self {
directory_id: "custom".to_owned(),
segment_lookup: Arc::new(move |tag: &str| lookup_map.get(tag).copied()),
owned_defs: None,
owned_index: None,
is_code_valid: Arc::new(|_de: &str, _code: &str| true),
suggest_code: Arc::new(|_de: &str, _code: &str| None),
expected_components: Arc::new(|_tag: &str, _idx: usize| None),
code_list_rules: Arc::new(base_code_list_rules),
additional_structure_rule: None,
required_segments: Arc::new(default_required_segments),
message_type: None,
enforce_known_tags: true,
structure_checks: true,
code_list_checks: false,
}
}
pub fn from_owned_definitions(definitions: Vec<OwnedSegmentDef>) -> Self {
Self {
directory_id: "custom".to_owned(),
segment_lookup: Arc::new(|_| None),
owned_index: Some(Arc::new(
definitions
.iter()
.enumerate()
.map(|(i, d)| (d.tag.clone(), i))
.collect(),
)),
owned_defs: Some(Arc::new(definitions)),
is_code_valid: Arc::new(|_de: &str, _code: &str| true),
suggest_code: Arc::new(|_de: &str, _code: &str| None),
expected_components: Arc::new(|_tag: &str, _idx: usize| None),
code_list_rules: Arc::new(base_code_list_rules),
additional_structure_rule: None,
required_segments: Arc::new(default_required_segments),
message_type: None,
enforce_known_tags: true,
structure_checks: true,
code_list_checks: false,
}
}
pub fn with_directory_id(mut self, id: impl Into<String>) -> Self {
self.directory_id = id.into();
self
}
pub fn with_code_list_rules(
mut self,
f: impl Fn(&str) -> &'static [(usize, usize, &'static str)] + Send + Sync + 'static,
) -> Self {
self.code_list_rules = Arc::new(f);
self
}
pub fn structure_only(mut self) -> Self {
self.structure_checks = true;
self.code_list_checks = false;
self
}
pub fn code_list_only(mut self) -> Self {
self.structure_checks = false;
self.code_list_checks = true;
self
}
pub fn enforce_known_tags(mut self, enforce: bool) -> Self {
self.enforce_known_tags = enforce;
self
}
pub fn with_required_segments(
mut self,
f: impl Fn(&str) -> &'static [&'static str] + Send + Sync + 'static,
) -> Self {
self.required_segments = Arc::new(f);
self
}
fn detect_message_type(&self, segments: &[Segment<'_>]) -> Option<String> {
if let Some(explicit) = self.message_type.as_deref() {
return Some(explicit.to_owned());
}
segments
.iter()
.find(|s| s.tag == "UNH")
.and_then(|s| s.get_element(1))
.and_then(|e| e.get_component(0))
.map(str::to_owned)
}
fn effective_component_count(seg: &Segment<'_>, element_idx: usize) -> Option<u8> {
let elem = seg.elements.get(element_idx)?;
let mut count = elem.components.len();
while count > 0 && elem.components[count - 1].0.as_ref().is_empty() {
count -= 1;
}
u8::try_from(count).ok()
}
fn collect_component_count_issues(
&self,
seg: &Segment<'_>,
def: &SegmentDefRef<'_>,
out: &mut Vec<EdifactError>,
) {
for idx in 0..seg.elements.len() {
let actual = Self::effective_component_count(seg, idx).unwrap_or(0);
if let Some(expected) = (self.expected_components)(seg.tag, idx) {
if actual != expected {
out.push(EdifactError::InvalidComponentCount {
tag: seg.tag.to_owned(),
element_index: idx,
expected,
actual,
span: seg.element_span(idx).unwrap_or(seg.span),
});
}
continue;
}
if let Some(declared) = def.declared_component_count(idx) {
if actual > declared {
out.push(EdifactError::InvalidComponentCount {
tag: seg.tag.to_owned(),
element_index: idx,
expected: declared,
actual,
span: seg.element_span(idx).unwrap_or(seg.span),
});
}
}
}
}
fn collect_repetition_issues(
&self,
seg: &Segment<'_>,
def: &SegmentDefRef<'_>,
out: &mut Vec<EdifactError>,
) {
for (index, element) in seg.elements.iter().enumerate() {
let Some(max) = def.max_repeat_at(index) else {
continue;
};
if max == 0 {
continue;
}
let actual = element.repeat_count();
if actual > usize::from(max) {
out.push(EdifactError::TooManyRepetitions {
tag: seg.tag.to_owned(),
element_index: index,
max,
actual,
span: element.span,
});
}
}
}
fn collect_representation_issues(
&self,
seg: &Segment<'_>,
def: &SegmentDefRef<'_>,
syntax_version: Option<u8>,
out: &mut Vec<EdifactError>,
) {
for (element_index, element) in seg.elements.iter().enumerate() {
for occurrence in element.repetitions() {
for (component_index, (value, span)) in occurrence.iter().enumerate() {
if value.is_empty() {
continue;
}
let Some(repr) = def.repr_at(element_index, component_index, syntax_version)
else {
continue;
};
if !repr.permits_characters(value) {
out.push(EdifactError::InvalidCharacterType {
tag: seg.tag.to_owned(),
element_index,
component_index,
repr: repr.to_string(),
value: value.to_string(),
span: *span,
});
continue;
}
self.collect_insignificant_characters(
seg,
element_index,
component_index,
value,
*span,
repr.primary,
out,
);
if repr.permits_length(value) {
continue;
}
let actual = repr.measure(value);
out.push(if repr.is_too_short(value) {
EdifactError::DataElementTooShort {
tag: seg.tag.to_owned(),
element_index,
component_index,
repr: repr.to_string(),
actual,
span: *span,
}
} else {
EdifactError::DataElementTooLong {
tag: seg.tag.to_owned(),
element_index,
component_index,
repr: repr.to_string(),
actual,
span: *span,
}
});
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn collect_insignificant_characters(
&self,
seg: &Segment<'_>,
element_index: usize,
component_index: usize,
value: &str,
span: crate::Span,
repr: Repr,
out: &mut Vec<EdifactError>,
) {
if repr.is_fixed() {
return;
}
let kind = match repr.kind() {
ReprKind::Numeric => {
let digits = value.strip_prefix('-').unwrap_or(value);
let leading_zeroes = digits.starts_with('0')
&& digits.len() > 1
&& !digits.starts_with("0.")
&& !digits.starts_with("0,");
if !leading_zeroes {
return;
}
Insignificant::LeadingZeroes
}
ReprKind::Alphabetic | ReprKind::Alphanumeric => {
if !value.ends_with(' ') {
return;
}
Insignificant::TrailingSpaces
}
};
out.push(EdifactError::InsignificantCharacters {
tag: seg.tag.to_owned(),
element_index,
component_index,
kind,
span,
});
}
fn collect_code_list_issues(&self, seg: &Segment<'_>, out: &mut Vec<EdifactError>) {
for (elem_idx, comp_idx, de) in (self.code_list_rules)(seg.tag) {
let value = seg
.get_element(*elem_idx)
.and_then(|e| e.get_component(*comp_idx))
.unwrap_or("");
if !value.is_empty() && !(self.is_code_valid)(de, value) {
let suggestion = (self.suggest_code)(de, value);
let span = seg
.get_element(*elem_idx)
.and_then(|e| e.component_span(*comp_idx))
.unwrap_or(seg.span);
out.push(EdifactError::InvalidCodeValue {
tag: seg.tag.to_owned(),
element_index: *elem_idx,
value: value.to_owned(),
code_list: (*de).to_owned(),
span,
suggestion,
});
}
}
}
}
impl DirectoryValidator {
fn resolve_def<'a>(&'a self, tag: &str) -> Option<SegmentDefRef<'a>> {
if let Some(owned) = &self.owned_defs {
let index = self.owned_index.as_ref()?;
owned.get(*index.get(tag)?).map(SegmentDefRef::Owned)
} else {
(self.segment_lookup)(tag).map(SegmentDefRef::Static)
}
}
fn collect_segment_issues(
&self,
seg: &Segment<'_>,
syntax_version: Option<u8>,
out: &mut Vec<EdifactError>,
) {
if !self.structure_checks && !self.code_list_checks {
return;
}
let Some(def) = self.resolve_def(seg.tag) else {
if self.structure_checks && self.enforce_known_tags {
out.push(EdifactError::InvalidSegmentForMessage {
tag: seg.tag.to_owned(),
message_type: self
.message_type
.clone()
.unwrap_or_else(|| self.directory_id.clone()),
span: seg.tag_span,
});
}
return;
};
if self.structure_checks {
let max_elements = def.max_element_position();
let min_elements = def.last_mandatory_position();
let actual = seg.elements.len();
if actual < min_elements || actual > max_elements {
out.push(EdifactError::InvalidElementCount {
tag: seg.tag.to_owned(),
min: min_elements,
max: max_elements,
actual,
span: seg.span,
});
}
def.for_each_mandatory_position::<std::convert::Infallible, _>(|idx, _de| {
let is_present = seg.elements.get(idx).is_some_and(|elem| {
elem.components.iter().any(|(c, _)| !c.as_ref().is_empty())
});
if !is_present {
out.push(EdifactError::MissingRequiredElement {
tag: seg.tag.to_owned(),
element_index: idx,
});
}
Ok(())
})
.unwrap_or_else(|never| match never {});
def.for_each_mandatory_component::<std::convert::Infallible, _>(
|elem_idx, comp_idx, _de| {
let Some(elem) = seg.elements.get(elem_idx) else {
return Ok(());
};
let present = elem
.get_component(comp_idx)
.is_some_and(|value| !value.is_empty());
if !present {
out.push(EdifactError::MissingRequiredComponent {
tag: seg.tag.to_owned(),
element_index: elem_idx,
component_index: comp_idx,
});
}
Ok(())
},
)
.unwrap_or_else(|never| match never {});
self.collect_component_count_issues(seg, &def, out);
self.collect_repetition_issues(seg, &def, out);
self.collect_representation_issues(seg, &def, syntax_version, out);
if let Some(rule) = &self.additional_structure_rule {
if let Err(error) = rule(seg) {
out.push(error);
}
}
}
if self.code_list_checks {
self.collect_code_list_issues(seg, out);
}
}
}
impl Validator for DirectoryValidator {
fn set_message_type(&mut self, message_type: Option<&str>) {
self.message_type = message_type.map(str::to_owned);
}
fn validate_batch(
&self,
segments: &[Segment<'_>],
report: &mut ValidationReport,
_context: &ValidationRuleContext<'_>,
) {
let syntax_version = detect_syntax_version(segments);
let mut issues = Vec::new();
for seg in segments {
self.collect_segment_issues(seg, syntax_version, &mut issues);
for err in issues.drain(..) {
report_error(report, err);
}
}
if self.structure_checks {
if let Some(message_type) = self.detect_message_type(segments) {
let mut first_index: std::collections::HashMap<&str, usize> =
std::collections::HashMap::with_capacity(segments.len());
for (i, seg) in segments.iter().enumerate() {
first_index.entry(seg.tag).or_insert(i);
}
let required = (self.required_segments)(&message_type);
for required_tag in required {
if !first_index.contains_key(*required_tag) {
report.add_error(
ValidationIssue::new(
ValidationSeverity::Error,
format!(
"required segment {} missing for message type {}",
required_tag, message_type
),
)
.with_segment(*required_tag)
.with_suggestion("Add the mandatory segment at the correct position"),
);
}
}
let mut last_idx = None;
for tag in required {
if let Some(&idx) = first_index.get(*tag) {
if let Some(prev) = last_idx {
if idx < prev {
report.add_error(
ValidationIssue::new(
ValidationSeverity::Error,
format!(
"segment sequence violation for message type {}: '{}' appears out of order",
message_type, tag
),
)
.with_segment(*tag)
.with_suggestion(
"Ensure required segments follow UN/EDIFACT canonical order",
),
);
}
}
last_idx = Some(idx);
}
}
}
}
}
}
#[derive(Debug, Default)]
pub struct DirectoryValidatorBuilder {
directory_id: Option<String>,
segments: Vec<OwnedSegmentDef>,
}
impl DirectoryValidatorBuilder {
pub fn new(directory_id: impl Into<String>) -> Self {
Self {
directory_id: Some(directory_id.into()),
segments: Vec::new(),
}
}
pub fn add_segment(mut self, def: OwnedSegmentDef) -> Self {
self.segments.push(def);
self
}
pub fn add_segments(mut self, defs: impl IntoIterator<Item = OwnedSegmentDef>) -> Self {
self.segments.extend(defs);
self
}
pub fn build(self) -> DirectoryValidator {
let mut validator = DirectoryValidator::from_owned_definitions(self.segments);
if let Some(id) = self.directory_id {
validator.directory_id = id;
}
validator
}
}
#[cfg(test)]
mod tests {
use super::*;
static TEST_ELEMENTS: &[ElementRef] = &[ElementRef::new(1, "C507", Status::Mandatory, 1)];
static TEST_SEGMENT: SegmentDefinition =
SegmentDefinition::new("TST", "Test segment", TEST_ELEMENTS);
fn segment_lookup(tag: &str) -> Option<&'static SegmentDefinition> {
match tag {
"TST" => Some(&TEST_SEGMENT),
_ => None,
}
}
fn code_valid(_de: &str, _code: &str) -> bool {
true
}
fn suggest_code(_de: &str, _code: &str) -> Option<&'static str> {
None
}
fn expected_components(_tag: &str, _idx: usize) -> Option<u8> {
None
}
#[test]
fn mandatory_composite_present_when_any_component_non_empty() {
let input = b"TST+:ABC'";
let segments: Vec<_> = crate::from_bytes(input)
.collect::<Result<Vec<_>, _>>()
.expect("parse should succeed");
let validator = DirectoryValidator::new(
"TEST",
segment_lookup,
code_valid,
suggest_code,
expected_components,
None,
);
let mut report = ValidationReport::default();
validator.validate_batch(
&segments,
&mut report,
&crate::validator::ValidationRuleContext::empty(),
);
assert!(!report.has_errors());
}
fn parse_single(input: &[u8]) -> crate::OwnedSegment {
crate::from_reader_collect(std::io::Cursor::new(input))
.expect("parse should succeed")
.into_iter()
.next()
.expect("at least one segment")
}
#[test]
fn trailing_empty_component_stripped_from_dtm() {
let owned = parse_single(b"DTM+137:20200101:'");
let seg = owned.as_borrowed();
let count = DirectoryValidator::effective_component_count(&seg, 0);
assert_eq!(
count,
Some(2),
"trailing empty component should be stripped"
);
}
#[test]
fn all_empty_components_result_in_zero() {
let owned = parse_single(b"NAD+MS++:'");
let seg = owned.as_borrowed();
let count = DirectoryValidator::effective_component_count(&seg, 2);
assert_eq!(
count,
Some(0),
"all-empty composite should have effective count 0"
);
}
#[test]
fn non_empty_component_not_stripped() {
let owned = parse_single(b"DTM+137:20200101:102'");
let seg = owned.as_borrowed();
let count = DirectoryValidator::effective_component_count(&seg, 0);
assert_eq!(
count,
Some(3),
"no components should be stripped when all non-empty"
);
}
#[test]
fn with_code_list_rules_overrides_base() {
fn custom_rules(tag: &str) -> &'static [(usize, usize, &'static str)] {
match tag {
"TST" => &[(0, 0, "CUSTOM_DE")],
_ => &[],
}
}
fn custom_code_valid(_de: &str, code: &str) -> bool {
code == "VALID"
}
fn no_suggestion(_de: &str, _code: &str) -> Option<&'static str> {
None
}
let input = b"TST+INVALID'";
let segments: Vec<_> = crate::from_bytes(input)
.collect::<Result<Vec<_>, _>>()
.expect("parse should succeed");
let validator = DirectoryValidator::new(
"TEST",
segment_lookup,
custom_code_valid,
no_suggestion,
expected_components,
None,
)
.with_code_list_rules(custom_rules);
let mut report = ValidationReport::default();
validator.validate_batch(
&segments,
&mut report,
&crate::validator::ValidationRuleContext::empty(),
);
assert!(
report.has_warnings(),
"INVALID is not in the custom code list so validation must warn"
);
}
}