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(
294 &self,
295 segments: &[Segment<'_>],
296 ) -> Result<ValidationReport, EdifactError> {
297 let report = self.validate_lenient(segments);
298 if report.has_errors() {
299 let first_message = report
300 .errors
301 .first()
302 .map(|e| e.message.clone())
303 .unwrap_or_else(|| "unknown validation failure".to_owned());
304 return Err(EdifactError::ValidationFailed {
305 error_count: report.errors.len(),
306 first_message,
307 });
308 }
309 Ok(report)
310 }
311
312 pub fn message_type(&self) -> Option<&str> {
314 self.message_type.as_deref()
315 }
316
317 fn layer_enabled(&self, layer: ValidationLayer) -> bool {
318 match layer {
319 ValidationLayer::Structure => self.structure_enabled,
320 ValidationLayer::CodeList => self.code_list_enabled,
321 ValidationLayer::Profile => self.profile_enabled,
322 }
323 }
324}
325
326pub trait Validator: Send + Sync {
344 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport);
346
347 fn set_message_type(&mut self, _message_type: Option<&str>) {}
349}
350
351pub fn validate_each<F>(segments: &[Segment<'_>], report: &mut ValidationReport, mut f: F)
362where
363 F: FnMut(&Segment<'_>) -> Result<(), EdifactError>,
364{
365 for segment in segments {
366 if let Err(err) = f(segment) {
367 report_error(report, err);
368 }
369 }
370}
371
372pub(crate) fn report_error(report: &mut ValidationReport, err: EdifactError) {
374 let issue = issue_from_error(err);
375 match issue.severity {
376 ValidationSeverity::Critical | ValidationSeverity::Error => report.add_error(issue),
377 ValidationSeverity::Warning => report.add_warning(issue),
378 ValidationSeverity::Info => report.add_info(issue),
379 }
380}
381
382fn issue_from_error(err: EdifactError) -> ValidationIssue {
383 let code = err.stable_code();
384 let mut issue = ValidationIssue::new(severity_for(&err), err.to_string()).with_error_code(code);
385 let default_hint = err.recovery_hint();
386
387 match err {
388 EdifactError::InvalidSegmentForMessage { tag, offset, .. } => {
389 issue = issue.with_segment(tag).with_offset(offset);
390 }
391 EdifactError::InvalidElementCount { tag, offset, .. } => {
392 issue = issue.with_segment(tag).with_offset(offset);
393 }
394 EdifactError::InvalidComponentCount {
395 tag,
396 element_index,
397 offset,
398 ..
399 } => {
400 issue = issue
401 .with_segment(tag)
402 .with_element_index(element_index as u8)
403 .with_offset(offset);
404 }
405 EdifactError::InvalidCodeValue {
406 tag,
407 element_index,
408 offset,
409 suggestion,
410 ..
411 } => {
412 issue = issue
413 .with_segment(tag)
414 .with_element_index(element_index as u8)
415 .with_offset(offset);
416 if let Some(s) = suggestion {
417 issue = issue.with_suggestion(s);
418 }
419 }
420 EdifactError::MissingSegment { tag, .. } => {
421 issue = issue.with_segment(tag);
422 }
423 EdifactError::QualifierMismatch { tag, offset, .. } => {
424 issue = issue
425 .with_segment(tag)
426 .with_element_index(0)
427 .with_offset(offset);
428 }
429 EdifactError::ConditionalRequirementNotMet {
430 tag,
431 element_index,
432 offset,
433 ..
434 } => {
435 issue = issue
436 .with_segment(tag)
437 .with_element_index(element_index as u8)
438 .with_offset(offset);
439 }
440 EdifactError::MissingRequiredElement { tag, element_index } => {
441 issue = issue.with_segment(tag);
442 if let Ok(idx) = u8::try_from(element_index) {
443 issue = issue.with_element_index(idx);
444 }
445 }
446 EdifactError::MissingRequiredComponent {
447 tag,
448 element_index,
449 component_index,
450 } => {
451 issue = issue.with_segment(tag);
452 if let Ok(ei) = u8::try_from(element_index) {
453 issue = issue.with_element_index(ei);
454 }
455 if let Ok(ci) = u8::try_from(component_index) {
456 issue = issue.with_component_index(ci);
457 }
458 }
459 EdifactError::InvalidReleaseSequence { offset }
460 | EdifactError::InvalidDelimiter { offset, .. }
461 | EdifactError::InvalidText { offset }
462 | EdifactError::UnexpectedEof { offset } => {
463 issue = issue.with_offset(offset);
464 }
465 _ => {}
466 }
467
468 if issue.suggestion.is_none() {
469 if let Some(hint) = default_hint {
470 issue = issue.with_suggestion(hint);
471 }
472 }
473
474 issue
475}
476
477fn severity_for(err: &EdifactError) -> ValidationSeverity {
478 match err {
479 EdifactError::InvalidCodeValue { .. } | EdifactError::QualifierMismatch { .. } => {
480 ValidationSeverity::Warning
481 }
482 _ => ValidationSeverity::Error,
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use crate::model::Element;
490
491 fn demo_orders_profile_pack() -> ProfileRulePack {
492 ProfileRulePack::builder("ORDERS-DEMO")
493 .for_message_type("ORDERS")
494 .with_rule_fn(|segments| {
495 let bgm = segments.iter().find(|segment| segment.tag == "BGM")?;
496 let document_code = bgm.get_element(0)?.get_component(0)?;
497 (document_code == "220").then(|| {
498 ValidationIssue::new(
499 ValidationSeverity::Error,
500 "profile rule DEMO-P001 violated: BGM document code 220 is rejected in this demo pack",
501 )
502 .with_rule_id("DEMO-P001")
503 .with_segment("BGM")
504 .with_element_index(0)
505 .with_suggestion("Use a different BGM document code in this demo pack")
506 })
507 })
508 .with_rule_fn(|segments| {
509 let bgm = segments.iter().find(|segment| segment.tag == "BGM")?;
510 let reference = bgm.get_element(1)?.get_component(0)?;
511 (reference == "PO123").then(|| {
512 ValidationIssue::new(
513 ValidationSeverity::Warning,
514 "profile rule DEMO-P002 warning: purchase-order reference PO123 is reserved in this demo pack",
515 )
516 .with_rule_id("DEMO-P002")
517 .with_segment("BGM")
518 .with_element_index(1)
519 .with_suggestion("Use a non-reserved reference in this demo pack")
520 })
521 })
522 }
523
524 struct RejectBgm;
525
526 struct WarnBgm;
527
528 impl Validator for RejectBgm {
529 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport) {
530 validate_each(segments, report, |segment| {
531 if segment.tag == "BGM" {
532 return Err(EdifactError::InvalidSegmentForMessage {
533 tag: "BGM".to_owned(),
534 message_type: "TEST".to_owned(),
535 offset: segment.tag_span.start,
536 });
537 }
538 Ok(())
539 });
540 }
541 }
542
543 impl Validator for WarnBgm {
544 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport) {
545 validate_each(segments, report, |segment| {
546 if segment.tag == "BGM" {
547 return Err(EdifactError::InvalidCodeValue {
548 tag: "BGM".to_owned(),
549 element_index: 0,
550 value: "XXX".to_owned(),
551 code_list: "1001".to_owned(),
552 offset: segment.span.start,
553 suggestion: None,
554 });
555 }
556 Ok(())
557 });
558 }
559 }
560
561 fn test_segment(tag: &'static str) -> Segment<'static> {
562 Segment {
563 tag,
564 span: crate::Span::new(0, 0),
565 tag_span: crate::Span::new(0, 0),
566 elements: vec![Element::of(&["x"])],
567 }
568 }
569
570 #[test]
571 fn lenient_collects_issues() {
572 let segments = vec![test_segment("UNH"), test_segment("BGM")];
573 let mut report = ValidationReport::default();
574 RejectBgm.validate_batch(&segments, &mut report);
575 assert!(report.has_errors());
576 assert_eq!(report.errors.len(), 1);
577 }
578
579 #[test]
580 fn strict_fails_on_errors() {
581 let segments = vec![test_segment("BGM")];
582 let mut report = ValidationReport::default();
583 RejectBgm.validate_batch(&segments, &mut report);
584 assert!(report.has_errors());
585 assert_eq!(report.errors.len(), 1);
586 }
587
588 #[test]
589 fn context_builder_respects_layer_toggles() {
590 let segments = vec![test_segment("BGM")];
591 let ctx = ValidationContext::builder()
592 .structure(false)
593 .with_validator(ValidationLayer::Structure, RejectBgm)
594 .with_validator(ValidationLayer::CodeList, WarnBgm)
595 .build();
596
597 let report = ctx.validate_lenient(&segments);
598 assert!(!report.has_errors());
599 assert_eq!(report.warnings.len(), 1);
600 }
601
602 #[test]
603 fn context_strict_fails_when_structure_enabled() {
604 let segments = vec![test_segment("BGM")];
605 let ctx = ValidationContext::builder()
606 .with_message_type("ORDERS")
607 .with_validator(ValidationLayer::Structure, RejectBgm)
608 .build();
609
610 assert_eq!(ctx.message_type(), Some("ORDERS"));
611 let result = ctx.validate_strict(&segments);
612 assert!(matches!(result, Err(EdifactError::ValidationFailed { .. })));
613 }
614
615 #[test]
616 fn report_error_applies_default_recovery_hint() {
617 let mut report = ValidationReport::default();
618 report_error(
619 &mut report,
620 EdifactError::InvalidReleaseSequence { offset: 9 },
621 );
622
623 let issue = report
624 .errors
625 .first()
626 .expect("expected one issue in the report");
627 let hint = issue
628 .suggestion
629 .as_deref()
630 .expect("expected default hint to be set");
631 assert!(hint.contains("Release character"));
632 assert_eq!(issue.error_code, Some("E019"));
633 }
634
635 #[test]
636 fn missing_required_component_maps_metadata_to_issue() {
637 let mut report = ValidationReport::default();
638 report_error(
639 &mut report,
640 EdifactError::MissingRequiredComponent {
641 tag: "BGM".to_owned(),
642 element_index: 2,
643 component_index: 1,
644 },
645 );
646
647 let issue = report
648 .errors
649 .first()
650 .expect("expected one issue");
651 assert_eq!(issue.error_code, Some("E021"));
652 assert_eq!(issue.segment_tag.as_deref(), Some("BGM"));
653 assert_eq!(issue.element_index, Some(2));
654 assert_eq!(issue.component_index, Some(1));
655 }
656
657 #[test]
658 fn profile_pack_lenient_collects_profile_rule_issues() {
659 let input = b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'";
660 let segments = crate::from_bytes(input)
661 .collect::<Result<Vec<_>, _>>()
662 .expect("expected parse success");
663
664 let ctx = ValidationContext::builder()
665 .with_profile_pack(demo_orders_profile_pack())
666 .build();
667
668 let report = ctx.validate_lenient(&segments);
669 assert!(report.has_errors());
670 assert!(
671 report
672 .errors
673 .iter()
674 .any(|issue| issue.rule_id.as_deref() == Some("DEMO-P001"))
675 );
676 assert!(
677 report
678 .warnings
679 .iter()
680 .any(|issue| issue.rule_id.as_deref() == Some("DEMO-P002"))
681 );
682 }
683
684 #[test]
685 fn profile_pack_strict_fails_when_profile_errors_exist() {
686 let input = b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'";
687 let segments = crate::from_bytes(input)
688 .collect::<Result<Vec<_>, _>>()
689 .expect("expected parse success");
690
691 let ctx = ValidationContext::builder()
692 .with_profile_pack(demo_orders_profile_pack())
693 .build();
694 let result = ctx.validate_strict(&segments);
695 assert!(matches!(result, Err(EdifactError::ValidationFailed { .. })));
696 }
697}