1use crate::validator::{ValidationRuleContext, Validator, report_error};
4use crate::{EdifactError, Segment, ValidationIssue, ValidationReport, ValidationSeverity};
5use std::sync::Arc;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Status {
10 Mandatory,
12 Conditional,
14}
15
16#[derive(Debug, Clone, Copy)]
18pub struct ElementRef {
19 pub position: u8,
21 pub data_element: &'static str,
23 pub status: Status,
25 pub max_repeat: u8,
27}
28
29#[derive(Debug)]
31pub struct SegmentDefinition {
32 pub tag: &'static str,
34 pub name: &'static str,
36 pub elements: &'static [ElementRef],
38}
39
40type SegmentLookupFn = Arc<dyn Fn(&str) -> Option<&'static SegmentDefinition> + Send + Sync>;
41type IsCodeValidFn = Arc<dyn Fn(&str, &str) -> bool + Send + Sync>;
42type SuggestCodeFn = Arc<dyn Fn(&str, &str) -> Option<&'static str> + Send + Sync>;
43type ExpectedComponentsFn = Arc<dyn Fn(&str, usize) -> Option<u8> + Send + Sync>;
44type AdditionalStructureRuleRefFn = fn(&Segment<'_>) -> Result<(), EdifactError>;
45type AdditionalStructureRuleFn = Arc<dyn Fn(&Segment<'_>) -> Result<(), EdifactError> + Send + Sync>;
46type CodeListRulesFn = Arc<dyn Fn(&str) -> &'static [(usize, usize, &'static str)] + Send + Sync>;
49type RequiredSegmentsFn = Arc<dyn Fn(&str) -> &'static [&'static str] + Send + Sync>;
56
57fn default_required_segments(message_type: &str) -> &'static [&'static str] {
59 match message_type {
60 "UTILMD" | "ORDERS" | "INVOIC" => &["UNH", "BGM", "UNT"],
61 _ => &["UNH", "UNT"],
62 }
63}
64
65pub(crate) fn base_code_list_rules(tag: &str) -> &'static [(usize, usize, &'static str)] {
73 match tag {
74 "BGM" => &[(0, 0, "1001")],
75 "DTM" => &[(0, 0, "2005")],
76 "NAD" => &[(0, 0, "3035")],
77 "QTY" => &[(0, 0, "6063")],
78 "RFF" => &[(0, 0, "1153")],
79 "MOA" => &[(0, 0, "5025")],
80 "PRI" => &[(0, 0, "5125")],
81 "LOC" => &[(0, 0, "3227")],
82 _ => &[],
83 }
84}
85
86#[derive(Clone)]
99pub struct DirectoryValidator {
100 directory_id: &'static str,
101 segment_lookup: SegmentLookupFn,
102 is_code_valid: IsCodeValidFn,
103 suggest_code: SuggestCodeFn,
104 expected_components: ExpectedComponentsFn,
105 code_list_rules: CodeListRulesFn,
106 additional_structure_rule: Option<AdditionalStructureRuleFn>,
107 required_segments: RequiredSegmentsFn,
109 message_type: Option<String>,
110 enforce_known_tags: bool,
111 structure_checks: bool,
112 code_list_checks: bool,
113}
114
115impl std::fmt::Debug for DirectoryValidator {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 f.debug_struct("DirectoryValidator")
118 .field("directory_id", &self.directory_id)
119 .field("message_type", &self.message_type)
120 .field("enforce_known_tags", &self.enforce_known_tags)
121 .field("structure_checks", &self.structure_checks)
122 .field("code_list_checks", &self.code_list_checks)
123 .finish_non_exhaustive()
124 }
125}
126
127impl DirectoryValidator {
128 pub fn new(
130 directory_id: &'static str,
131 segment_lookup: fn(&str) -> Option<&'static SegmentDefinition>,
132 is_code_valid: fn(&str, &str) -> bool,
133 suggest_code: fn(&str, &str) -> Option<&'static str>,
134 expected_components: fn(&str, usize) -> Option<u8>,
135 additional_structure_rule: Option<AdditionalStructureRuleRefFn>,
136 ) -> Self {
137 Self {
138 directory_id,
139 segment_lookup: Arc::new(segment_lookup),
140 is_code_valid: Arc::new(is_code_valid),
141 suggest_code: Arc::new(suggest_code),
142 expected_components: Arc::new(expected_components),
143 code_list_rules: Arc::new(base_code_list_rules),
144 additional_structure_rule: additional_structure_rule
145 .map(|f| Arc::new(f) as AdditionalStructureRuleFn),
146 required_segments: Arc::new(default_required_segments),
147 message_type: None,
148 enforce_known_tags: true,
149 structure_checks: true,
150 code_list_checks: true,
151 }
152 }
153
154 pub fn from_definitions(definitions: &'static [SegmentDefinition]) -> Self {
172 Self {
173 directory_id: "custom",
174 segment_lookup: Arc::new(move |tag: &str| {
175 definitions.iter().find(|d| d.tag == tag)
176 }),
177 is_code_valid: Arc::new(|_de: &str, _code: &str| true),
178 suggest_code: Arc::new(|_de: &str, _code: &str| None),
179 expected_components: Arc::new(|_tag: &str, _idx: usize| None),
180 code_list_rules: Arc::new(base_code_list_rules),
181 additional_structure_rule: None,
182 required_segments: Arc::new(default_required_segments),
183 message_type: None,
184 enforce_known_tags: true,
185 structure_checks: true,
186 code_list_checks: false,
187 }
188 }
189
190 pub fn with_directory_id(mut self, id: &'static str) -> Self {
192 self.directory_id = id;
193 self
194 }
195
196 pub fn with_code_list_rules(mut self, f: impl Fn(&str) -> &'static [(usize, usize, &'static str)] + Send + Sync + 'static) -> Self {
201 self.code_list_rules = Arc::new(f);
202 self
203 }
204
205 pub fn structure_only(mut self) -> Self {
207 self.structure_checks = true;
208 self.code_list_checks = false;
209 self
210 }
211
212 pub fn code_list_only(mut self) -> Self {
214 self.structure_checks = false;
215 self.code_list_checks = true;
216 self
217 }
218
219 pub fn enforce_known_tags(mut self, enforce: bool) -> Self {
221 self.enforce_known_tags = enforce;
222 self
223 }
224
225 pub fn with_required_segments(
247 mut self,
248 f: impl Fn(&str) -> &'static [&'static str] + Send + Sync + 'static,
249 ) -> Self {
250 self.required_segments = Arc::new(f);
251 self
252 }
253
254 fn detect_message_type(&self, segments: &[Segment<'_>]) -> Option<String> {
255 if let Some(explicit) = self.message_type.as_deref() {
256 return Some(explicit.to_owned());
257 }
258
259 segments
260 .iter()
261 .find(|s| s.tag == "UNH")
262 .and_then(|s| s.get_element(1))
263 .and_then(|e| e.get_component(0))
264 .map(str::to_owned)
265 }
266
267 fn effective_component_count(seg: &Segment<'_>, element_idx: usize) -> Option<u8> {
279 let elem = seg.elements.get(element_idx)?;
280 let mut count = elem.components.len();
281 while count > 0 && elem.components[count - 1].as_ref().is_empty() {
282 count -= 1;
283 }
284 u8::try_from(count).ok()
285 }
286
287 fn validate_component_counts(&self, seg: &Segment<'_>) -> Result<(), EdifactError> {
288 for idx in 0..seg.elements.len() {
289 if let Some(expected) = (self.expected_components)(seg.tag, idx) {
290 let actual = Self::effective_component_count(seg, idx).unwrap_or(0);
291 if actual != expected {
292 return Err(EdifactError::InvalidComponentCount {
293 tag: seg.tag.to_owned(),
294 element_index: idx,
295 expected,
296 actual,
297 offset: seg.span.start,
298 });
299 }
300 }
301 }
302 Ok(())
303 }
304
305 fn validate_code_lists(&self, seg: &Segment<'_>) -> Result<(), EdifactError> {
306 let rules = (self.code_list_rules)(seg.tag);
307
308 for (elem_idx, comp_idx, de) in rules {
309 let value = seg
310 .get_element(*elem_idx)
311 .and_then(|e| e.get_component(*comp_idx))
312 .unwrap_or("");
313 if !value.is_empty() && !(self.is_code_valid)(de, value) {
314 let suggestion = (self.suggest_code)(de, value);
315 return Err(EdifactError::InvalidCodeValue {
316 tag: seg.tag.to_owned(),
317 element_index: *elem_idx,
318 value: value.to_owned(),
319 code_list: (*de).to_owned(),
320 offset: seg.span.start,
321 suggestion,
322 });
323 }
324 }
325
326 Ok(())
327 }
328}
329
330impl DirectoryValidator {
331 fn validate_segment(&self, seg: &Segment<'_>) -> Result<(), EdifactError> {
332 if !self.structure_checks && !self.code_list_checks {
333 return Ok(());
334 }
335
336 let Some(def) = (self.segment_lookup)(seg.tag) else {
337 if self.structure_checks && self.enforce_known_tags {
338 return Err(EdifactError::InvalidSegmentForMessage {
339 tag: seg.tag.to_owned(),
340 message_type: self
341 .message_type
342 .clone()
343 .unwrap_or_else(|| self.directory_id.to_owned()),
344 offset: seg.tag_span.start,
345 });
346 }
347 return Ok(());
348 };
349
350 let max_elements = def.elements.len();
351 let min_elements = def
352 .elements
353 .iter()
354 .rposition(|e| e.status == Status::Mandatory)
355 .map(|idx| idx + 1)
356 .unwrap_or(0);
357 let actual = seg.elements.len();
358
359 if self.structure_checks && (actual < min_elements || actual > max_elements) {
360 return Err(EdifactError::InvalidElementCount {
361 tag: seg.tag.to_owned(),
362 min: min_elements,
363 max: max_elements,
364 actual,
365 offset: seg.span.start,
366 });
367 }
368
369 if self.structure_checks {
370 for element in def
371 .elements
372 .iter()
373 .filter(|e| e.status == Status::Mandatory)
374 {
375 let idx = (element.position as usize).saturating_sub(1);
376 let is_present = seg
377 .elements
378 .get(idx)
379 .is_some_and(|elem| elem.components.iter().any(|c| !c.as_ref().is_empty()));
380 if !is_present {
381 return Err(EdifactError::MissingRequiredElement {
382 tag: seg.tag.to_owned(),
383 element_index: idx,
384 });
385 }
386 }
387 self.validate_component_counts(seg)?;
388
389 if let Some(rule) = &self.additional_structure_rule {
390 rule(seg)?;
391 }
392 }
393
394 if self.code_list_checks {
395 self.validate_code_lists(seg)?;
396 }
397
398 Ok(())
399 }
400}
401
402impl Validator for DirectoryValidator {
403 fn set_message_type(&mut self, message_type: Option<&str>) {
404 self.message_type = message_type.map(str::to_owned);
405 }
406
407 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport, _context: &ValidationRuleContext<'_>) {
408 for seg in segments {
409 if let Err(err) = self.validate_segment(seg) {
410 report_error(report, err);
411 }
412 }
413
414 if self.structure_checks {
415 if let Some(message_type) = self.detect_message_type(segments) {
416 for required_tag in (self.required_segments)(&message_type) {
417 if segments.iter().all(|s| s.tag != *required_tag) {
418 report.add_error(
419 ValidationIssue::new(
420 ValidationSeverity::Error,
421 format!(
422 "required segment {} missing for message type {}",
423 required_tag, message_type
424 ),
425 )
426 .with_segment(*required_tag)
427 .with_suggestion("Add the mandatory segment at the correct position"),
428 );
429 }
430 }
431
432 let seq = (self.required_segments)(&message_type);
433 let mut last_idx = None;
434 for tag in seq {
435 if let Some(idx) = segments.iter().position(|s| s.tag == *tag) {
436 if let Some(prev) = last_idx {
437 if idx < prev {
438 report.add_error(
439 ValidationIssue::new(
440 ValidationSeverity::Error,
441 format!(
442 "segment sequence violation for message type {}: '{}' appears out of order",
443 message_type, tag
444 ),
445 )
446 .with_segment(*tag)
447 .with_suggestion(
448 "Ensure required segments follow UN/EDIFACT canonical order",
449 ),
450 );
451 }
452 }
453 last_idx = Some(idx);
454 }
455 }
456 }
457 }
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 static TEST_ELEMENTS: &[ElementRef] = &[ElementRef {
466 position: 1,
467 data_element: "C507",
468 status: Status::Mandatory,
469 max_repeat: 1,
470 }];
471
472 static TEST_SEGMENT: SegmentDefinition = SegmentDefinition {
473 tag: "TST",
474 name: "Test segment",
475 elements: TEST_ELEMENTS,
476 };
477
478 fn segment_lookup(tag: &str) -> Option<&'static SegmentDefinition> {
479 match tag {
480 "TST" => Some(&TEST_SEGMENT),
481 _ => None,
482 }
483 }
484
485 fn code_valid(_de: &str, _code: &str) -> bool {
486 true
487 }
488
489 fn suggest_code(_de: &str, _code: &str) -> Option<&'static str> {
490 None
491 }
492
493 fn expected_components(_tag: &str, _idx: usize) -> Option<u8> {
494 None
495 }
496
497 #[test]
498 fn mandatory_composite_present_when_any_component_non_empty() {
499 let input = b"TST+:ABC'";
500 let segments: Vec<_> = crate::from_bytes(input)
501 .collect::<Result<Vec<_>, _>>()
502 .expect("parse should succeed");
503
504 let validator = DirectoryValidator::new(
505 "TEST",
506 segment_lookup,
507 code_valid,
508 suggest_code,
509 expected_components,
510 None,
511 );
512
513 let mut report = ValidationReport::default();
514 validator.validate_batch(&segments, &mut report, &crate::validator::ValidationRuleContext::empty());
515 assert!(!report.has_errors());
516 }
517
518 fn parse_single(input: &[u8]) -> crate::OwnedSegment {
521 crate::from_reader(std::io::Cursor::new(input))
522 .expect("parse should succeed")
523 .into_iter()
524 .next()
525 .expect("at least one segment")
526 }
527
528 #[test]
529 fn trailing_empty_component_stripped_from_dtm() {
530 let owned = parse_single(b"DTM+137:20200101:'");
534 let seg = owned.as_borrowed();
535 let count = DirectoryValidator::effective_component_count(&seg, 0);
536 assert_eq!(count, Some(2), "trailing empty component should be stripped");
537 }
538
539 #[test]
540 fn all_empty_components_result_in_zero() {
541 let owned = parse_single(b"NAD+MS++:'");
543 let seg = owned.as_borrowed();
544 let count = DirectoryValidator::effective_component_count(&seg, 2);
545 assert_eq!(count, Some(0), "all-empty composite should have effective count 0");
546 }
547
548 #[test]
549 fn non_empty_component_not_stripped() {
550 let owned = parse_single(b"DTM+137:20200101:102'");
552 let seg = owned.as_borrowed();
553 let count = DirectoryValidator::effective_component_count(&seg, 0);
554 assert_eq!(count, Some(3), "no components should be stripped when all non-empty");
555 }
556
557 #[test]
558 fn with_code_list_rules_overrides_base() {
559 fn custom_rules(tag: &str) -> &'static [(usize, usize, &'static str)] {
561 match tag {
562 "TST" => &[(0, 0, "CUSTOM_DE")],
563 _ => &[],
564 }
565 }
566 fn custom_code_valid(_de: &str, code: &str) -> bool {
567 code == "VALID"
568 }
569 fn no_suggestion(_de: &str, _code: &str) -> Option<&'static str> {
570 None
571 }
572
573 let input = b"TST+INVALID'";
574 let segments: Vec<_> = crate::from_bytes(input)
575 .collect::<Result<Vec<_>, _>>()
576 .expect("parse should succeed");
577
578 let validator = DirectoryValidator::new(
579 "TEST",
580 segment_lookup,
581 custom_code_valid,
582 no_suggestion,
583 expected_components,
584 None,
585 )
586 .with_code_list_rules(custom_rules);
587
588 let mut report = ValidationReport::default();
589 validator.validate_batch(&segments, &mut report, &crate::validator::ValidationRuleContext::empty());
590 assert!(
591 report.has_warnings(),
592 "INVALID is not in the custom code list so validation must warn"
593 );
594 }
595}