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(
76 &self,
77 segments: &[Segment<'_>],
78 context: &ValidationRuleContext<'_>,
79 ) -> Option<ValidationIssue>;
80}
81
82struct ClosureProfileRule<F>(F);
84
85impl<F> ProfileRule for ClosureProfileRule<F>
86where
87 F: for<'a> Fn(&[Segment<'a>], &ValidationRuleContext<'_>) -> Option<ValidationIssue>
88 + Send
89 + Sync,
90{
91 fn evaluate(
92 &self,
93 segments: &[Segment<'_>],
94 context: &ValidationRuleContext<'_>,
95 ) -> Option<ValidationIssue> {
96 (self.0)(segments, context)
97 }
98}
99
100struct StatelessClosureProfileRule<F>(F);
102
103impl<F> ProfileRule for StatelessClosureProfileRule<F>
104where
105 F: for<'a> Fn(&[Segment<'a>]) -> Option<ValidationIssue> + Send + Sync,
106{
107 fn evaluate(
108 &self,
109 segments: &[Segment<'_>],
110 _context: &ValidationRuleContext<'_>,
111 ) -> Option<ValidationIssue> {
112 (self.0)(segments)
113 }
114}
115
116struct NamedRule {
122 id: Option<Arc<str>>,
126 rule: Arc<dyn ProfileRule + Send + Sync>,
127}
128
129impl Clone for NamedRule {
130 fn clone(&self) -> Self {
131 Self {
132 id: self.id.clone(),
133 rule: Arc::clone(&self.rule),
134 }
135 }
136}
137
138pub struct ProfileRulePack {
140 name: String,
141 message_types: Vec<String>,
142 release: Option<String>,
146 rules: Vec<NamedRule>,
147 bail_on_first_error: bool,
148}
149
150impl ProfileRulePack {
151 pub fn new(name: impl Into<String>) -> Self {
153 Self {
154 name: name.into(),
155 message_types: Vec::new(),
156 release: None,
157 rules: Vec::new(),
158 bail_on_first_error: false,
159 }
160 }
161
162 pub fn name(&self) -> &str {
164 &self.name
165 }
166
167 pub fn message_types(&self) -> &[String] {
169 &self.message_types
170 }
171
172 pub fn rule_count(&self) -> usize {
174 self.rules.len()
175 }
176
177 pub fn release(&self) -> Option<&str> {
181 self.release.as_deref()
182 }
183
184 pub fn for_message_type(mut self, message_type: impl Into<String>) -> Self {
200 let message_type = message_type.into();
201 if !self.message_types.contains(&message_type) {
202 self.message_types.push(message_type);
203 }
204 self
205 }
206
207 pub fn for_release(mut self, release: impl Into<String>) -> Self {
222 self.release = Some(release.into());
223 self
224 }
225
226 pub fn bail_on_first_error(mut self, bail: bool) -> Self {
234 self.bail_on_first_error = bail;
235 self
236 }
237
238 pub fn with_rule_fn<F>(mut self, rule: F) -> Self
246 where
247 F: for<'a> Fn(&[Segment<'a>], &ValidationRuleContext<'_>) -> Option<ValidationIssue>
248 + Send
249 + Sync
250 + 'static,
251 {
252 self.rules.push(NamedRule {
253 id: None,
254 rule: Arc::new(ClosureProfileRule(rule)),
255 });
256 self
257 }
258
259 pub fn with_named_rule_fn<F>(mut self, id: impl Into<Arc<str>>, rule: F) -> Self
265 where
266 F: for<'a> Fn(&[Segment<'a>], &ValidationRuleContext<'_>) -> Option<ValidationIssue>
267 + Send
268 + Sync
269 + 'static,
270 {
271 self.rules.push(NamedRule {
272 id: Some(id.into()),
273 rule: Arc::new(ClosureProfileRule(rule)),
274 });
275 self
276 }
277
278 pub fn with_stateless_rule_fn<F>(mut self, rule: F) -> Self
283 where
284 F: for<'a> Fn(&[Segment<'a>]) -> Option<ValidationIssue> + Send + Sync + 'static,
285 {
286 self.rules.push(NamedRule {
287 id: None,
288 rule: Arc::new(StatelessClosureProfileRule(rule)),
289 });
290 self
291 }
292
293 pub fn with_named_stateless_rule_fn<F>(mut self, id: impl Into<Arc<str>>, rule: F) -> Self
297 where
298 F: for<'a> Fn(&[Segment<'a>]) -> Option<ValidationIssue> + Send + Sync + 'static,
299 {
300 self.rules.push(NamedRule {
301 id: Some(id.into()),
302 rule: Arc::new(StatelessClosureProfileRule(rule)),
303 });
304 self
305 }
306
307 pub fn with_rule(mut self, rule: impl ProfileRule + 'static) -> Self {
309 self.rules.push(NamedRule {
310 id: None,
311 rule: Arc::new(rule),
312 });
313 self
314 }
315
316 pub fn with_named_rule(
320 mut self,
321 id: impl Into<Arc<str>>,
322 rule: impl ProfileRule + 'static,
323 ) -> Self {
324 self.rules.push(NamedRule {
325 id: Some(id.into()),
326 rule: Arc::new(rule),
327 });
328 self
329 }
330
331 pub fn extend_from(mut self, base: &ProfileRulePack) -> Result<Self, EdifactError> {
350 let mut combined = base.rules.clone();
351 combined.append(&mut self.rules);
352 self.rules = combined;
353 for mt in &base.message_types {
354 if !self.message_types.contains(mt) {
355 self.message_types.push(mt.clone());
356 }
357 }
358 self.release = merge_release_scopes(self.release.take(), base.release.clone())?;
359 Ok(self)
360 }
361
362 pub fn merge(mut self, mut other: Self) -> Result<Self, EdifactError> {
370 for message_type in other.message_types.drain(..) {
371 if !self.message_types.contains(&message_type) {
372 self.message_types.push(message_type);
373 }
374 }
375 self.release = merge_release_scopes(self.release.take(), other.release.take())?;
376 self.rules.append(&mut other.rules);
377 Ok(self)
378 }
379
380 pub fn merge_with_override(mut self, mut other: Self) -> Result<Self, EdifactError> {
408 let mut id_to_index: std::collections::HashMap<Arc<str>, usize> = Default::default();
410 for (idx, rule) in self.rules.iter().enumerate() {
411 if let Some(id) = &rule.id {
412 id_to_index.insert(id.clone(), idx);
413 }
414 }
415
416 let mut replacements: Vec<(usize, NamedRule)> = Vec::new();
418 let mut to_append = Vec::new();
419
420 for other_rule in other.rules.drain(..) {
421 if let Some(id) = &other_rule.id {
422 if let Some(&idx) = id_to_index.get(id) {
423 replacements.push((idx, other_rule));
424 } else {
425 to_append.push(other_rule);
426 }
427 } else {
428 to_append.push(other_rule);
429 }
430 }
431
432 for (idx, rule) in replacements {
434 if idx < self.rules.len() {
435 self.rules[idx] = rule;
436 }
437 }
438
439 self.rules.append(&mut to_append);
441
442 for message_type in other.message_types.drain(..) {
443 if !self.message_types.contains(&message_type) {
444 self.message_types.push(message_type);
445 }
446 }
447 self.release = merge_release_scopes(self.release.take(), other.release.take())?;
448 Ok(self)
449 }
450}
451
452fn merge_release_scopes(
453 current: Option<String>,
454 incoming: Option<String>,
455) -> Result<Option<String>, EdifactError> {
456 match (current, incoming) {
457 (Some(current), Some(incoming)) => {
458 if current != incoming {
460 return Err(EdifactError::IncompatibleReleaseScopes { current, incoming });
461 }
462 Ok(Some(current))
463 }
464 (current @ Some(_), None) => Ok(current),
465 (None, incoming) => Ok(incoming),
466 }
467}
468
469impl Validator for ProfileRulePack {
470 fn validate_batch(
471 &self,
472 segments: &[Segment<'_>],
473 report: &mut ValidationReport,
474 context: &ValidationRuleContext<'_>,
475 ) {
476 let unh = segments.iter().find(|segment| segment.tag == "UNH");
477
478 let message_type = unh
480 .and_then(|s| s.get_element(1))
481 .and_then(|e| e.get_component(0));
482 if !self.message_types.is_empty()
483 && !message_type.is_some_and(|mt| self.message_types.iter().any(|t| t == mt))
484 {
485 return;
486 }
487
488 if let Some(bound_release) = &self.release {
491 let msg_association = unh
492 .and_then(|s| s.get_element(1))
493 .and_then(|e| e.get_component(4));
494 if msg_association != Some(bound_release.as_str()) {
495 return;
496 }
497 }
498
499 for named in &self.rules {
500 if let Some(issue) = named.rule.evaluate(segments, context) {
501 let was_error = match issue.severity {
502 ValidationSeverity::Critical | ValidationSeverity::Error => {
503 report.add_error(issue);
504 true
505 }
506 ValidationSeverity::Warning => {
507 report.add_warning(issue);
508 false
509 }
510 ValidationSeverity::Info => {
511 report.add_info(issue);
512 false
513 }
514 };
515 if self.bail_on_first_error && was_error {
516 return;
517 }
518 }
519 }
520 }
521}
522
523impl std::fmt::Debug for ProfileRulePack {
524 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525 f.debug_struct("ProfileRulePack")
526 .field("name", &self.name)
527 .field("message_types", &self.message_types)
528 .field("release", &self.release)
529 .field("rule_count", &self.rules.len())
530 .field("bail_on_first_error", &self.bail_on_first_error)
531 .finish()
532 }
533}
534
535#[derive(Debug, Clone, Copy, PartialEq, Eq)]
537#[non_exhaustive]
538pub enum ValidationLayer {
539 Structure,
541 CodeList,
543 Profile,
545}
546
547struct LayeredValidator {
548 layer: ValidationLayer,
549 validator: Box<dyn Validator + Send + Sync>,
550}
551
552pub struct ValidationContext {
554 validators: Vec<LayeredValidator>,
555 structure_enabled: bool,
556 code_list_enabled: bool,
557 profile_enabled: bool,
558 message_type: Option<String>,
559 metadata: Option<Arc<dyn Any + Send + Sync>>,
560}
561
562#[must_use = "call `.build()` to produce a `ValidationContext`"]
564pub struct ValidationContextBuilder {
565 inner: ValidationContext,
566}
567
568impl Default for ValidationContextBuilder {
569 fn default() -> Self {
571 Self::new()
572 }
573}
574
575impl ValidationContextBuilder {
576 pub fn new() -> Self {
578 Self {
579 inner: ValidationContext {
580 validators: Vec::new(),
581 structure_enabled: true,
582 code_list_enabled: true,
583 profile_enabled: true,
584 message_type: None,
585 metadata: None,
586 },
587 }
588 }
589
590 pub fn with_metadata<T: Any + Send + Sync + 'static>(mut self, value: T) -> Self {
599 self.inner.metadata = Some(Arc::new(value));
600 self
601 }
602
603 pub fn with_message_type(mut self, message_type: impl Into<String>) -> Self {
605 self.inner.message_type = Some(message_type.into());
606 let configured = self.inner.message_type.as_deref();
607 for layered in &mut self.inner.validators {
608 layered.validator.set_message_type(configured);
609 }
610 self
611 }
612
613 pub fn structure(mut self, enabled: bool) -> Self {
615 self.inner.structure_enabled = enabled;
616 self
617 }
618
619 pub fn code_list(mut self, enabled: bool) -> Self {
621 self.inner.code_list_enabled = enabled;
622 self
623 }
624
625 pub fn profile(mut self, enabled: bool) -> Self {
627 self.inner.profile_enabled = enabled;
628 self
629 }
630
631 pub fn with_validator<V>(mut self, layer: ValidationLayer, mut validator: V) -> Self
633 where
634 V: Validator + 'static,
635 {
636 validator.set_message_type(self.inner.message_type.as_deref());
637 self.inner.validators.push(LayeredValidator {
638 layer,
639 validator: Box::new(validator),
640 });
641 self
642 }
643
644 pub fn with_profile_pack(mut self, mut pack: ProfileRulePack) -> Self {
646 pack.set_message_type(self.inner.message_type.as_deref());
647 self.inner.validators.push(LayeredValidator {
648 layer: ValidationLayer::Profile,
649 validator: Box::new(pack),
650 });
651 self
652 }
653
654 #[must_use = "call `.validate_lenient()` or `.validate_strict()` on the resulting context"]
656 pub fn build(self) -> ValidationContext {
657 self.inner
658 }
659}
660
661impl ValidationContext {
662 pub fn builder() -> ValidationContextBuilder {
664 ValidationContextBuilder::new()
665 }
666
667 pub fn validate_lenient(&self, segments: &[Segment<'_>]) -> ValidationReport {
672 let ctx = self
673 .metadata
674 .as_ref()
675 .map(|arc| ValidationRuleContext {
676 metadata: Some(arc.as_ref() as &(dyn Any + Send + Sync)),
677 })
678 .unwrap_or_else(ValidationRuleContext::empty);
679 self.validate_with_context(segments, &ctx)
680 }
681
682 pub fn validate_lenient_with<T: Any + Send + Sync>(
690 &self,
691 segments: &[Segment<'_>],
692 value: &T,
693 ) -> ValidationReport {
694 let ctx = ValidationRuleContext::new(value);
695 self.validate_with_context(segments, &ctx)
696 }
697
698 pub fn validate_strict(
705 &self,
706 segments: &[Segment<'_>],
707 ) -> Result<ValidationReport, EdifactError> {
708 let report = self.validate_lenient(segments);
709 Self::strict_check(report)
710 }
711
712 pub fn validate_strict_with<T: Any + Send + Sync>(
717 &self,
718 segments: &[Segment<'_>],
719 value: &T,
720 ) -> Result<ValidationReport, EdifactError> {
721 let report = self.validate_lenient_with(segments, value);
722 Self::strict_check(report)
723 }
724
725 fn validate_with_context(
726 &self,
727 segments: &[Segment<'_>],
728 context: &ValidationRuleContext<'_>,
729 ) -> ValidationReport {
730 let mut report = ValidationReport::default();
731 for lv in &self.validators {
732 if self.layer_enabled(lv.layer) {
733 lv.validator.validate_batch(segments, &mut report, context);
734 }
735 }
736 report
737 }
738
739 fn strict_check(report: ValidationReport) -> Result<ValidationReport, EdifactError> {
740 if report.has_errors() {
741 let first_message = report
742 .errors()
743 .first()
744 .map(|e| e.message.clone())
745 .unwrap_or_else(|| "unknown validation failure".to_owned());
746 return Err(EdifactError::ValidationFailed {
747 error_count: report.errors().len(),
748 first_message,
749 });
750 }
751 Ok(report)
752 }
753
754 pub fn message_type(&self) -> Option<&str> {
756 self.message_type.as_deref()
757 }
758
759 fn layer_enabled(&self, layer: ValidationLayer) -> bool {
760 match layer {
761 ValidationLayer::Structure => self.structure_enabled,
762 ValidationLayer::CodeList => self.code_list_enabled,
763 ValidationLayer::Profile => self.profile_enabled,
764 }
765 }
766}
767
768pub trait Validator: Send + Sync {
790 fn validate_batch(
794 &self,
795 segments: &[Segment<'_>],
796 report: &mut ValidationReport,
797 context: &ValidationRuleContext<'_>,
798 );
799
800 fn set_message_type(&mut self, _message_type: Option<&str>) {}
802}
803
804pub fn validate_each<F>(segments: &[Segment<'_>], report: &mut ValidationReport, mut f: F)
815where
816 F: FnMut(&Segment<'_>) -> Result<(), EdifactError>,
817{
818 for segment in segments {
819 if let Err(err) = f(segment) {
820 report_error(report, err);
821 }
822 }
823}
824
825pub(crate) fn report_error(report: &mut ValidationReport, err: EdifactError) {
827 let issue = issue_from_error(err);
828 match issue.severity {
829 ValidationSeverity::Critical | ValidationSeverity::Error => report.add_error(issue),
830 ValidationSeverity::Warning => report.add_warning(issue),
831 ValidationSeverity::Info => report.add_info(issue),
832 }
833}
834
835fn issue_from_error(err: EdifactError) -> ValidationIssue {
836 let code = err.stable_code();
837 let mut issue = ValidationIssue::new(severity_for(&err), err.to_string()).with_error_code(code);
838 let default_hint = err.recovery_hint();
839
840 match err {
841 EdifactError::InvalidSegmentForMessage { tag, offset, .. } => {
842 issue = issue.with_segment(tag).with_offset(offset);
843 }
844 EdifactError::InvalidElementCount { tag, offset, .. } => {
845 issue = issue.with_segment(tag).with_offset(offset);
846 }
847 EdifactError::InvalidComponentCount {
848 tag,
849 element_index,
850 offset,
851 ..
852 } => {
853 issue = issue
854 .with_segment(tag)
855 .with_element_index(u8::try_from(element_index).unwrap_or(u8::MAX))
856 .with_offset(offset);
857 }
858 EdifactError::InvalidCodeValue {
859 tag,
860 element_index,
861 offset,
862 suggestion,
863 ..
864 } => {
865 issue = issue
866 .with_segment(tag)
867 .with_element_index(u8::try_from(element_index).unwrap_or(u8::MAX))
868 .with_offset(offset);
869 if let Some(s) = suggestion {
870 issue = issue.with_suggestion(s);
871 }
872 }
873 EdifactError::MissingSegment { tag, .. } => {
874 issue = issue.with_segment(tag);
875 }
876 EdifactError::QualifierMismatch { tag, offset, .. } => {
877 issue = issue
878 .with_segment(tag)
879 .with_element_index(0)
880 .with_offset(offset);
881 }
882 EdifactError::ConditionalRequirementNotMet {
883 tag,
884 element_index,
885 offset,
886 ..
887 } => {
888 issue = issue
889 .with_segment(tag)
890 .with_element_index(u8::try_from(element_index).unwrap_or(u8::MAX))
891 .with_offset(offset);
892 }
893 EdifactError::MissingRequiredElement { tag, element_index } => {
894 issue = issue.with_segment(tag);
895 if let Ok(idx) = u8::try_from(element_index) {
896 issue = issue.with_element_index(idx);
897 }
898 }
899 EdifactError::MissingRequiredComponent {
900 tag,
901 element_index,
902 component_index,
903 } => {
904 issue = issue.with_segment(tag);
905 if let Ok(ei) = u8::try_from(element_index) {
906 issue = issue.with_element_index(ei);
907 }
908 if let Ok(ci) = u8::try_from(component_index) {
909 issue = issue.with_component_index(ci);
910 }
911 }
912 EdifactError::InvalidReleaseSequence { offset }
913 | EdifactError::InvalidDelimiter { offset, .. }
914 | EdifactError::InvalidText { offset }
915 | EdifactError::UnexpectedEof { offset } => {
916 issue = issue.with_offset(offset);
917 }
918 _ => {}
919 }
920
921 if issue.suggestion.is_none() {
922 if let Some(hint) = default_hint {
923 issue = issue.with_suggestion(hint);
924 }
925 }
926
927 issue
928}
929
930fn severity_for(err: &EdifactError) -> ValidationSeverity {
931 match err {
932 EdifactError::InvalidCodeValue { .. } | EdifactError::QualifierMismatch { .. } => {
933 ValidationSeverity::Warning
934 }
935 _ => ValidationSeverity::Error,
936 }
937}
938
939#[cfg(test)]
940mod tests {
941 use super::*;
942 use crate::model::Element;
943
944 fn demo_orders_profile_pack() -> ProfileRulePack {
945 ProfileRulePack::new("ORDERS-DEMO")
946 .for_message_type("ORDERS")
947 .with_stateless_rule_fn(|segments| {
948 let bgm = segments.iter().find(|segment| segment.tag == "BGM")?;
949 let document_code = bgm.get_element(0)?.get_component(0)?;
950 (document_code == "220").then(|| {
951 ValidationIssue::new(
952 ValidationSeverity::Error,
953 "profile rule DEMO-P001 violated: BGM document code 220 is rejected in this demo pack",
954 )
955 .with_rule_id("DEMO-P001")
956 .with_segment("BGM")
957 .with_element_index(0)
958 .with_suggestion("Use a different BGM document code in this demo pack")
959 })
960 })
961 .with_stateless_rule_fn(|segments| {
962 let bgm = segments.iter().find(|segment| segment.tag == "BGM")?;
963 let reference = bgm.get_element(1)?.get_component(0)?;
964 (reference == "PO123").then(|| {
965 ValidationIssue::new(
966 ValidationSeverity::Warning,
967 "profile rule DEMO-P002 warning: purchase-order reference PO123 is reserved in this demo pack",
968 )
969 .with_rule_id("DEMO-P002")
970 .with_segment("BGM")
971 .with_element_index(1)
972 .with_suggestion("Use a non-reserved reference in this demo pack")
973 })
974 })
975 }
976
977 struct RejectBgm;
978
979 struct WarnBgm;
980
981 impl Validator for RejectBgm {
982 fn validate_batch(
983 &self,
984 segments: &[Segment<'_>],
985 report: &mut ValidationReport,
986 _context: &ValidationRuleContext<'_>,
987 ) {
988 validate_each(segments, report, |segment| {
989 if segment.tag == "BGM" {
990 return Err(EdifactError::InvalidSegmentForMessage {
991 tag: "BGM".to_owned(),
992 message_type: "TEST".to_owned(),
993 offset: segment.tag_span.start,
994 });
995 }
996 Ok(())
997 });
998 }
999 }
1000
1001 impl Validator for WarnBgm {
1002 fn validate_batch(
1003 &self,
1004 segments: &[Segment<'_>],
1005 report: &mut ValidationReport,
1006 _context: &ValidationRuleContext<'_>,
1007 ) {
1008 validate_each(segments, report, |segment| {
1009 if segment.tag == "BGM" {
1010 return Err(EdifactError::InvalidCodeValue {
1011 tag: "BGM".to_owned(),
1012 element_index: 0,
1013 value: "XXX".to_owned(),
1014 code_list: "1001".to_owned(),
1015 offset: segment.span.start,
1016 suggestion: None,
1017 });
1018 }
1019 Ok(())
1020 });
1021 }
1022 }
1023
1024 fn test_segment(tag: &'static str) -> Segment<'static> {
1025 Segment {
1026 tag,
1027 span: crate::Span::new(0, 0),
1028 tag_span: crate::Span::new(0, 0),
1029 elements: vec![Element::of(&["x"])],
1030 }
1031 }
1032
1033 #[test]
1034 fn lenient_collects_issues() {
1035 let segments = vec![test_segment("UNH"), test_segment("BGM")];
1036 let mut report = ValidationReport::default();
1037 RejectBgm.validate_batch(&segments, &mut report, &ValidationRuleContext::empty());
1038 assert!(report.has_errors());
1039 assert_eq!(report.errors().len(), 1);
1040 }
1041
1042 #[test]
1043 fn strict_fails_on_errors() {
1044 let segments = vec![test_segment("BGM")];
1045 let mut report = ValidationReport::default();
1046 RejectBgm.validate_batch(&segments, &mut report, &ValidationRuleContext::empty());
1047 assert!(report.has_errors());
1048 assert_eq!(report.errors().len(), 1);
1049 }
1050
1051 #[test]
1052 fn context_builder_respects_layer_toggles() {
1053 let segments = vec![test_segment("BGM")];
1054 let ctx = ValidationContext::builder()
1055 .structure(false)
1056 .with_validator(ValidationLayer::Structure, RejectBgm)
1057 .with_validator(ValidationLayer::CodeList, WarnBgm)
1058 .build();
1059
1060 let report = ctx.validate_lenient(&segments);
1061 assert!(!report.has_errors());
1062 assert_eq!(report.warnings().len(), 1);
1063 }
1064
1065 #[test]
1066 fn context_strict_fails_when_structure_enabled() {
1067 let segments = vec![test_segment("BGM")];
1068 let ctx = ValidationContext::builder()
1069 .with_message_type("ORDERS")
1070 .with_validator(ValidationLayer::Structure, RejectBgm)
1071 .build();
1072
1073 assert_eq!(ctx.message_type(), Some("ORDERS"));
1074 let result = ctx.validate_strict(&segments);
1075 assert!(matches!(result, Err(EdifactError::ValidationFailed { .. })));
1076 }
1077
1078 #[test]
1079 fn report_error_applies_default_recovery_hint() {
1080 let mut report = ValidationReport::default();
1081 report_error(
1082 &mut report,
1083 EdifactError::InvalidReleaseSequence { offset: 9 },
1084 );
1085
1086 let issue = report
1087 .errors()
1088 .first()
1089 .expect("expected one issue in the report");
1090 let hint = issue
1091 .suggestion
1092 .as_deref()
1093 .expect("expected default hint to be set");
1094 assert!(hint.contains("Release character"));
1095 assert_eq!(issue.error_code, Some("E019"));
1096 }
1097
1098 #[test]
1099 fn missing_required_component_maps_metadata_to_issue() {
1100 let mut report = ValidationReport::default();
1101 report_error(
1102 &mut report,
1103 EdifactError::MissingRequiredComponent {
1104 tag: "BGM".to_owned(),
1105 element_index: 2,
1106 component_index: 1,
1107 },
1108 );
1109
1110 let issue = report.errors().first().expect("expected one issue");
1111 assert_eq!(issue.error_code, Some("E021"));
1112 assert_eq!(issue.segment_tag.as_deref(), Some("BGM"));
1113 assert_eq!(issue.element_index, Some(2));
1114 assert_eq!(issue.component_index, Some(1));
1115 }
1116
1117 #[test]
1118 fn profile_pack_lenient_collects_profile_rule_issues() {
1119 let input = b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'";
1120 let segments = crate::from_bytes(input)
1121 .collect::<Result<Vec<_>, _>>()
1122 .expect("expected parse success");
1123
1124 let ctx = ValidationContext::builder()
1125 .with_profile_pack(demo_orders_profile_pack())
1126 .build();
1127
1128 let report = ctx.validate_lenient(&segments);
1129 assert!(report.has_errors());
1130 assert!(
1131 report
1132 .errors()
1133 .iter()
1134 .any(|issue| issue.rule_id.as_deref() == Some("DEMO-P001"))
1135 );
1136 assert!(
1137 report
1138 .warnings()
1139 .iter()
1140 .any(|issue| issue.rule_id.as_deref() == Some("DEMO-P002"))
1141 );
1142 }
1143
1144 #[test]
1145 fn profile_pack_strict_fails_when_profile_errors_exist() {
1146 let input = b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'";
1147 let segments = crate::from_bytes(input)
1148 .collect::<Result<Vec<_>, _>>()
1149 .expect("expected parse success");
1150
1151 let ctx = ValidationContext::builder()
1152 .with_profile_pack(demo_orders_profile_pack())
1153 .build();
1154 let result = ctx.validate_strict(&segments);
1155 assert!(matches!(result, Err(EdifactError::ValidationFailed { .. })));
1156 }
1157}