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