1use crate::{
4 EdifactError, OwnedSegment, Segment, ValidationIssue, ValidationReport, ValidationSeverity,
5};
6use std::any::Any;
7use std::sync::Arc;
8
9#[derive(Clone, Copy)]
30pub struct ValidationRuleContext<'a> {
31 metadata: Option<&'a (dyn Any + Send + Sync)>,
32}
33
34impl<'a> ValidationRuleContext<'a> {
35 pub fn empty() -> Self {
37 Self { metadata: None }
38 }
39
40 pub fn new<T: Any + Send + Sync>(value: &'a T) -> Self {
42 Self {
43 metadata: Some(value as &(dyn Any + Send + Sync)),
44 }
45 }
46
47 pub fn metadata<T: Any + Send + Sync>(&self) -> Option<&T> {
50 self.metadata?.downcast_ref::<T>()
51 }
52
53 pub fn has_metadata(&self) -> bool {
55 self.metadata.is_some()
56 }
57}
58
59impl std::fmt::Debug for ValidationRuleContext<'_> {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.debug_struct("ValidationRuleContext")
62 .field("has_metadata", &self.metadata.is_some())
63 .finish()
64 }
65}
66
67pub trait ProfileRule: Send + Sync {
74 fn evaluate(
78 &self,
79 segments: &[Segment<'_>],
80 context: &ValidationRuleContext<'_>,
81 ) -> Option<ValidationIssue>;
82}
83
84struct ClosureProfileRule<F>(F);
86
87impl<F> ProfileRule for ClosureProfileRule<F>
88where
89 F: for<'a> Fn(&[Segment<'a>], &ValidationRuleContext<'_>) -> Option<ValidationIssue>
90 + Send
91 + Sync,
92{
93 fn evaluate(
94 &self,
95 segments: &[Segment<'_>],
96 context: &ValidationRuleContext<'_>,
97 ) -> Option<ValidationIssue> {
98 (self.0)(segments, context)
99 }
100}
101
102struct StatelessClosureProfileRule<F>(F);
104
105impl<F> ProfileRule for StatelessClosureProfileRule<F>
106where
107 F: for<'a> Fn(&[Segment<'a>]) -> Option<ValidationIssue> + Send + Sync,
108{
109 fn evaluate(
110 &self,
111 segments: &[Segment<'_>],
112 _context: &ValidationRuleContext<'_>,
113 ) -> Option<ValidationIssue> {
114 (self.0)(segments)
115 }
116}
117
118struct NamedRule {
124 id: Option<Arc<str>>,
128 rule: Arc<dyn ProfileRule + Send + Sync>,
129}
130
131impl Clone for NamedRule {
132 fn clone(&self) -> Self {
133 Self {
134 id: self.id.clone(),
135 rule: Arc::clone(&self.rule),
136 }
137 }
138}
139
140pub struct ProfileRulePack {
142 name: String,
143 message_types: std::collections::BTreeSet<String>,
150 release: Option<String>,
154 rules: Vec<NamedRule>,
155 bail_on_first_error: bool,
156}
157
158impl ProfileRulePack {
159 pub fn new(name: impl Into<String>) -> Self {
161 Self {
162 name: name.into(),
163 message_types: std::collections::BTreeSet::new(),
164 release: None,
165 rules: Vec::new(),
166 bail_on_first_error: false,
167 }
168 }
169
170 pub fn name(&self) -> &str {
172 &self.name
173 }
174
175 pub fn message_types(&self) -> impl Iterator<Item = &str> {
177 self.message_types.iter().map(|s| s.as_str())
178 }
179
180 pub fn rule_count(&self) -> usize {
182 self.rules.len()
183 }
184
185 pub fn rule_ids(&self) -> impl Iterator<Item = &str> {
189 self.rules.iter().filter_map(|r| r.id.as_deref())
190 }
191
192 pub fn release(&self) -> Option<&str> {
196 self.release.as_deref()
197 }
198
199 pub fn for_message_type(mut self, message_type: impl Into<String>) -> Self {
215 self.message_types.insert(message_type.into());
216 self
217 }
218
219 pub fn for_release(mut self, release: impl Into<String>) -> Self {
234 self.release = Some(release.into());
235 self
236 }
237
238 pub fn bail_on_first_error(mut self, bail: bool) -> Self {
246 self.bail_on_first_error = bail;
247 self
248 }
249
250 pub fn with_rule_fn<F>(mut self, rule: F) -> Self
258 where
259 F: for<'a> Fn(&[Segment<'a>], &ValidationRuleContext<'_>) -> Option<ValidationIssue>
260 + Send
261 + Sync
262 + 'static,
263 {
264 self.rules.push(NamedRule {
265 id: None,
266 rule: Arc::new(ClosureProfileRule(rule)),
267 });
268 self
269 }
270
271 pub fn with_named_rule_fn<F>(mut self, id: impl Into<Arc<str>>, rule: F) -> Self
277 where
278 F: for<'a> Fn(&[Segment<'a>], &ValidationRuleContext<'_>) -> Option<ValidationIssue>
279 + Send
280 + Sync
281 + 'static,
282 {
283 self.rules.push(NamedRule {
284 id: Some(id.into()),
285 rule: Arc::new(ClosureProfileRule(rule)),
286 });
287 self
288 }
289
290 pub fn with_stateless_rule_fn<F>(mut self, rule: F) -> Self
295 where
296 F: for<'a> Fn(&[Segment<'a>]) -> Option<ValidationIssue> + Send + Sync + 'static,
297 {
298 self.rules.push(NamedRule {
299 id: None,
300 rule: Arc::new(StatelessClosureProfileRule(rule)),
301 });
302 self
303 }
304
305 pub fn with_named_stateless_rule_fn<F>(mut self, id: impl Into<Arc<str>>, rule: F) -> Self
309 where
310 F: for<'a> Fn(&[Segment<'a>]) -> Option<ValidationIssue> + Send + Sync + 'static,
311 {
312 self.rules.push(NamedRule {
313 id: Some(id.into()),
314 rule: Arc::new(StatelessClosureProfileRule(rule)),
315 });
316 self
317 }
318
319 pub fn with_rule(mut self, rule: impl ProfileRule + 'static) -> Self {
321 self.rules.push(NamedRule {
322 id: None,
323 rule: Arc::new(rule),
324 });
325 self
326 }
327
328 pub fn with_named_rule(
332 mut self,
333 id: impl Into<Arc<str>>,
334 rule: impl ProfileRule + 'static,
335 ) -> Self {
336 self.rules.push(NamedRule {
337 id: Some(id.into()),
338 rule: Arc::new(rule),
339 });
340 self
341 }
342
343 pub fn extend_from(mut self, base: &ProfileRulePack) -> Result<Self, EdifactError> {
362 let mut combined = base.rules.clone();
363 combined.append(&mut self.rules);
364 self.rules = combined;
365 for mt in &base.message_types {
366 self.message_types.insert(mt.clone());
367 }
368 self.release = merge_release_scopes(self.release.take(), base.release.clone())?;
369 Ok(self)
370 }
371
372 pub fn merge(mut self, mut other: Self) -> Result<Self, EdifactError> {
380 self.message_types.append(&mut other.message_types);
381 self.release = merge_release_scopes(self.release.take(), other.release.take())?;
382 self.rules.append(&mut other.rules);
383 Ok(self)
384 }
385
386 pub fn merge_with_override(mut self, mut other: Self) -> Result<Self, EdifactError> {
414 let mut id_to_index: std::collections::HashMap<Arc<str>, usize> = Default::default();
416 for (idx, rule) in self.rules.iter().enumerate() {
417 if let Some(id) = &rule.id {
418 id_to_index.insert(id.clone(), idx);
419 }
420 }
421
422 let mut replacements: Vec<(usize, NamedRule)> = Vec::new();
424 let mut to_append = Vec::new();
425
426 for other_rule in other.rules.drain(..) {
427 if let Some(id) = &other_rule.id {
428 if let Some(&idx) = id_to_index.get(id) {
429 replacements.push((idx, other_rule));
430 } else {
431 to_append.push(other_rule);
432 }
433 } else {
434 to_append.push(other_rule);
435 }
436 }
437
438 for (idx, rule) in replacements {
440 if idx < self.rules.len() {
441 self.rules[idx] = rule;
442 }
443 }
444
445 self.rules.append(&mut to_append);
447
448 self.message_types.append(&mut other.message_types);
449 self.release = merge_release_scopes(self.release.take(), other.release.take())?;
450 Ok(self)
451 }
452}
453
454fn merge_release_scopes(
455 current: Option<String>,
456 incoming: Option<String>,
457) -> Result<Option<String>, EdifactError> {
458 match (current, incoming) {
459 (Some(current), Some(incoming)) => {
460 if current != incoming {
462 return Err(EdifactError::IncompatibleReleaseScopes { current, incoming });
463 }
464 Ok(Some(current))
465 }
466 (current @ Some(_), None) => Ok(current),
467 (None, incoming) => Ok(incoming),
468 }
469}
470
471impl Validator for ProfileRulePack {
472 fn validate_batch(
473 &self,
474 segments: &[Segment<'_>],
475 report: &mut ValidationReport,
476 context: &ValidationRuleContext<'_>,
477 ) {
478 let unh = segments.iter().find(|segment| segment.tag == "UNH");
479
480 let message_type = unh
482 .and_then(|s| s.get_element(1))
483 .and_then(|e| e.get_component(0));
484 if !self.message_types.is_empty()
485 && !message_type.is_some_and(|mt| self.message_types.contains(mt))
486 {
487 return;
488 }
489
490 if let Some(bound_release) = &self.release {
493 let msg_association = unh
494 .and_then(|s| s.get_element(1))
495 .and_then(|e| e.get_component(4));
496 if msg_association != Some(bound_release.as_str()) {
497 return;
498 }
499 }
500
501 for named in &self.rules {
502 if let Some(issue) = named.rule.evaluate(segments, context) {
503 let was_error = match issue.severity {
504 ValidationSeverity::Critical | ValidationSeverity::Error => {
505 report.add_error(issue);
506 true
507 }
508 ValidationSeverity::Warning => {
509 report.add_warning(issue);
510 false
511 }
512 ValidationSeverity::Info => {
513 report.add_info(issue);
514 false
515 }
516 };
517 if self.bail_on_first_error && was_error {
518 return;
519 }
520 }
521 }
522 }
523}
524
525impl std::fmt::Debug for ProfileRulePack {
526 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
527 f.debug_struct("ProfileRulePack")
528 .field("name", &self.name)
529 .field("message_types", &self.message_types)
530 .field("release", &self.release)
531 .field("rule_count", &self.rules.len())
532 .field("bail_on_first_error", &self.bail_on_first_error)
533 .finish()
534 }
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, Eq)]
539#[non_exhaustive]
540pub enum ValidationLayer {
541 Envelope,
543 Structure,
545 CodeList,
547 Profile,
549}
550
551struct LayeredValidator {
552 layer: ValidationLayer,
553 validator: Box<dyn Validator + Send + Sync>,
554}
555
556pub struct ValidationContext {
558 validators: Vec<LayeredValidator>,
559 envelope_enabled: bool,
560 structure_enabled: bool,
561 code_list_enabled: bool,
562 profile_enabled: bool,
563 message_type: Option<String>,
564 message_ref: Option<String>,
566 metadata: Option<Arc<dyn Any + Send + Sync>>,
567}
568
569#[must_use = "call `.build()` to produce a `ValidationContext`"]
571pub struct ValidationContextBuilder {
572 inner: ValidationContext,
573}
574
575impl Default for ValidationContextBuilder {
576 fn default() -> Self {
578 Self::new()
579 }
580}
581
582impl ValidationContextBuilder {
583 pub fn new() -> Self {
585 Self {
586 inner: ValidationContext {
587 validators: Vec::new(),
588 envelope_enabled: false,
589 structure_enabled: true,
590 code_list_enabled: true,
591 profile_enabled: true,
592 message_type: None,
593 message_ref: None,
594 metadata: None,
595 },
596 }
597 }
598
599 pub fn with_metadata<T: Any + Send + Sync + 'static>(mut self, value: T) -> Self {
608 self.inner.metadata = Some(Arc::new(value));
609 self
610 }
611
612 pub fn with_message_ref(mut self, message_ref: impl Into<String>) -> Self {
619 self.inner.message_ref = Some(message_ref.into());
620 self
621 }
622
623 pub fn with_message_type(mut self, message_type: impl Into<String>) -> Self {
625 self.inner.message_type = Some(message_type.into());
626 let configured = self.inner.message_type.as_deref();
627 for layered in &mut self.inner.validators {
628 layered.validator.set_message_type(configured);
629 }
630 self
631 }
632
633 pub fn structure(mut self, enabled: bool) -> Self {
635 self.inner.structure_enabled = enabled;
636 self
637 }
638
639 pub fn code_list(mut self, enabled: bool) -> Self {
641 self.inner.code_list_enabled = enabled;
642 self
643 }
644
645 pub fn profile(mut self, enabled: bool) -> Self {
647 self.inner.profile_enabled = enabled;
648 self
649 }
650
651 pub fn envelope(mut self, enabled: bool) -> Self {
656 self.inner.envelope_enabled = enabled;
657 self
658 }
659
660 pub fn with_envelope_validation(mut self) -> Self {
677 self.inner.envelope_enabled = true;
678 self.inner.validators.push(LayeredValidator {
679 layer: ValidationLayer::Envelope,
680 validator: Box::new(EnvelopeValidator),
681 });
682 self
683 }
684
685 pub fn with_validator<V>(mut self, layer: ValidationLayer, mut validator: V) -> Self
687 where
688 V: Validator + 'static,
689 {
690 validator.set_message_type(self.inner.message_type.as_deref());
691 self.inner.validators.push(LayeredValidator {
692 layer,
693 validator: Box::new(validator),
694 });
695 self
696 }
697
698 pub fn with_profile_pack(mut self, mut pack: ProfileRulePack) -> Self {
700 pack.set_message_type(self.inner.message_type.as_deref());
701 self.inner.validators.push(LayeredValidator {
702 layer: ValidationLayer::Profile,
703 validator: Box::new(pack),
704 });
705 self
706 }
707
708 #[must_use = "call `.validate_lenient()` or `.validate_strict()` on the resulting context"]
710 pub fn build(self) -> ValidationContext {
711 self.inner
712 }
713}
714
715impl ValidationContext {
716 pub fn builder() -> ValidationContextBuilder {
718 ValidationContextBuilder::new()
719 }
720
721 pub fn validate_lenient(&self, segments: &[Segment<'_>]) -> ValidationReport {
726 let ctx = self
727 .metadata
728 .as_ref()
729 .map(|arc| ValidationRuleContext {
730 metadata: Some(arc.as_ref() as &(dyn Any + Send + Sync)),
731 })
732 .unwrap_or_else(ValidationRuleContext::empty);
733 self.validate_with_context(segments, &ctx)
734 }
735
736 pub fn validate_lenient_with<T: Any + Send + Sync>(
744 &self,
745 segments: &[Segment<'_>],
746 value: &T,
747 ) -> ValidationReport {
748 let ctx = ValidationRuleContext::new(value);
749 self.validate_with_context(segments, &ctx)
750 }
751
752 pub fn validate_strict(
762 &self,
763 segments: &[Segment<'_>],
764 ) -> Result<ValidationReport, ValidationReport> {
765 self.validate_lenient(segments).result()
766 }
767
768 pub fn validate_strict_with<T: Any + Send + Sync>(
773 &self,
774 segments: &[Segment<'_>],
775 value: &T,
776 ) -> Result<ValidationReport, ValidationReport> {
777 self.validate_lenient_with(segments, value).result()
778 }
779
780 pub fn validate_lenient_owned(&self, segments: &[OwnedSegment]) -> ValidationReport {
785 let borrowed: Vec<Segment<'_>> = segments.iter().map(|s| s.as_borrowed()).collect();
786 self.validate_lenient(&borrowed)
787 }
788
789 pub fn validate_strict_owned(
795 &self,
796 segments: &[OwnedSegment],
797 ) -> Result<ValidationReport, ValidationReport> {
798 self.validate_lenient_owned(segments).result()
799 }
800
801 fn validate_with_context(
802 &self,
803 segments: &[Segment<'_>],
804 context: &ValidationRuleContext<'_>,
805 ) -> ValidationReport {
806 let mut report = ValidationReport::default();
807 for lv in &self.validators {
808 if self.layer_enabled(lv.layer) {
809 lv.validator.validate_batch(segments, &mut report, context);
810 }
811 }
812 if let Some(ref msg_ref) = self.message_ref {
814 for issue in report
815 .errors
816 .iter_mut()
817 .chain(report.warnings.iter_mut())
818 .chain(report.infos.iter_mut())
819 {
820 if issue.message_ref.is_none() {
821 issue.message_ref = Some(msg_ref.clone());
822 }
823 }
824 }
825 report
826 }
827
828 pub fn message_type(&self) -> Option<&str> {
830 self.message_type.as_deref()
831 }
832
833 pub fn message_ref(&self) -> Option<&str> {
835 self.message_ref.as_deref()
836 }
837
838 fn layer_enabled(&self, layer: ValidationLayer) -> bool {
839 match layer {
840 ValidationLayer::Envelope => self.envelope_enabled,
841 ValidationLayer::Structure => self.structure_enabled,
842 ValidationLayer::CodeList => self.code_list_enabled,
843 ValidationLayer::Profile => self.profile_enabled,
844 }
845 }
846}
847
848pub trait Validator: Send + Sync {
870 fn validate_batch(
874 &self,
875 segments: &[Segment<'_>],
876 report: &mut ValidationReport,
877 context: &ValidationRuleContext<'_>,
878 );
879
880 fn set_message_type(&mut self, _message_type: Option<&str>) {}
882}
883
884pub fn validate_each<F>(segments: &[Segment<'_>], report: &mut ValidationReport, mut f: F)
895where
896 F: FnMut(&Segment<'_>) -> Result<(), EdifactError>,
897{
898 for segment in segments {
899 if let Err(err) = f(segment) {
900 report_error(report, err);
901 }
902 }
903}
904
905pub(crate) fn report_error(report: &mut ValidationReport, err: EdifactError) {
907 let issue = issue_from_error(err);
908 match issue.severity {
909 ValidationSeverity::Critical | ValidationSeverity::Error => report.add_error(issue),
910 ValidationSeverity::Warning => report.add_warning(issue),
911 ValidationSeverity::Info => report.add_info(issue),
912 }
913}
914
915pub struct EnvelopeValidator;
928
929impl Validator for EnvelopeValidator {
930 fn validate_batch(
931 &self,
932 segments: &[Segment<'_>],
933 report: &mut ValidationReport,
934 _ctx: &ValidationRuleContext<'_>,
935 ) {
936 if let Err(e) = crate::envelope::validate_envelope(segments) {
937 report_error(report, e);
938 }
939 }
940}
941
942fn issue_from_error(err: EdifactError) -> ValidationIssue {
943 let code = err.stable_code();
944 let mut issue = ValidationIssue::new(severity_for(&err), err.to_string()).with_error_code(code);
945 let default_hint = err.recovery_hint();
946
947 match err {
948 EdifactError::InvalidSegmentForMessage { tag, offset, .. } => {
949 issue = issue.with_segment(tag).with_offset(offset);
950 }
951 EdifactError::InvalidElementCount { tag, offset, .. } => {
952 issue = issue.with_segment(tag).with_offset(offset);
953 }
954 EdifactError::InvalidComponentCount {
955 tag,
956 element_index,
957 offset,
958 ..
959 } => {
960 issue = issue
961 .with_segment(tag)
962 .with_element_index(u8::try_from(element_index).unwrap_or(u8::MAX))
963 .with_offset(offset);
964 }
965 EdifactError::InvalidCodeValue {
966 tag,
967 element_index,
968 offset,
969 suggestion,
970 ..
971 } => {
972 issue = issue
973 .with_segment(tag)
974 .with_element_index(u8::try_from(element_index).unwrap_or(u8::MAX))
975 .with_offset(offset);
976 if let Some(s) = suggestion {
977 issue = issue.with_suggestion(s);
978 }
979 }
980 EdifactError::MissingSegment { tag, .. } => {
981 issue = issue.with_segment(tag);
982 }
983 EdifactError::QualifierMismatch { tag, offset, .. } => {
984 issue = issue
985 .with_segment(tag)
986 .with_element_index(0)
987 .with_offset(offset);
988 }
989 EdifactError::ConditionalRequirementNotMet {
990 tag,
991 element_index,
992 offset,
993 ..
994 } => {
995 issue = issue
996 .with_segment(tag)
997 .with_element_index(u8::try_from(element_index).unwrap_or(u8::MAX))
998 .with_offset(offset);
999 }
1000 EdifactError::MissingRequiredElement { tag, element_index } => {
1001 issue = issue.with_segment(tag);
1002 if let Ok(idx) = u8::try_from(element_index) {
1003 issue = issue.with_element_index(idx);
1004 }
1005 }
1006 EdifactError::MissingRequiredComponent {
1007 tag,
1008 element_index,
1009 component_index,
1010 } => {
1011 issue = issue.with_segment(tag);
1012 if let Ok(ei) = u8::try_from(element_index) {
1013 issue = issue.with_element_index(ei);
1014 }
1015 if let Ok(ci) = u8::try_from(component_index) {
1016 issue = issue.with_component_index(ci);
1017 }
1018 }
1019 EdifactError::InvalidReleaseSequence { offset }
1020 | EdifactError::InvalidDelimiter { offset, .. }
1021 | EdifactError::InvalidText { offset }
1022 | EdifactError::UnexpectedEof { offset }
1023 | EdifactError::UnexpectedDataToken { offset }
1024 | EdifactError::FunctionalGroupNotSupported { offset } => {
1025 issue = issue.with_offset(offset);
1026 }
1027 _ => {}
1028 }
1029
1030 if issue.suggestion.is_none() {
1031 if let Some(hint) = default_hint {
1032 issue = issue.with_suggestion(hint);
1033 }
1034 }
1035
1036 issue
1037}
1038
1039fn severity_for(err: &EdifactError) -> ValidationSeverity {
1040 match err {
1041 EdifactError::InvalidCodeValue { .. } | EdifactError::QualifierMismatch { .. } => {
1042 ValidationSeverity::Warning
1043 }
1044 _ => ValidationSeverity::Error,
1045 }
1046}
1047
1048#[cfg(test)]
1049mod tests {
1050 use super::*;
1051 use crate::model::Element;
1052
1053 fn demo_orders_profile_pack() -> ProfileRulePack {
1054 ProfileRulePack::new("ORDERS-DEMO")
1055 .for_message_type("ORDERS")
1056 .with_stateless_rule_fn(|segments| {
1057 let bgm = segments.iter().find(|segment| segment.tag == "BGM")?;
1058 let document_code = bgm.get_element(0)?.get_component(0)?;
1059 (document_code == "220").then(|| {
1060 ValidationIssue::new(
1061 ValidationSeverity::Error,
1062 "profile rule DEMO-P001 violated: BGM document code 220 is rejected in this demo pack",
1063 )
1064 .with_rule_id("DEMO-P001")
1065 .with_segment("BGM")
1066 .with_element_index(0)
1067 .with_suggestion("Use a different BGM document code in this demo pack")
1068 })
1069 })
1070 .with_stateless_rule_fn(|segments| {
1071 let bgm = segments.iter().find(|segment| segment.tag == "BGM")?;
1072 let reference = bgm.get_element(1)?.get_component(0)?;
1073 (reference == "PO123").then(|| {
1074 ValidationIssue::new(
1075 ValidationSeverity::Warning,
1076 "profile rule DEMO-P002 warning: purchase-order reference PO123 is reserved in this demo pack",
1077 )
1078 .with_rule_id("DEMO-P002")
1079 .with_segment("BGM")
1080 .with_element_index(1)
1081 .with_suggestion("Use a non-reserved reference in this demo pack")
1082 })
1083 })
1084 }
1085
1086 struct RejectBgm;
1087
1088 struct WarnBgm;
1089
1090 impl Validator for RejectBgm {
1091 fn validate_batch(
1092 &self,
1093 segments: &[Segment<'_>],
1094 report: &mut ValidationReport,
1095 _context: &ValidationRuleContext<'_>,
1096 ) {
1097 validate_each(segments, report, |segment| {
1098 if segment.tag == "BGM" {
1099 return Err(EdifactError::InvalidSegmentForMessage {
1100 tag: "BGM".to_owned(),
1101 message_type: "TEST".to_owned(),
1102 offset: segment.tag_span.start,
1103 });
1104 }
1105 Ok(())
1106 });
1107 }
1108 }
1109
1110 impl Validator for WarnBgm {
1111 fn validate_batch(
1112 &self,
1113 segments: &[Segment<'_>],
1114 report: &mut ValidationReport,
1115 _context: &ValidationRuleContext<'_>,
1116 ) {
1117 validate_each(segments, report, |segment| {
1118 if segment.tag == "BGM" {
1119 return Err(EdifactError::InvalidCodeValue {
1120 tag: "BGM".to_owned(),
1121 element_index: 0,
1122 value: "XXX".to_owned(),
1123 code_list: "1001".to_owned(),
1124 offset: segment.span.start,
1125 suggestion: None,
1126 });
1127 }
1128 Ok(())
1129 });
1130 }
1131 }
1132
1133 fn test_segment(tag: &'static str) -> Segment<'static> {
1134 Segment {
1135 tag,
1136 span: crate::Span::new(0, 0),
1137 tag_span: crate::Span::new(0, 0),
1138 elements: vec![Element::of(&["x"])],
1139 }
1140 }
1141
1142 #[test]
1143 fn lenient_collects_issues() {
1144 let segments = vec![test_segment("UNH"), test_segment("BGM")];
1145 let mut report = ValidationReport::default();
1146 RejectBgm.validate_batch(&segments, &mut report, &ValidationRuleContext::empty());
1147 assert!(report.has_errors());
1148 assert_eq!(report.errors().len(), 1);
1149 }
1150
1151 #[test]
1152 fn strict_fails_on_errors() {
1153 let segments = vec![test_segment("BGM")];
1154 let mut report = ValidationReport::default();
1155 RejectBgm.validate_batch(&segments, &mut report, &ValidationRuleContext::empty());
1156 assert!(report.has_errors());
1157 assert_eq!(report.errors().len(), 1);
1158 }
1159
1160 #[test]
1161 fn context_builder_respects_layer_toggles() {
1162 let segments = vec![test_segment("BGM")];
1163 let ctx = ValidationContext::builder()
1164 .structure(false)
1165 .with_validator(ValidationLayer::Structure, RejectBgm)
1166 .with_validator(ValidationLayer::CodeList, WarnBgm)
1167 .build();
1168
1169 let report = ctx.validate_lenient(&segments);
1170 assert!(!report.has_errors());
1171 assert_eq!(report.warnings().len(), 1);
1172 }
1173
1174 #[test]
1175 fn context_strict_fails_when_structure_enabled() {
1176 let segments = vec![test_segment("BGM")];
1177 let ctx = ValidationContext::builder()
1178 .with_message_type("ORDERS")
1179 .with_validator(ValidationLayer::Structure, RejectBgm)
1180 .build();
1181
1182 assert_eq!(ctx.message_type(), Some("ORDERS"));
1183 let result = ctx.validate_strict(&segments);
1184 assert!(result.is_err());
1185 assert!(result.unwrap_err().has_errors());
1186 }
1187
1188 #[test]
1189 fn report_error_applies_default_recovery_hint() {
1190 let mut report = ValidationReport::default();
1191 report_error(
1192 &mut report,
1193 EdifactError::InvalidReleaseSequence { offset: 9 },
1194 );
1195
1196 let issue = report
1197 .errors()
1198 .first()
1199 .expect("expected one issue in the report");
1200 let hint = issue
1201 .suggestion
1202 .as_deref()
1203 .expect("expected default hint to be set");
1204 assert!(hint.contains("Release character"));
1205 assert_eq!(issue.error_code, Some("E019"));
1206 }
1207
1208 #[test]
1209 fn missing_required_component_maps_metadata_to_issue() {
1210 let mut report = ValidationReport::default();
1211 report_error(
1212 &mut report,
1213 EdifactError::MissingRequiredComponent {
1214 tag: "BGM".to_owned(),
1215 element_index: 2,
1216 component_index: 1,
1217 },
1218 );
1219
1220 let issue = report.errors().first().expect("expected one issue");
1221 assert_eq!(issue.error_code, Some("E021"));
1222 assert_eq!(issue.segment_tag.as_deref(), Some("BGM"));
1223 assert_eq!(issue.element_index, Some(2));
1224 assert_eq!(issue.component_index, Some(1));
1225 }
1226
1227 #[test]
1228 fn profile_pack_lenient_collects_profile_rule_issues() {
1229 let input = b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'";
1230 let segments = crate::from_bytes(input)
1231 .collect::<Result<Vec<_>, _>>()
1232 .expect("expected parse success");
1233
1234 let ctx = ValidationContext::builder()
1235 .with_profile_pack(demo_orders_profile_pack())
1236 .build();
1237
1238 let report = ctx.validate_lenient(&segments);
1239 assert!(report.has_errors());
1240 assert!(
1241 report
1242 .errors()
1243 .iter()
1244 .any(|issue| issue.rule_id.as_deref() == Some("DEMO-P001"))
1245 );
1246 assert!(
1247 report
1248 .warnings()
1249 .iter()
1250 .any(|issue| issue.rule_id.as_deref() == Some("DEMO-P002"))
1251 );
1252 }
1253
1254 #[test]
1255 fn profile_pack_strict_fails_when_profile_errors_exist() {
1256 let input = b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'";
1257 let segments = crate::from_bytes(input)
1258 .collect::<Result<Vec<_>, _>>()
1259 .expect("expected parse success");
1260
1261 let ctx = ValidationContext::builder()
1262 .with_profile_pack(demo_orders_profile_pack())
1263 .build();
1264 let result = ctx.validate_strict(&segments);
1265 assert!(result.is_err());
1266 assert!(result.unwrap_err().has_errors());
1267 }
1268}