1use crate::{EdifactError, Segment, ValidationIssue, ValidationReport, ValidationSeverity};
4use std::any::Any;
5use std::sync::Arc;
6
7#[derive(Clone, Copy)]
28pub struct ValidationRuleContext<'a> {
29 metadata: Option<&'a (dyn Any + Send + Sync)>,
30}
31
32impl<'a> ValidationRuleContext<'a> {
33 pub fn empty() -> Self {
35 Self { metadata: None }
36 }
37
38 pub fn new<T: Any + Send + Sync>(value: &'a T) -> Self {
40 Self {
41 metadata: Some(value as &(dyn Any + Send + Sync)),
42 }
43 }
44
45 pub fn metadata<T: Any + Send + Sync>(&self) -> Option<&T> {
48 self.metadata?.downcast_ref::<T>()
49 }
50
51 pub fn has_metadata(&self) -> bool {
53 self.metadata.is_some()
54 }
55}
56
57impl std::fmt::Debug for ValidationRuleContext<'_> {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 f.debug_struct("ValidationRuleContext")
60 .field("has_metadata", &self.metadata.is_some())
61 .finish()
62 }
63}
64
65pub trait ProfileRule: Send + Sync {
72 fn evaluate(&self, segments: &[Segment<'_>], context: &ValidationRuleContext<'_>) -> Option<ValidationIssue>;
76}
77
78struct ClosureProfileRule<F>(F);
80
81impl<F> ProfileRule for ClosureProfileRule<F>
82where
83 F: for<'a> Fn(&[Segment<'a>], &ValidationRuleContext<'_>) -> Option<ValidationIssue>
84 + Send
85 + Sync,
86{
87 fn evaluate(&self, segments: &[Segment<'_>], context: &ValidationRuleContext<'_>) -> Option<ValidationIssue> {
88 (self.0)(segments, context)
89 }
90}
91
92struct StatelessClosureProfileRule<F>(F);
94
95impl<F> ProfileRule for StatelessClosureProfileRule<F>
96where
97 F: for<'a> Fn(&[Segment<'a>]) -> Option<ValidationIssue> + Send + Sync,
98{
99 fn evaluate(&self, segments: &[Segment<'_>], _context: &ValidationRuleContext<'_>) -> Option<ValidationIssue> {
100 (self.0)(segments)
101 }
102}
103
104pub struct ProfileRulePack {
106 name: String,
107 message_types: Vec<String>,
108 rules: Vec<Arc<dyn ProfileRule + Send + Sync>>,
109 bail_on_first_error: bool,
110}
111
112impl ProfileRulePack {
113 pub fn new(name: impl Into<String>) -> Self {
115 Self {
116 name: name.into(),
117 message_types: Vec::new(),
118 rules: Vec::new(),
119 bail_on_first_error: false,
120 }
121 }
122
123 pub fn name(&self) -> &str {
125 &self.name
126 }
127
128 pub fn message_types(&self) -> &[String] {
130 &self.message_types
131 }
132
133 pub fn rule_count(&self) -> usize {
135 self.rules.len()
136 }
137
138 pub fn for_message_type(mut self, message_type: impl Into<String>) -> Self {
154 let message_type = message_type.into();
155 if !self.message_types.contains(&message_type) {
156 self.message_types.push(message_type);
157 }
158 self
159 }
160
161 pub fn bail_on_first_error(mut self, bail: bool) -> Self {
169 self.bail_on_first_error = bail;
170 self
171 }
172
173 pub fn with_rule_fn<F>(mut self, rule: F) -> Self
181 where
182 F: for<'a> Fn(&[Segment<'a>], &ValidationRuleContext<'_>) -> Option<ValidationIssue>
183 + Send
184 + Sync
185 + 'static,
186 {
187 self.rules.push(Arc::new(ClosureProfileRule(rule)));
188 self
189 }
190
191 pub fn with_stateless_rule_fn<F>(mut self, rule: F) -> Self
196 where
197 F: for<'a> Fn(&[Segment<'a>]) -> Option<ValidationIssue> + Send + Sync + 'static,
198 {
199 self.rules.push(Arc::new(StatelessClosureProfileRule(rule)));
200 self
201 }
202
203 pub fn with_rule(mut self, rule: impl ProfileRule + 'static) -> Self {
205 self.rules.push(Arc::new(rule));
206 self
207 }
208
209 pub fn extend_from(mut self, base: &ProfileRulePack) -> Self {
225 let mut combined = base.rules.clone();
226 combined.append(&mut self.rules);
227 self.rules = combined;
228 for mt in &base.message_types {
229 if !self.message_types.contains(mt) {
230 self.message_types.push(mt.clone());
231 }
232 }
233 self
234 }
235
236 pub fn merge(mut self, mut other: Self) -> Self {
240 for message_type in other.message_types.drain(..) {
241 if !self.message_types.contains(&message_type) {
242 self.message_types.push(message_type);
243 }
244 }
245 self.rules.append(&mut other.rules);
246 self
247 }
248}
249
250impl Validator for ProfileRulePack {
251 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport, context: &ValidationRuleContext<'_>) {
252 let message_type = segments
253 .iter()
254 .find(|segment| segment.tag == "UNH")
255 .and_then(|segment| segment.get_element(1))
256 .and_then(|element| element.get_component(0));
257 if !self.message_types.is_empty()
258 && !message_type.is_some_and(|mt| self.message_types.iter().any(|t| t == mt))
259 {
260 return;
261 }
262
263 for rule in &self.rules {
264 if let Some(issue) = rule.evaluate(segments, context) {
265 let was_error = match issue.severity {
266 ValidationSeverity::Critical | ValidationSeverity::Error => {
267 report.add_error(issue);
268 true
269 }
270 ValidationSeverity::Warning => {
271 report.add_warning(issue);
272 false
273 }
274 ValidationSeverity::Info => {
275 report.add_info(issue);
276 false
277 }
278 };
279 if self.bail_on_first_error && was_error {
280 return;
281 }
282 }
283 }
284 }
285}
286
287impl std::fmt::Debug for ProfileRulePack {
288 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289 f.debug_struct("ProfileRulePack")
290 .field("name", &self.name)
291 .field("message_types", &self.message_types)
292 .field("rule_count", &self.rules.len())
293 .field("bail_on_first_error", &self.bail_on_first_error)
294 .finish()
295 }
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300#[non_exhaustive]
301pub enum ValidationLayer {
302 Structure,
304 CodeList,
306 Profile,
308}
309
310struct LayeredValidator {
311 layer: ValidationLayer,
312 validator: Box<dyn Validator + Send + Sync>,
313}
314
315pub struct ValidationContext {
317 validators: Vec<LayeredValidator>,
318 structure_enabled: bool,
319 code_list_enabled: bool,
320 profile_enabled: bool,
321 message_type: Option<String>,
322 metadata: Option<Arc<dyn Any + Send + Sync>>,
323}
324
325#[must_use = "call `.build()` to produce a `ValidationContext`"]
327pub struct ValidationContextBuilder {
328 inner: ValidationContext,
329}
330
331impl Default for ValidationContextBuilder {
332 fn default() -> Self {
334 Self::new()
335 }
336}
337
338impl ValidationContextBuilder {
339 pub fn new() -> Self {
341 Self {
342 inner: ValidationContext {
343 validators: Vec::new(),
344 structure_enabled: true,
345 code_list_enabled: true,
346 profile_enabled: true,
347 message_type: None,
348 metadata: None,
349 },
350 }
351 }
352
353 pub fn with_metadata<T: Any + Send + Sync + 'static>(mut self, value: T) -> Self {
362 self.inner.metadata = Some(Arc::new(value));
363 self
364 }
365
366 pub fn with_message_type(mut self, message_type: impl Into<String>) -> Self {
368 self.inner.message_type = Some(message_type.into());
369 let configured = self.inner.message_type.as_deref();
370 for layered in &mut self.inner.validators {
371 layered.validator.set_message_type(configured);
372 }
373 self
374 }
375
376 pub fn structure(mut self, enabled: bool) -> Self {
378 self.inner.structure_enabled = enabled;
379 self
380 }
381
382 pub fn code_list(mut self, enabled: bool) -> Self {
384 self.inner.code_list_enabled = enabled;
385 self
386 }
387
388 pub fn profile(mut self, enabled: bool) -> Self {
390 self.inner.profile_enabled = enabled;
391 self
392 }
393
394 pub fn with_validator<V>(mut self, layer: ValidationLayer, mut validator: V) -> Self
396 where
397 V: Validator + 'static,
398 {
399 validator.set_message_type(self.inner.message_type.as_deref());
400 self.inner.validators.push(LayeredValidator {
401 layer,
402 validator: Box::new(validator),
403 });
404 self
405 }
406
407 pub fn with_profile_pack(mut self, mut pack: ProfileRulePack) -> Self {
409 pack.set_message_type(self.inner.message_type.as_deref());
410 self.inner.validators.push(LayeredValidator {
411 layer: ValidationLayer::Profile,
412 validator: Box::new(pack),
413 });
414 self
415 }
416
417 #[must_use = "call `.validate_lenient()` or `.validate_strict()` on the resulting context"]
419 pub fn build(self) -> ValidationContext {
420 self.inner
421 }
422}
423
424impl ValidationContext {
425 pub fn builder() -> ValidationContextBuilder {
427 ValidationContextBuilder::new()
428 }
429
430 pub fn validate_lenient(&self, segments: &[Segment<'_>]) -> ValidationReport {
435 let ctx = self
436 .metadata
437 .as_ref()
438 .map(|arc| ValidationRuleContext {
439 metadata: Some(arc.as_ref() as &(dyn Any + Send + Sync)),
440 })
441 .unwrap_or_else(ValidationRuleContext::empty);
442 self.validate_with_context(segments, &ctx)
443 }
444
445 pub fn validate_lenient_with<T: Any + Send + Sync>(
453 &self,
454 segments: &[Segment<'_>],
455 value: &T,
456 ) -> ValidationReport {
457 let ctx = ValidationRuleContext::new(value);
458 self.validate_with_context(segments, &ctx)
459 }
460
461 pub fn validate_strict(
468 &self,
469 segments: &[Segment<'_>],
470 ) -> Result<ValidationReport, EdifactError> {
471 let report = self.validate_lenient(segments);
472 Self::strict_check(report)
473 }
474
475 pub fn validate_strict_with<T: Any + Send + Sync>(
480 &self,
481 segments: &[Segment<'_>],
482 value: &T,
483 ) -> Result<ValidationReport, EdifactError> {
484 let report = self.validate_lenient_with(segments, value);
485 Self::strict_check(report)
486 }
487
488 fn validate_with_context(
489 &self,
490 segments: &[Segment<'_>],
491 context: &ValidationRuleContext<'_>,
492 ) -> ValidationReport {
493 let mut report = ValidationReport::default();
494 for lv in &self.validators {
495 if self.layer_enabled(lv.layer) {
496 lv.validator.validate_batch(segments, &mut report, context);
497 }
498 }
499 report
500 }
501
502 fn strict_check(report: ValidationReport) -> Result<ValidationReport, EdifactError> {
503 if report.has_errors() {
504 let first_message = report
505 .errors()
506 .first()
507 .map(|e| e.message.clone())
508 .unwrap_or_else(|| "unknown validation failure".to_owned());
509 return Err(EdifactError::ValidationFailed {
510 error_count: report.errors().len(),
511 first_message,
512 });
513 }
514 Ok(report)
515 }
516
517 pub fn message_type(&self) -> Option<&str> {
519 self.message_type.as_deref()
520 }
521
522 fn layer_enabled(&self, layer: ValidationLayer) -> bool {
523 match layer {
524 ValidationLayer::Structure => self.structure_enabled,
525 ValidationLayer::CodeList => self.code_list_enabled,
526 ValidationLayer::Profile => self.profile_enabled,
527 }
528 }
529}
530
531pub trait Validator: Send + Sync {
553 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport, context: &ValidationRuleContext<'_>);
557
558 fn set_message_type(&mut self, _message_type: Option<&str>) {}
560}
561
562pub fn validate_each<F>(segments: &[Segment<'_>], report: &mut ValidationReport, mut f: F)
573where
574 F: FnMut(&Segment<'_>) -> Result<(), EdifactError>,
575{
576 for segment in segments {
577 if let Err(err) = f(segment) {
578 report_error(report, err);
579 }
580 }
581}
582
583pub(crate) fn report_error(report: &mut ValidationReport, err: EdifactError) {
585 let issue = issue_from_error(err);
586 match issue.severity {
587 ValidationSeverity::Critical | ValidationSeverity::Error => report.add_error(issue),
588 ValidationSeverity::Warning => report.add_warning(issue),
589 ValidationSeverity::Info => report.add_info(issue),
590 }
591}
592
593fn issue_from_error(err: EdifactError) -> ValidationIssue {
594 let code = err.stable_code();
595 let mut issue = ValidationIssue::new(severity_for(&err), err.to_string()).with_error_code(code);
596 let default_hint = err.recovery_hint();
597
598 match err {
599 EdifactError::InvalidSegmentForMessage { tag, offset, .. } => {
600 issue = issue.with_segment(tag).with_offset(offset);
601 }
602 EdifactError::InvalidElementCount { tag, offset, .. } => {
603 issue = issue.with_segment(tag).with_offset(offset);
604 }
605 EdifactError::InvalidComponentCount {
606 tag,
607 element_index,
608 offset,
609 ..
610 } => {
611 issue = issue
612 .with_segment(tag)
613 .with_element_index(u8::try_from(element_index).unwrap_or(u8::MAX))
614 .with_offset(offset);
615 }
616 EdifactError::InvalidCodeValue {
617 tag,
618 element_index,
619 offset,
620 suggestion,
621 ..
622 } => {
623 issue = issue
624 .with_segment(tag)
625 .with_element_index(u8::try_from(element_index).unwrap_or(u8::MAX))
626 .with_offset(offset);
627 if let Some(s) = suggestion {
628 issue = issue.with_suggestion(s);
629 }
630 }
631 EdifactError::MissingSegment { tag, .. } => {
632 issue = issue.with_segment(tag);
633 }
634 EdifactError::QualifierMismatch { tag, offset, .. } => {
635 issue = issue
636 .with_segment(tag)
637 .with_element_index(0)
638 .with_offset(offset);
639 }
640 EdifactError::ConditionalRequirementNotMet {
641 tag,
642 element_index,
643 offset,
644 ..
645 } => {
646 issue = issue
647 .with_segment(tag)
648 .with_element_index(u8::try_from(element_index).unwrap_or(u8::MAX))
649 .with_offset(offset);
650 }
651 EdifactError::MissingRequiredElement { tag, element_index } => {
652 issue = issue.with_segment(tag);
653 if let Ok(idx) = u8::try_from(element_index) {
654 issue = issue.with_element_index(idx);
655 }
656 }
657 EdifactError::MissingRequiredComponent {
658 tag,
659 element_index,
660 component_index,
661 } => {
662 issue = issue.with_segment(tag);
663 if let Ok(ei) = u8::try_from(element_index) {
664 issue = issue.with_element_index(ei);
665 }
666 if let Ok(ci) = u8::try_from(component_index) {
667 issue = issue.with_component_index(ci);
668 }
669 }
670 EdifactError::InvalidReleaseSequence { offset }
671 | EdifactError::InvalidDelimiter { offset, .. }
672 | EdifactError::InvalidText { offset }
673 | EdifactError::UnexpectedEof { offset } => {
674 issue = issue.with_offset(offset);
675 }
676 _ => {}
677 }
678
679 if issue.suggestion.is_none() {
680 if let Some(hint) = default_hint {
681 issue = issue.with_suggestion(hint);
682 }
683 }
684
685 issue
686}
687
688fn severity_for(err: &EdifactError) -> ValidationSeverity {
689 match err {
690 EdifactError::InvalidCodeValue { .. } | EdifactError::QualifierMismatch { .. } => {
691 ValidationSeverity::Warning
692 }
693 _ => ValidationSeverity::Error,
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700 use crate::model::Element;
701
702 fn demo_orders_profile_pack() -> ProfileRulePack {
703 ProfileRulePack::new("ORDERS-DEMO")
704 .for_message_type("ORDERS")
705 .with_stateless_rule_fn(|segments| {
706 let bgm = segments.iter().find(|segment| segment.tag == "BGM")?;
707 let document_code = bgm.get_element(0)?.get_component(0)?;
708 (document_code == "220").then(|| {
709 ValidationIssue::new(
710 ValidationSeverity::Error,
711 "profile rule DEMO-P001 violated: BGM document code 220 is rejected in this demo pack",
712 )
713 .with_rule_id("DEMO-P001")
714 .with_segment("BGM")
715 .with_element_index(0)
716 .with_suggestion("Use a different BGM document code in this demo pack")
717 })
718 })
719 .with_stateless_rule_fn(|segments| {
720 let bgm = segments.iter().find(|segment| segment.tag == "BGM")?;
721 let reference = bgm.get_element(1)?.get_component(0)?;
722 (reference == "PO123").then(|| {
723 ValidationIssue::new(
724 ValidationSeverity::Warning,
725 "profile rule DEMO-P002 warning: purchase-order reference PO123 is reserved in this demo pack",
726 )
727 .with_rule_id("DEMO-P002")
728 .with_segment("BGM")
729 .with_element_index(1)
730 .with_suggestion("Use a non-reserved reference in this demo pack")
731 })
732 })
733 }
734
735 struct RejectBgm;
736
737 struct WarnBgm;
738
739 impl Validator for RejectBgm {
740 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport, _context: &ValidationRuleContext<'_>) {
741 validate_each(segments, report, |segment| {
742 if segment.tag == "BGM" {
743 return Err(EdifactError::InvalidSegmentForMessage {
744 tag: "BGM".to_owned(),
745 message_type: "TEST".to_owned(),
746 offset: segment.tag_span.start,
747 });
748 }
749 Ok(())
750 });
751 }
752 }
753
754 impl Validator for WarnBgm {
755 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport, _context: &ValidationRuleContext<'_>) {
756 validate_each(segments, report, |segment| {
757 if segment.tag == "BGM" {
758 return Err(EdifactError::InvalidCodeValue {
759 tag: "BGM".to_owned(),
760 element_index: 0,
761 value: "XXX".to_owned(),
762 code_list: "1001".to_owned(),
763 offset: segment.span.start,
764 suggestion: None,
765 });
766 }
767 Ok(())
768 });
769 }
770 }
771
772 fn test_segment(tag: &'static str) -> Segment<'static> {
773 Segment {
774 tag,
775 span: crate::Span::new(0, 0),
776 tag_span: crate::Span::new(0, 0),
777 elements: vec![Element::of(&["x"])],
778 }
779 }
780
781 #[test]
782 fn lenient_collects_issues() {
783 let segments = vec![test_segment("UNH"), test_segment("BGM")];
784 let mut report = ValidationReport::default();
785 RejectBgm.validate_batch(&segments, &mut report, &ValidationRuleContext::empty());
786 assert!(report.has_errors());
787 assert_eq!(report.errors().len(), 1);
788 }
789
790 #[test]
791 fn strict_fails_on_errors() {
792 let segments = vec![test_segment("BGM")];
793 let mut report = ValidationReport::default();
794 RejectBgm.validate_batch(&segments, &mut report, &ValidationRuleContext::empty());
795 assert!(report.has_errors());
796 assert_eq!(report.errors().len(), 1);
797 }
798
799 #[test]
800 fn context_builder_respects_layer_toggles() {
801 let segments = vec![test_segment("BGM")];
802 let ctx = ValidationContext::builder()
803 .structure(false)
804 .with_validator(ValidationLayer::Structure, RejectBgm)
805 .with_validator(ValidationLayer::CodeList, WarnBgm)
806 .build();
807
808 let report = ctx.validate_lenient(&segments);
809 assert!(!report.has_errors());
810 assert_eq!(report.warnings().len(), 1);
811 }
812
813 #[test]
814 fn context_strict_fails_when_structure_enabled() {
815 let segments = vec![test_segment("BGM")];
816 let ctx = ValidationContext::builder()
817 .with_message_type("ORDERS")
818 .with_validator(ValidationLayer::Structure, RejectBgm)
819 .build();
820
821 assert_eq!(ctx.message_type(), Some("ORDERS"));
822 let result = ctx.validate_strict(&segments);
823 assert!(matches!(result, Err(EdifactError::ValidationFailed { .. })));
824 }
825
826 #[test]
827 fn report_error_applies_default_recovery_hint() {
828 let mut report = ValidationReport::default();
829 report_error(
830 &mut report,
831 EdifactError::InvalidReleaseSequence { offset: 9 },
832 );
833
834 let issue = report
835 .errors()
836 .first()
837 .expect("expected one issue in the report");
838 let hint = issue
839 .suggestion
840 .as_deref()
841 .expect("expected default hint to be set");
842 assert!(hint.contains("Release character"));
843 assert_eq!(issue.error_code, Some("E019"));
844 }
845
846 #[test]
847 fn missing_required_component_maps_metadata_to_issue() {
848 let mut report = ValidationReport::default();
849 report_error(
850 &mut report,
851 EdifactError::MissingRequiredComponent {
852 tag: "BGM".to_owned(),
853 element_index: 2,
854 component_index: 1,
855 },
856 );
857
858 let issue = report
859 .errors()
860 .first()
861 .expect("expected one issue");
862 assert_eq!(issue.error_code, Some("E021"));
863 assert_eq!(issue.segment_tag.as_deref(), Some("BGM"));
864 assert_eq!(issue.element_index, Some(2));
865 assert_eq!(issue.component_index, Some(1));
866 }
867
868 #[test]
869 fn profile_pack_lenient_collects_profile_rule_issues() {
870 let input = b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'";
871 let segments = crate::from_bytes(input)
872 .collect::<Result<Vec<_>, _>>()
873 .expect("expected parse success");
874
875 let ctx = ValidationContext::builder()
876 .with_profile_pack(demo_orders_profile_pack())
877 .build();
878
879 let report = ctx.validate_lenient(&segments);
880 assert!(report.has_errors());
881 assert!(
882 report
883 .errors()
884 .iter()
885 .any(|issue| issue.rule_id.as_deref() == Some("DEMO-P001"))
886 );
887 assert!(
888 report
889 .warnings()
890 .iter()
891 .any(|issue| issue.rule_id.as_deref() == Some("DEMO-P002"))
892 );
893 }
894
895 #[test]
896 fn profile_pack_strict_fails_when_profile_errors_exist() {
897 let input = b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'";
898 let segments = crate::from_bytes(input)
899 .collect::<Result<Vec<_>, _>>()
900 .expect("expected parse success");
901
902 let ctx = ValidationContext::builder()
903 .with_profile_pack(demo_orders_profile_pack())
904 .build();
905 let result = ctx.validate_strict(&segments);
906 assert!(matches!(result, Err(EdifactError::ValidationFailed { .. })));
907 }
908}