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 {
75 let message_type = message_type.into();
76 if !self.message_types.contains(&message_type) {
77 self.message_types.push(message_type);
78 }
79 self
80 }
81
82 pub fn with_rule_fn<F>(mut self, rule: F) -> Self
84 where
85 F: for<'a> Fn(&[Segment<'a>]) -> Option<ValidationIssue> + Send + Sync + 'static,
86 {
87 self.rules.push(Box::new(ClosureProfileRule(rule)));
88 self
89 }
90
91 pub fn with_rule(mut self, rule: impl ProfileRule + 'static) -> Self {
93 self.rules.push(Box::new(rule));
94 self
95 }
96
97 pub fn merge(mut self, mut other: Self) -> Self {
99 for message_type in other.message_types.drain(..) {
100 if !self.message_types.contains(&message_type) {
101 self.message_types.push(message_type);
102 }
103 }
104 self.rules.append(&mut other.rules);
105 self
106 }
107}
108
109impl Validator for ProfileRulePack {
110 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport) {
111 let message_type = segments
112 .iter()
113 .find(|segment| segment.tag == "UNH")
114 .and_then(|segment| segment.get_element(1))
115 .and_then(|element| element.get_component(0));
116 if !self.message_types.is_empty()
117 && !message_type.is_some_and(|mt| self.message_types.iter().any(|t| t == mt))
118 {
119 return;
120 }
121
122 for rule in &self.rules {
123 if let Some(issue) = rule.evaluate(segments) {
124 match issue.severity {
125 ValidationSeverity::Critical | ValidationSeverity::Error => {
126 report.add_error(issue);
127 }
128 ValidationSeverity::Warning => {
129 report.add_warning(issue);
130 }
131 ValidationSeverity::Info => {
132 report.add_info(issue);
133 }
134 }
135 }
136 }
137 }
138}
139
140impl std::fmt::Debug for ProfileRulePack {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 f.debug_struct("ProfileRulePack")
143 .field("name", &self.name)
144 .field("message_types", &self.message_types)
145 .field("rule_count", &self.rules.len())
146 .finish()
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum ValidationLayer {
153 Structure,
155 CodeList,
157 Profile,
159}
160
161struct LayeredValidator {
162 layer: ValidationLayer,
163 validator: Box<dyn Validator + Send + Sync>,
164}
165
166pub struct ValidationContext {
168 validators: Vec<LayeredValidator>,
169 structure_enabled: bool,
170 code_list_enabled: bool,
171 profile_enabled: bool,
172 message_type: Option<String>,
173}
174
175#[must_use = "call `.build()` to produce a `ValidationContext`"]
177pub struct ValidationContextBuilder {
178 inner: ValidationContext,
179}
180
181impl Default for ValidationContextBuilder {
182 fn default() -> Self {
184 Self::new()
185 }
186}
187
188impl ValidationContextBuilder {
189 pub fn new() -> Self {
191 Self {
192 inner: ValidationContext {
193 validators: Vec::new(),
194 structure_enabled: true,
195 code_list_enabled: true,
196 profile_enabled: true,
197 message_type: None,
198 },
199 }
200 }
201
202 pub fn with_message_type(mut self, message_type: impl Into<String>) -> Self {
204 self.inner.message_type = Some(message_type.into());
205 let configured = self.inner.message_type.as_deref();
206 for layered in &mut self.inner.validators {
207 layered.validator.set_message_type(configured);
208 }
209 self
210 }
211
212 pub fn structure(mut self, enabled: bool) -> Self {
214 self.inner.structure_enabled = enabled;
215 self
216 }
217
218 pub fn code_list(mut self, enabled: bool) -> Self {
220 self.inner.code_list_enabled = enabled;
221 self
222 }
223
224 pub fn profile(mut self, enabled: bool) -> Self {
226 self.inner.profile_enabled = enabled;
227 self
228 }
229
230 pub fn with_validator<V>(mut self, layer: ValidationLayer, mut validator: V) -> Self
232 where
233 V: Validator + 'static,
234 {
235 validator.set_message_type(self.inner.message_type.as_deref());
236 self.inner.validators.push(LayeredValidator {
237 layer,
238 validator: Box::new(validator),
239 });
240 self
241 }
242
243 pub fn with_profile_pack(mut self, pack: ProfileRulePack) -> Self {
245 self.inner.validators.push(LayeredValidator {
246 layer: ValidationLayer::Profile,
247 validator: Box::new(pack),
248 });
249 self
250 }
251
252 #[must_use = "call `.validate_lenient()` or `.validate_strict()` on the resulting context"]
254 pub fn build(self) -> ValidationContext {
255 self.inner
256 }
257}
258
259impl ValidationContext {
260 pub fn builder() -> ValidationContextBuilder {
262 ValidationContextBuilder::new()
263 }
264
265 pub fn validate_lenient(&self, segments: &[Segment<'_>]) -> ValidationReport {
267 let mut report = ValidationReport::default();
268 for lv in &self.validators {
269 if self.layer_enabled(lv.layer) {
270 lv.validator.validate_batch(segments, &mut report);
271 }
272 }
273 report
274 }
275
276 pub fn validate_strict(
278 &self,
279 segments: &[Segment<'_>],
280 ) -> Result<ValidationReport, EdifactError> {
281 let report = self.validate_lenient(segments);
282 if report.has_errors() {
283 let first_message = report
284 .errors
285 .first()
286 .map(|e| e.message.clone())
287 .unwrap_or_else(|| "unknown validation failure".to_owned());
288 return Err(EdifactError::ValidationFailed {
289 error_count: report.errors.len(),
290 first_message,
291 });
292 }
293 Ok(report)
294 }
295
296 pub fn message_type(&self) -> Option<&str> {
298 self.message_type.as_deref()
299 }
300
301 fn layer_enabled(&self, layer: ValidationLayer) -> bool {
302 match layer {
303 ValidationLayer::Structure => self.structure_enabled,
304 ValidationLayer::CodeList => self.code_list_enabled,
305 ValidationLayer::Profile => self.profile_enabled,
306 }
307 }
308}
309
310pub trait Validator: Send + Sync {
328 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport);
330
331 fn set_message_type(&mut self, _message_type: Option<&str>) {}
333}
334
335pub fn validate_each<F>(segments: &[Segment<'_>], report: &mut ValidationReport, mut f: F)
346where
347 F: FnMut(&Segment<'_>) -> Result<(), EdifactError>,
348{
349 for segment in segments {
350 if let Err(err) = f(segment) {
351 report_error(report, err);
352 }
353 }
354}
355
356pub(crate) fn report_error(report: &mut ValidationReport, err: EdifactError) {
358 let issue = issue_from_error(err);
359 match issue.severity {
360 ValidationSeverity::Critical | ValidationSeverity::Error => report.add_error(issue),
361 ValidationSeverity::Warning => report.add_warning(issue),
362 ValidationSeverity::Info => report.add_info(issue),
363 }
364}
365
366fn issue_from_error(err: EdifactError) -> ValidationIssue {
367 let code = err.stable_code();
368 let mut issue = ValidationIssue::new(severity_for(&err), err.to_string()).with_error_code(code);
369 let default_hint = err.recovery_hint();
370
371 match err {
372 EdifactError::InvalidSegmentForMessage { tag, offset, .. } => {
373 issue = issue.with_segment(tag).with_offset(offset);
374 }
375 EdifactError::InvalidElementCount { tag, offset, .. } => {
376 issue = issue.with_segment(tag).with_offset(offset);
377 }
378 EdifactError::InvalidComponentCount {
379 tag,
380 element_index,
381 offset,
382 ..
383 } => {
384 issue = issue
385 .with_segment(tag)
386 .with_element_index(element_index as u8)
387 .with_offset(offset);
388 }
389 EdifactError::InvalidCodeValue {
390 tag,
391 element_index,
392 offset,
393 suggestion,
394 ..
395 } => {
396 issue = issue
397 .with_segment(tag)
398 .with_element_index(element_index as u8)
399 .with_offset(offset);
400 if let Some(s) = suggestion {
401 issue = issue.with_suggestion(s);
402 }
403 }
404 EdifactError::MissingSegment { tag, .. } => {
405 issue = issue.with_segment(tag);
406 }
407 EdifactError::QualifierMismatch { tag, offset, .. } => {
408 issue = issue
409 .with_segment(tag)
410 .with_element_index(0)
411 .with_offset(offset);
412 }
413 EdifactError::ConditionalRequirementNotMet {
414 tag,
415 element_index,
416 offset,
417 ..
418 } => {
419 issue = issue
420 .with_segment(tag)
421 .with_element_index(element_index as u8)
422 .with_offset(offset);
423 }
424 EdifactError::MissingRequiredElement { tag, element_index } => {
425 issue = issue.with_segment(tag).with_element_index(element_index as u8);
426 }
427 EdifactError::InvalidReleaseSequence { offset }
428 | EdifactError::InvalidDelimiter { offset, .. }
429 | EdifactError::InvalidText { offset }
430 | EdifactError::UnexpectedEof { offset } => {
431 issue = issue.with_offset(offset);
432 }
433 _ => {}
434 }
435
436 if issue.suggestion.is_none() {
437 if let Some(hint) = default_hint {
438 issue = issue.with_suggestion(hint);
439 }
440 }
441
442 issue
443}
444
445fn severity_for(err: &EdifactError) -> ValidationSeverity {
446 match err {
447 EdifactError::InvalidCodeValue { .. } | EdifactError::QualifierMismatch { .. } => {
448 ValidationSeverity::Warning
449 }
450 _ => ValidationSeverity::Error,
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use crate::model::Element;
458
459 fn demo_orders_profile_pack() -> ProfileRulePack {
460 ProfileRulePack::builder("ORDERS-DEMO")
461 .for_message_type("ORDERS")
462 .with_rule_fn(|segments| {
463 let bgm = segments.iter().find(|segment| segment.tag == "BGM")?;
464 let document_code = bgm.get_element(0)?.get_component(0)?;
465 (document_code == "220").then(|| {
466 ValidationIssue::new(
467 ValidationSeverity::Error,
468 "profile rule DEMO-P001 violated: BGM document code 220 is rejected in this demo pack",
469 )
470 .with_rule_id("DEMO-P001")
471 .with_segment("BGM")
472 .with_element_index(0)
473 .with_suggestion("Use a different BGM document code in this demo pack")
474 })
475 })
476 .with_rule_fn(|segments| {
477 let bgm = segments.iter().find(|segment| segment.tag == "BGM")?;
478 let reference = bgm.get_element(1)?.get_component(0)?;
479 (reference == "PO123").then(|| {
480 ValidationIssue::new(
481 ValidationSeverity::Warning,
482 "profile rule DEMO-P002 warning: purchase-order reference PO123 is reserved in this demo pack",
483 )
484 .with_rule_id("DEMO-P002")
485 .with_segment("BGM")
486 .with_element_index(1)
487 .with_suggestion("Use a non-reserved reference in this demo pack")
488 })
489 })
490 }
491
492 struct RejectBgm;
493
494 struct WarnBgm;
495
496 impl Validator for RejectBgm {
497 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport) {
498 validate_each(segments, report, |segment| {
499 if segment.tag == "BGM" {
500 return Err(EdifactError::InvalidSegmentForMessage {
501 tag: "BGM".to_owned(),
502 message_type: "TEST".to_owned(),
503 offset: segment.tag_span.start,
504 });
505 }
506 Ok(())
507 });
508 }
509 }
510
511 impl Validator for WarnBgm {
512 fn validate_batch(&self, segments: &[Segment<'_>], report: &mut ValidationReport) {
513 validate_each(segments, report, |segment| {
514 if segment.tag == "BGM" {
515 return Err(EdifactError::InvalidCodeValue {
516 tag: "BGM".to_owned(),
517 element_index: 0,
518 value: "XXX".to_owned(),
519 code_list: "1001".to_owned(),
520 offset: segment.span.start,
521 suggestion: None,
522 });
523 }
524 Ok(())
525 });
526 }
527 }
528
529 fn test_segment(tag: &'static str) -> Segment<'static> {
530 Segment {
531 tag,
532 span: crate::Span::new(0, 0),
533 tag_span: crate::Span::new(0, 0),
534 elements: vec![Element::of(&["x"])],
535 }
536 }
537
538 #[test]
539 fn lenient_collects_issues() {
540 let segments = vec![test_segment("UNH"), test_segment("BGM")];
541 let mut report = ValidationReport::default();
542 RejectBgm.validate_batch(&segments, &mut report);
543 assert!(report.has_errors());
544 assert_eq!(report.errors.len(), 1);
545 }
546
547 #[test]
548 fn strict_fails_on_errors() {
549 let segments = vec![test_segment("BGM")];
550 let mut report = ValidationReport::default();
551 RejectBgm.validate_batch(&segments, &mut report);
552 assert!(report.has_errors());
553 assert_eq!(report.errors.len(), 1);
554 }
555
556 #[test]
557 fn context_builder_respects_layer_toggles() {
558 let segments = vec![test_segment("BGM")];
559 let ctx = ValidationContext::builder()
560 .structure(false)
561 .with_validator(ValidationLayer::Structure, RejectBgm)
562 .with_validator(ValidationLayer::CodeList, WarnBgm)
563 .build();
564
565 let report = ctx.validate_lenient(&segments);
566 assert!(!report.has_errors());
567 assert_eq!(report.warnings.len(), 1);
568 }
569
570 #[test]
571 fn context_strict_fails_when_structure_enabled() {
572 let segments = vec![test_segment("BGM")];
573 let ctx = ValidationContext::builder()
574 .with_message_type("ORDERS")
575 .with_validator(ValidationLayer::Structure, RejectBgm)
576 .build();
577
578 assert_eq!(ctx.message_type(), Some("ORDERS"));
579 let result = ctx.validate_strict(&segments);
580 assert!(matches!(result, Err(EdifactError::ValidationFailed { .. })));
581 }
582
583 #[test]
584 fn report_error_applies_default_recovery_hint() {
585 let mut report = ValidationReport::default();
586 report_error(
587 &mut report,
588 EdifactError::InvalidReleaseSequence { offset: 9 },
589 );
590
591 let issue = report
592 .errors
593 .first()
594 .expect("expected one issue in the report");
595 let hint = issue
596 .suggestion
597 .as_deref()
598 .expect("expected default hint to be set");
599 assert!(hint.contains("Release character"));
600 assert_eq!(issue.error_code, Some("E019"));
601 }
602
603 #[test]
604 fn profile_pack_lenient_collects_profile_rule_issues() {
605 let input = b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'";
606 let segments = crate::from_bytes(input)
607 .collect::<Result<Vec<_>, _>>()
608 .expect("expected parse success");
609
610 let ctx = ValidationContext::builder()
611 .with_profile_pack(demo_orders_profile_pack())
612 .build();
613
614 let report = ctx.validate_lenient(&segments);
615 assert!(report.has_errors());
616 assert!(
617 report
618 .errors
619 .iter()
620 .any(|issue| issue.rule_id.as_deref() == Some("DEMO-P001"))
621 );
622 assert!(
623 report
624 .warnings
625 .iter()
626 .any(|issue| issue.rule_id.as_deref() == Some("DEMO-P002"))
627 );
628 }
629
630 #[test]
631 fn profile_pack_strict_fails_when_profile_errors_exist() {
632 let input = b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'";
633 let segments = crate::from_bytes(input)
634 .collect::<Result<Vec<_>, _>>()
635 .expect("expected parse success");
636
637 let ctx = ValidationContext::builder()
638 .with_profile_pack(demo_orders_profile_pack())
639 .build();
640 let result = ctx.validate_strict(&segments);
641 assert!(matches!(result, Err(EdifactError::ValidationFailed { .. })));
642 }
643}