1use crate::validator::{Validator, report_error};
4use crate::{EdifactError, Segment, ValidationIssue, ValidationReport, ValidationSeverity};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Status {
9 Mandatory,
11 Conditional,
13}
14
15#[derive(Debug, Clone, Copy)]
17pub struct ElementRef {
18 pub position: u8,
20 pub data_element: &'static str,
22 pub status: Status,
24 pub max_repeat: u8,
26}
27
28#[derive(Debug)]
30pub struct SegmentDefinition {
31 pub tag: &'static str,
33 pub name: &'static str,
35 pub elements: &'static [ElementRef],
37}
38
39type SegmentLookupFn = fn(&str) -> Option<&'static SegmentDefinition>;
40type IsCodeValidFn = fn(&str, &str) -> bool;
41type SuggestCodeFn = fn(&str, &str) -> Option<&'static str>;
42type ExpectedComponentsFn = fn(&str, usize) -> Option<u8>;
43type AdditionalStructureRuleFn = fn(&Segment<'_>) -> Result<(), EdifactError>;
44type CodeListRulesFn = fn(tag: &str) -> &'static [(usize, usize, &'static str)];
47
48pub(crate) fn base_code_list_rules(tag: &str) -> &'static [(usize, usize, &'static str)] {
56 match tag {
57 "BGM" => &[(0, 0, "1001")],
58 "DTM" => &[(0, 0, "2005")],
59 "NAD" => &[(0, 0, "3035")],
60 "QTY" => &[(0, 0, "6063")],
61 "RFF" => &[(0, 0, "1153")],
62 "MOA" => &[(0, 0, "5025")],
63 "PRI" => &[(0, 0, "5125")],
64 "LOC" => &[(0, 0, "3227")],
65 _ => &[],
66 }
67}
68
69#[derive(Clone)]
82pub struct DirectoryValidator {
83 directory_id: &'static str,
84 segment_lookup: SegmentLookupFn,
85 is_code_valid: IsCodeValidFn,
86 suggest_code: SuggestCodeFn,
87 expected_components: ExpectedComponentsFn,
88 code_list_rules: CodeListRulesFn,
89 additional_structure_rule: Option<AdditionalStructureRuleFn>,
90 message_type: Option<String>,
91 enforce_known_tags: bool,
92 structure_checks: bool,
93 code_list_checks: bool,
94}
95
96impl std::fmt::Debug for DirectoryValidator {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 f.debug_struct("DirectoryValidator")
99 .field("directory_id", &self.directory_id)
100 .field("message_type", &self.message_type)
101 .field("enforce_known_tags", &self.enforce_known_tags)
102 .field("structure_checks", &self.structure_checks)
103 .field("code_list_checks", &self.code_list_checks)
104 .finish_non_exhaustive()
105 }
106}
107
108impl DirectoryValidator {
109 pub fn new(
111 directory_id: &'static str,
112 segment_lookup: SegmentLookupFn,
113 is_code_valid: IsCodeValidFn,
114 suggest_code: SuggestCodeFn,
115 expected_components: ExpectedComponentsFn,
116 additional_structure_rule: Option<AdditionalStructureRuleFn>,
117 ) -> Self {
118 Self {
119 directory_id,
120 segment_lookup,
121 is_code_valid,
122 suggest_code,
123 expected_components,
124 code_list_rules: base_code_list_rules,
125 additional_structure_rule,
126 message_type: None,
127 enforce_known_tags: true,
128 structure_checks: true,
129 code_list_checks: true,
130 }
131 }
132
133 pub fn with_code_list_rules(mut self, f: CodeListRulesFn) -> Self {
138 self.code_list_rules = f;
139 self
140 }
141
142 pub fn structure_only(mut self) -> Self {
144 self.structure_checks = true;
145 self.code_list_checks = false;
146 self
147 }
148
149 pub fn code_list_only(mut self) -> Self {
151 self.structure_checks = false;
152 self.code_list_checks = true;
153 self
154 }
155
156 pub fn enforce_known_tags(mut self, enforce: bool) -> Self {
158 self.enforce_known_tags = enforce;
159 self
160 }
161
162 fn detect_message_type(&self, segments: &[Segment<'_>]) -> Option<String> {
163 if let Some(explicit) = self.message_type.as_deref() {
164 return Some(explicit.to_owned());
165 }
166
167 segments
168 .iter()
169 .find(|s| s.tag == "UNH")
170 .and_then(|s| s.get_element(1))
171 .and_then(|e| e.get_component(0))
172 .map(str::to_owned)
173 }
174
175 fn required_segments_for(message_type: &str) -> &'static [&'static str] {
186 match message_type {
187 "UTILMD" | "ORDERS" | "INVOIC" => &["UNH", "BGM", "UNT"],
188 _ => &["UNH", "UNT"],
189 }
190 }
191
192 fn effective_component_count(seg: &Segment<'_>, element_idx: usize) -> Option<u8> {
204 let elem = seg.elements.get(element_idx)?;
205 let mut count = elem.components.len();
206 while count > 0 && elem.components[count - 1].as_ref().is_empty() {
207 count -= 1;
208 }
209 u8::try_from(count).ok()
210 }
211
212 fn validate_component_counts(&self, seg: &Segment<'_>) -> Result<(), EdifactError> {
213 for idx in 0..seg.elements.len() {
214 if let Some(expected) = (self.expected_components)(seg.tag, idx) {
215 let actual = Self::effective_component_count(seg, idx).unwrap_or(0);
216 if actual != expected {
217 return Err(EdifactError::InvalidComponentCount {
218 tag: seg.tag.to_owned(),
219 element_index: idx,
220 expected,
221 actual,
222 offset: seg.span.start,
223 });
224 }
225 }
226 }
227 Ok(())
228 }
229
230 fn validate_code_lists(&self, seg: &Segment<'_>) -> Result<(), EdifactError> {
231 let rules = (self.code_list_rules)(seg.tag);
232
233 for (elem_idx, comp_idx, de) in rules {
234 let value = seg
235 .get_element(*elem_idx)
236 .and_then(|e| e.get_component(*comp_idx))
237 .unwrap_or("");
238 if !value.is_empty() && !(self.is_code_valid)(de, value) {
239 let suggestion = (self.suggest_code)(de, value);
240 return Err(EdifactError::InvalidCodeValue {
241 tag: seg.tag.to_owned(),
242 element_index: *elem_idx,
243 value: value.to_owned(),
244 code_list: (*de).to_owned(),
245 offset: seg.span.start,
246 suggestion,
247 });
248 }
249 }
250
251 Ok(())
252 }
253}
254
255impl DirectoryValidator {
256 fn validate_segment(&self, seg: &Segment<'_>) -> Result<(), EdifactError> {
257 if !self.structure_checks && !self.code_list_checks {
258 return Ok(());
259 }
260
261 let Some(def) = (self.segment_lookup)(seg.tag) else {
262 if self.structure_checks && self.enforce_known_tags {
263 return Err(EdifactError::InvalidSegmentForMessage {
264 tag: seg.tag.to_owned(),
265 message_type: self
266 .message_type
267 .clone()
268 .unwrap_or_else(|| self.directory_id.to_owned()),
269 offset: seg.tag_span.start,
270 });
271 }
272 return Ok(());
273 };
274
275 let max_elements = def.elements.len();
276 let min_elements = def
277 .elements
278 .iter()
279 .rposition(|e| e.status == Status::Mandatory)
280 .map(|idx| idx + 1)
281 .unwrap_or(0);
282 let actual = seg.elements.len();
283
284 if self.structure_checks && (actual < min_elements || actual > max_elements) {
285 return Err(EdifactError::InvalidElementCount {
286 tag: seg.tag.to_owned(),
287 min: min_elements,
288 max: max_elements,
289 actual,
290 offset: seg.span.start,
291 });
292 }
293
294 if self.structure_checks {
295 for element in def
296 .elements
297 .iter()
298 .filter(|e| e.status == Status::Mandatory)
299 {
300 let idx = (element.position as usize).saturating_sub(1);
301 let is_present = seg
302 .elements
303 .get(idx)
304 .is_some_and(|elem| elem.components.iter().any(|c| !c.as_ref().is_empty()));
305 if !is_present {
306 return Err(EdifactError::MissingRequiredElement {
307 tag: seg.tag.to_owned(),
308 element_index: idx,
309 });
310 }
311 }
312 self.validate_component_counts(seg)?;
313
314 if let Some(rule) = self.additional_structure_rule {
315 rule(seg)?;
316 }
317 }
318
319 if self.code_list_checks {
320 self.validate_code_lists(seg)?;
321 }
322
323 Ok(())
324 }
325}
326
327impl Validator for DirectoryValidator {
328 fn set_message_type(&mut self, message_type: Option<&str>) {
329 self.message_type = message_type.map(str::to_owned);
330 }
331
332 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport) {
333 for seg in segments {
334 if let Err(err) = self.validate_segment(seg) {
335 report_error(report, err);
336 }
337 }
338
339 if self.structure_checks {
340 if let Some(message_type) = self.detect_message_type(segments) {
341 for required_tag in Self::required_segments_for(&message_type) {
342 if segments.iter().all(|s| s.tag != *required_tag) {
343 report.add_error(
344 ValidationIssue::new(
345 ValidationSeverity::Error,
346 format!(
347 "required segment {} missing for message type {}",
348 required_tag, message_type
349 ),
350 )
351 .with_segment(*required_tag)
352 .with_suggestion("Add the mandatory segment at the correct position"),
353 );
354 }
355 }
356
357 let seq = Self::required_segments_for(&message_type);
358 let mut last_idx = None;
359 for tag in seq {
360 if let Some(idx) = segments.iter().position(|s| s.tag == *tag) {
361 if let Some(prev) = last_idx {
362 if idx < prev {
363 report.add_error(
364 ValidationIssue::new(
365 ValidationSeverity::Error,
366 format!(
367 "segment sequence violation for message type {}: '{}' appears out of order",
368 message_type, tag
369 ),
370 )
371 .with_segment(*tag)
372 .with_suggestion(
373 "Ensure required segments follow UN/EDIFACT canonical order",
374 ),
375 );
376 }
377 }
378 last_idx = Some(idx);
379 }
380 }
381 }
382 }
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 static TEST_ELEMENTS: &[ElementRef] = &[ElementRef {
391 position: 1,
392 data_element: "C507",
393 status: Status::Mandatory,
394 max_repeat: 1,
395 }];
396
397 static TEST_SEGMENT: SegmentDefinition = SegmentDefinition {
398 tag: "TST",
399 name: "Test segment",
400 elements: TEST_ELEMENTS,
401 };
402
403 fn segment_lookup(tag: &str) -> Option<&'static SegmentDefinition> {
404 match tag {
405 "TST" => Some(&TEST_SEGMENT),
406 _ => None,
407 }
408 }
409
410 fn code_valid(_de: &str, _code: &str) -> bool {
411 true
412 }
413
414 fn suggest_code(_de: &str, _code: &str) -> Option<&'static str> {
415 None
416 }
417
418 fn expected_components(_tag: &str, _idx: usize) -> Option<u8> {
419 None
420 }
421
422 #[test]
423 fn mandatory_composite_present_when_any_component_non_empty() {
424 let input = b"TST+:ABC'";
425 let segments: Vec<_> = crate::from_bytes(input)
426 .collect::<Result<Vec<_>, _>>()
427 .expect("parse should succeed");
428
429 let validator = DirectoryValidator::new(
430 "TEST",
431 segment_lookup,
432 code_valid,
433 suggest_code,
434 expected_components,
435 None,
436 );
437
438 let mut report = ValidationReport::default();
439 validator.validate_batch(&segments, &mut report);
440 assert!(!report.has_errors());
441 }
442
443 fn parse_single(input: &[u8]) -> crate::model::Segment<'static> {
446 let leaked: &'static [u8] = Box::leak(input.to_vec().into_boxed_slice());
450 crate::from_bytes(leaked)
451 .collect::<Result<Vec<_>, _>>()
452 .expect("parse should succeed")
453 .into_iter()
454 .next()
455 .expect("at least one segment")
456 }
457
458 #[test]
459 fn trailing_empty_component_stripped_from_dtm() {
460 let seg = parse_single(b"DTM+137:20200101:'");
464 let count = DirectoryValidator::effective_component_count(&seg, 0);
465 assert_eq!(count, Some(2), "trailing empty component should be stripped");
466 }
467
468 #[test]
469 fn all_empty_components_result_in_zero() {
470 let seg = parse_single(b"NAD+MS++:'");
472 let count = DirectoryValidator::effective_component_count(&seg, 2);
473 assert_eq!(count, Some(0), "all-empty composite should have effective count 0");
474 }
475
476 #[test]
477 fn non_empty_component_not_stripped() {
478 let seg = parse_single(b"DTM+137:20200101:102'");
480 let count = DirectoryValidator::effective_component_count(&seg, 0);
481 assert_eq!(count, Some(3), "no components should be stripped when all non-empty");
482 }
483
484 #[test]
485 fn with_code_list_rules_overrides_base() {
486 fn custom_rules(tag: &str) -> &'static [(usize, usize, &'static str)] {
488 match tag {
489 "TST" => &[(0, 0, "CUSTOM_DE")],
490 _ => &[],
491 }
492 }
493 fn custom_code_valid(_de: &str, code: &str) -> bool {
494 code == "VALID"
495 }
496 fn no_suggestion(_de: &str, _code: &str) -> Option<&'static str> {
497 None
498 }
499
500 let input = b"TST+INVALID'";
501 let segments: Vec<_> = crate::from_bytes(input)
502 .collect::<Result<Vec<_>, _>>()
503 .expect("parse should succeed");
504
505 let validator = DirectoryValidator::new(
506 "TEST",
507 segment_lookup,
508 custom_code_valid,
509 no_suggestion,
510 expected_components,
511 None,
512 )
513 .with_code_list_rules(custom_rules);
514
515 let mut report = ValidationReport::default();
516 validator.validate_batch(&segments, &mut report);
517 assert!(
518 report.has_warnings(),
519 "INVALID is not in the custom code list so validation must warn"
520 );
521 }
522}