1use crate::{EdifactError, Segment, ValidationIssue, ValidationReport, ValidationSeverity};
4
5pub trait ProfileRule: Send + Sync {
10 fn evaluate(&self, segments: &[Segment<'_>]) -> Option<ValidationIssue>;
14}
15
16struct ClosureProfileRule<F>(F);
17
18impl<F> ProfileRule for ClosureProfileRule<F>
19where
20 F: for<'a> Fn(&[Segment<'a>]) -> Option<ValidationIssue> + Send + Sync,
21{
22 fn evaluate(&self, segments: &[Segment<'_>]) -> Option<ValidationIssue> {
23 (self.0)(segments)
24 }
25}
26
27pub struct ProfileRulePack {
29 name: String,
30 message_types: Vec<String>,
31 rules: Vec<Box<dyn ProfileRule + Send + Sync>>,
32}
33
34impl ProfileRulePack {
35 pub fn new(name: impl Into<String>) -> Self {
37 Self {
38 name: name.into(),
39 message_types: Vec::new(),
40 rules: Vec::new(),
41 }
42 }
43
44 pub fn builder(name: impl Into<String>) -> Self {
55 Self::new(name)
56 }
57
58 pub fn name(&self) -> &str {
60 &self.name
61 }
62
63 pub fn message_types(&self) -> &[String] {
65 &self.message_types
66 }
67
68 pub fn rule_count(&self) -> usize {
70 self.rules.len()
71 }
72
73 pub fn for_message_type(mut self, message_type: impl Into<String>) -> Self {
89 let message_type = message_type.into();
90 if !self.message_types.contains(&message_type) {
91 self.message_types.push(message_type);
92 }
93 self
94 }
95
96 pub fn with_rule_fn<F>(mut self, rule: F) -> Self
98 where
99 F: for<'a> Fn(&[Segment<'a>]) -> Option<ValidationIssue> + Send + Sync + 'static,
100 {
101 self.rules.push(Box::new(ClosureProfileRule(rule)));
102 self
103 }
104
105 pub fn with_rule(mut self, rule: impl ProfileRule + 'static) -> Self {
107 self.rules.push(Box::new(rule));
108 self
109 }
110
111 pub fn merge(mut self, mut other: Self) -> Self {
113 for message_type in other.message_types.drain(..) {
114 if !self.message_types.contains(&message_type) {
115 self.message_types.push(message_type);
116 }
117 }
118 self.rules.append(&mut other.rules);
119 self
120 }
121}
122
123impl Validator for ProfileRulePack {
124 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport) {
125 let message_type = segments
126 .iter()
127 .find(|segment| segment.tag == "UNH")
128 .and_then(|segment| segment.get_element(1))
129 .and_then(|element| element.get_component(0));
130 if !self.message_types.is_empty()
131 && !message_type.is_some_and(|mt| self.message_types.iter().any(|t| t == mt))
132 {
133 return;
134 }
135
136 for rule in &self.rules {
137 if let Some(issue) = rule.evaluate(segments) {
138 match issue.severity {
139 ValidationSeverity::Critical | ValidationSeverity::Error => {
140 report.add_error(issue);
141 }
142 ValidationSeverity::Warning => {
143 report.add_warning(issue);
144 }
145 ValidationSeverity::Info => {
146 report.add_info(issue);
147 }
148 }
149 }
150 }
151 }
152}
153
154impl std::fmt::Debug for ProfileRulePack {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 f.debug_struct("ProfileRulePack")
157 .field("name", &self.name)
158 .field("message_types", &self.message_types)
159 .field("rule_count", &self.rules.len())
160 .finish()
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166#[non_exhaustive]
167pub enum ValidationLayer {
168 Structure,
170 CodeList,
172 Profile,
174}
175
176struct LayeredValidator {
177 layer: ValidationLayer,
178 validator: Box<dyn Validator + Send + Sync>,
179}
180
181pub struct ValidationContext {
183 validators: Vec<LayeredValidator>,
184 structure_enabled: bool,
185 code_list_enabled: bool,
186 profile_enabled: bool,
187 message_type: Option<String>,
188}
189
190#[must_use = "call `.build()` to produce a `ValidationContext`"]
192pub struct ValidationContextBuilder {
193 inner: ValidationContext,
194}
195
196impl Default for ValidationContextBuilder {
197 fn default() -> Self {
199 Self::new()
200 }
201}
202
203impl ValidationContextBuilder {
204 pub fn new() -> Self {
206 Self {
207 inner: ValidationContext {
208 validators: Vec::new(),
209 structure_enabled: true,
210 code_list_enabled: true,
211 profile_enabled: true,
212 message_type: None,
213 },
214 }
215 }
216
217 pub fn with_message_type(mut self, message_type: impl Into<String>) -> Self {
219 self.inner.message_type = Some(message_type.into());
220 let configured = self.inner.message_type.as_deref();
221 for layered in &mut self.inner.validators {
222 layered.validator.set_message_type(configured);
223 }
224 self
225 }
226
227 pub fn structure(mut self, enabled: bool) -> Self {
229 self.inner.structure_enabled = enabled;
230 self
231 }
232
233 pub fn code_list(mut self, enabled: bool) -> Self {
235 self.inner.code_list_enabled = enabled;
236 self
237 }
238
239 pub fn profile(mut self, enabled: bool) -> Self {
241 self.inner.profile_enabled = enabled;
242 self
243 }
244
245 pub fn with_validator<V>(mut self, layer: ValidationLayer, mut validator: V) -> Self
247 where
248 V: Validator + 'static,
249 {
250 validator.set_message_type(self.inner.message_type.as_deref());
251 self.inner.validators.push(LayeredValidator {
252 layer,
253 validator: Box::new(validator),
254 });
255 self
256 }
257
258 pub fn with_profile_pack(mut self, mut pack: ProfileRulePack) -> Self {
260 pack.set_message_type(self.inner.message_type.as_deref());
261 self.inner.validators.push(LayeredValidator {
262 layer: ValidationLayer::Profile,
263 validator: Box::new(pack),
264 });
265 self
266 }
267
268 #[must_use = "call `.validate_lenient()` or `.validate_strict()` on the resulting context"]
270 pub fn build(self) -> ValidationContext {
271 self.inner
272 }
273}
274
275impl ValidationContext {
276 pub fn builder() -> ValidationContextBuilder {
278 ValidationContextBuilder::new()
279 }
280
281 pub fn validate_lenient(&self, segments: &[Segment<'_>]) -> ValidationReport {
283 let mut report = ValidationReport::default();
284 for lv in &self.validators {
285 if self.layer_enabled(lv.layer) {
286 lv.validator.validate_batch(segments, &mut report);
287 }
288 }
289 report
290 }
291
292 pub fn validate_strict(
299 &self,
300 segments: &[Segment<'_>],
301 ) -> Result<ValidationReport, EdifactError> {
302 let report = self.validate_lenient(segments);
303 if report.has_errors() {
304 let first_message = report
305 .errors
306 .first()
307 .map(|e| e.message.clone())
308 .unwrap_or_else(|| "unknown validation failure".to_owned());
309 return Err(EdifactError::ValidationFailed {
310 error_count: report.errors.len(),
311 first_message,
312 });
313 }
314 Ok(report)
315 }
316
317 pub fn message_type(&self) -> Option<&str> {
319 self.message_type.as_deref()
320 }
321
322 fn layer_enabled(&self, layer: ValidationLayer) -> bool {
323 match layer {
324 ValidationLayer::Structure => self.structure_enabled,
325 ValidationLayer::CodeList => self.code_list_enabled,
326 ValidationLayer::Profile => self.profile_enabled,
327 }
328 }
329}
330
331pub trait Validator: Send + Sync {
349 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport);
351
352 fn set_message_type(&mut self, _message_type: Option<&str>) {}
354}
355
356pub fn validate_each<F>(segments: &[Segment<'_>], report: &mut ValidationReport, mut f: F)
367where
368 F: FnMut(&Segment<'_>) -> Result<(), EdifactError>,
369{
370 for segment in segments {
371 if let Err(err) = f(segment) {
372 report_error(report, err);
373 }
374 }
375}
376
377pub(crate) fn report_error(report: &mut ValidationReport, err: EdifactError) {
379 let issue = issue_from_error(err);
380 match issue.severity {
381 ValidationSeverity::Critical | ValidationSeverity::Error => report.add_error(issue),
382 ValidationSeverity::Warning => report.add_warning(issue),
383 ValidationSeverity::Info => report.add_info(issue),
384 }
385}
386
387fn issue_from_error(err: EdifactError) -> ValidationIssue {
388 let code = err.stable_code();
389 let mut issue = ValidationIssue::new(severity_for(&err), err.to_string()).with_error_code(code);
390 let default_hint = err.recovery_hint();
391
392 match err {
393 EdifactError::InvalidSegmentForMessage { tag, offset, .. } => {
394 issue = issue.with_segment(tag).with_offset(offset);
395 }
396 EdifactError::InvalidElementCount { tag, offset, .. } => {
397 issue = issue.with_segment(tag).with_offset(offset);
398 }
399 EdifactError::InvalidComponentCount {
400 tag,
401 element_index,
402 offset,
403 ..
404 } => {
405 issue = issue
406 .with_segment(tag)
407 .with_element_index(u8::try_from(element_index).unwrap_or(u8::MAX))
408 .with_offset(offset);
409 }
410 EdifactError::InvalidCodeValue {
411 tag,
412 element_index,
413 offset,
414 suggestion,
415 ..
416 } => {
417 issue = issue
418 .with_segment(tag)
419 .with_element_index(u8::try_from(element_index).unwrap_or(u8::MAX))
420 .with_offset(offset);
421 if let Some(s) = suggestion {
422 issue = issue.with_suggestion(s);
423 }
424 }
425 EdifactError::MissingSegment { tag, .. } => {
426 issue = issue.with_segment(tag);
427 }
428 EdifactError::QualifierMismatch { tag, offset, .. } => {
429 issue = issue
430 .with_segment(tag)
431 .with_element_index(0)
432 .with_offset(offset);
433 }
434 EdifactError::ConditionalRequirementNotMet {
435 tag,
436 element_index,
437 offset,
438 ..
439 } => {
440 issue = issue
441 .with_segment(tag)
442 .with_element_index(u8::try_from(element_index).unwrap_or(u8::MAX))
443 .with_offset(offset);
444 }
445 EdifactError::MissingRequiredElement { tag, element_index } => {
446 issue = issue.with_segment(tag);
447 if let Ok(idx) = u8::try_from(element_index) {
448 issue = issue.with_element_index(idx);
449 }
450 }
451 EdifactError::MissingRequiredComponent {
452 tag,
453 element_index,
454 component_index,
455 } => {
456 issue = issue.with_segment(tag);
457 if let Ok(ei) = u8::try_from(element_index) {
458 issue = issue.with_element_index(ei);
459 }
460 if let Ok(ci) = u8::try_from(component_index) {
461 issue = issue.with_component_index(ci);
462 }
463 }
464 EdifactError::InvalidReleaseSequence { offset }
465 | EdifactError::InvalidDelimiter { offset, .. }
466 | EdifactError::InvalidText { offset }
467 | EdifactError::UnexpectedEof { offset } => {
468 issue = issue.with_offset(offset);
469 }
470 _ => {}
471 }
472
473 if issue.suggestion.is_none() {
474 if let Some(hint) = default_hint {
475 issue = issue.with_suggestion(hint);
476 }
477 }
478
479 issue
480}
481
482fn severity_for(err: &EdifactError) -> ValidationSeverity {
483 match err {
484 EdifactError::InvalidCodeValue { .. } | EdifactError::QualifierMismatch { .. } => {
485 ValidationSeverity::Warning
486 }
487 _ => ValidationSeverity::Error,
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494 use crate::model::Element;
495
496 fn demo_orders_profile_pack() -> ProfileRulePack {
497 ProfileRulePack::builder("ORDERS-DEMO")
498 .for_message_type("ORDERS")
499 .with_rule_fn(|segments| {
500 let bgm = segments.iter().find(|segment| segment.tag == "BGM")?;
501 let document_code = bgm.get_element(0)?.get_component(0)?;
502 (document_code == "220").then(|| {
503 ValidationIssue::new(
504 ValidationSeverity::Error,
505 "profile rule DEMO-P001 violated: BGM document code 220 is rejected in this demo pack",
506 )
507 .with_rule_id("DEMO-P001")
508 .with_segment("BGM")
509 .with_element_index(0)
510 .with_suggestion("Use a different BGM document code in this demo pack")
511 })
512 })
513 .with_rule_fn(|segments| {
514 let bgm = segments.iter().find(|segment| segment.tag == "BGM")?;
515 let reference = bgm.get_element(1)?.get_component(0)?;
516 (reference == "PO123").then(|| {
517 ValidationIssue::new(
518 ValidationSeverity::Warning,
519 "profile rule DEMO-P002 warning: purchase-order reference PO123 is reserved in this demo pack",
520 )
521 .with_rule_id("DEMO-P002")
522 .with_segment("BGM")
523 .with_element_index(1)
524 .with_suggestion("Use a non-reserved reference in this demo pack")
525 })
526 })
527 }
528
529 struct RejectBgm;
530
531 struct WarnBgm;
532
533 impl Validator for RejectBgm {
534 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport) {
535 validate_each(segments, report, |segment| {
536 if segment.tag == "BGM" {
537 return Err(EdifactError::InvalidSegmentForMessage {
538 tag: "BGM".to_owned(),
539 message_type: "TEST".to_owned(),
540 offset: segment.tag_span.start,
541 });
542 }
543 Ok(())
544 });
545 }
546 }
547
548 impl Validator for WarnBgm {
549 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport) {
550 validate_each(segments, report, |segment| {
551 if segment.tag == "BGM" {
552 return Err(EdifactError::InvalidCodeValue {
553 tag: "BGM".to_owned(),
554 element_index: 0,
555 value: "XXX".to_owned(),
556 code_list: "1001".to_owned(),
557 offset: segment.span.start,
558 suggestion: None,
559 });
560 }
561 Ok(())
562 });
563 }
564 }
565
566 fn test_segment(tag: &'static str) -> Segment<'static> {
567 Segment {
568 tag,
569 span: crate::Span::new(0, 0),
570 tag_span: crate::Span::new(0, 0),
571 elements: vec![Element::of(&["x"])],
572 }
573 }
574
575 #[test]
576 fn lenient_collects_issues() {
577 let segments = vec![test_segment("UNH"), test_segment("BGM")];
578 let mut report = ValidationReport::default();
579 RejectBgm.validate_batch(&segments, &mut report);
580 assert!(report.has_errors());
581 assert_eq!(report.errors.len(), 1);
582 }
583
584 #[test]
585 fn strict_fails_on_errors() {
586 let segments = vec![test_segment("BGM")];
587 let mut report = ValidationReport::default();
588 RejectBgm.validate_batch(&segments, &mut report);
589 assert!(report.has_errors());
590 assert_eq!(report.errors.len(), 1);
591 }
592
593 #[test]
594 fn context_builder_respects_layer_toggles() {
595 let segments = vec![test_segment("BGM")];
596 let ctx = ValidationContext::builder()
597 .structure(false)
598 .with_validator(ValidationLayer::Structure, RejectBgm)
599 .with_validator(ValidationLayer::CodeList, WarnBgm)
600 .build();
601
602 let report = ctx.validate_lenient(&segments);
603 assert!(!report.has_errors());
604 assert_eq!(report.warnings.len(), 1);
605 }
606
607 #[test]
608 fn context_strict_fails_when_structure_enabled() {
609 let segments = vec![test_segment("BGM")];
610 let ctx = ValidationContext::builder()
611 .with_message_type("ORDERS")
612 .with_validator(ValidationLayer::Structure, RejectBgm)
613 .build();
614
615 assert_eq!(ctx.message_type(), Some("ORDERS"));
616 let result = ctx.validate_strict(&segments);
617 assert!(matches!(result, Err(EdifactError::ValidationFailed { .. })));
618 }
619
620 #[test]
621 fn report_error_applies_default_recovery_hint() {
622 let mut report = ValidationReport::default();
623 report_error(
624 &mut report,
625 EdifactError::InvalidReleaseSequence { offset: 9 },
626 );
627
628 let issue = report
629 .errors
630 .first()
631 .expect("expected one issue in the report");
632 let hint = issue
633 .suggestion
634 .as_deref()
635 .expect("expected default hint to be set");
636 assert!(hint.contains("Release character"));
637 assert_eq!(issue.error_code, Some("E019"));
638 }
639
640 #[test]
641 fn missing_required_component_maps_metadata_to_issue() {
642 let mut report = ValidationReport::default();
643 report_error(
644 &mut report,
645 EdifactError::MissingRequiredComponent {
646 tag: "BGM".to_owned(),
647 element_index: 2,
648 component_index: 1,
649 },
650 );
651
652 let issue = report
653 .errors
654 .first()
655 .expect("expected one issue");
656 assert_eq!(issue.error_code, Some("E021"));
657 assert_eq!(issue.segment_tag.as_deref(), Some("BGM"));
658 assert_eq!(issue.element_index, Some(2));
659 assert_eq!(issue.component_index, Some(1));
660 }
661
662 #[test]
663 fn profile_pack_lenient_collects_profile_rule_issues() {
664 let input = b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'";
665 let segments = crate::from_bytes(input)
666 .collect::<Result<Vec<_>, _>>()
667 .expect("expected parse success");
668
669 let ctx = ValidationContext::builder()
670 .with_profile_pack(demo_orders_profile_pack())
671 .build();
672
673 let report = ctx.validate_lenient(&segments);
674 assert!(report.has_errors());
675 assert!(
676 report
677 .errors
678 .iter()
679 .any(|issue| issue.rule_id.as_deref() == Some("DEMO-P001"))
680 );
681 assert!(
682 report
683 .warnings
684 .iter()
685 .any(|issue| issue.rule_id.as_deref() == Some("DEMO-P002"))
686 );
687 }
688
689 #[test]
690 fn profile_pack_strict_fails_when_profile_errors_exist() {
691 let input = b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'";
692 let segments = crate::from_bytes(input)
693 .collect::<Result<Vec<_>, _>>()
694 .expect("expected parse success");
695
696 let ctx = ValidationContext::builder()
697 .with_profile_pack(demo_orders_profile_pack())
698 .build();
699 let result = ctx.validate_strict(&segments);
700 assert!(matches!(result, Err(EdifactError::ValidationFailed { .. })));
701 }
702}