use super::ValidationRuleContext;
use super::Validator;
use crate::group::SegmentGroupIndexed;
use crate::{EdifactError, Segment, ValidationIssue, ValidationReport, ValidationSeverity};
use std::sync::Arc;
pub trait ProfileRule: Send + Sync {
fn evaluate(
&self,
segments: &[Segment<'_>],
context: &ValidationRuleContext<'_>,
issues: &mut Vec<ValidationIssue>,
);
}
struct ClosureProfileRule<F>(F);
impl<F> ProfileRule for ClosureProfileRule<F>
where
F: for<'a> Fn(&[Segment<'a>], &ValidationRuleContext<'_>, &mut Vec<ValidationIssue>)
+ Send
+ Sync,
{
fn evaluate(
&self,
segments: &[Segment<'_>],
context: &ValidationRuleContext<'_>,
issues: &mut Vec<ValidationIssue>,
) {
(self.0)(segments, context, issues);
}
}
struct StatelessClosureProfileRule<F>(F);
impl<F> ProfileRule for StatelessClosureProfileRule<F>
where
F: for<'a> Fn(&[Segment<'a>], &mut Vec<ValidationIssue>) + Send + Sync,
{
fn evaluate(
&self,
segments: &[Segment<'_>],
_context: &ValidationRuleContext<'_>,
issues: &mut Vec<ValidationIssue>,
) {
(self.0)(segments, issues);
}
}
pub(super) struct NamedRule {
pub(super) id: Option<Arc<str>>,
pub(super) rule: Arc<dyn ProfileRule + Send + Sync>,
}
impl Clone for NamedRule {
fn clone(&self) -> Self {
Self {
id: self.id.clone(),
rule: Arc::clone(&self.rule),
}
}
}
pub(super) struct NamedGroupRule {
pub(super) id: Option<Arc<str>>,
pub(super) group_scope: Option<&'static str>,
pub(super) rule: Arc<
dyn Fn(
&SegmentGroupIndexed,
&[Segment<'_>],
&ValidationRuleContext<'_>,
&mut Vec<ValidationIssue>,
) + Send
+ Sync,
>,
}
impl Clone for NamedGroupRule {
fn clone(&self) -> Self {
Self {
id: self.id.clone(),
group_scope: self.group_scope,
rule: Arc::clone(&self.rule),
}
}
}
pub struct ProfileRulePack {
name: String,
message_types: std::collections::BTreeSet<String>,
release: Option<String>,
pub(super) rules: Vec<NamedRule>,
pub(super) group_rules: Vec<NamedGroupRule>,
pub(super) bail_on_first_error: bool,
}
impl ProfileRulePack {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
message_types: std::collections::BTreeSet::new(),
release: None,
rules: Vec::new(),
group_rules: Vec::new(),
bail_on_first_error: false,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn message_types(&self) -> impl Iterator<Item = &str> {
self.message_types.iter().map(|s| s.as_str())
}
pub fn rule_count(&self) -> usize {
self.rules.len()
}
pub fn named_rule_count(&self) -> usize {
self.rules.iter().filter(|r| r.id.is_some()).count()
}
pub fn anonymous_rule_count(&self) -> usize {
self.rules.iter().filter(|r| r.id.is_none()).count()
}
pub fn rule_ids(&self) -> impl Iterator<Item = &str> {
self.rules.iter().filter_map(|r| r.id.as_deref())
}
pub fn release(&self) -> Option<&str> {
self.release.as_deref()
}
pub fn for_message_type(mut self, message_type: impl Into<String>) -> Self {
self.message_types.insert(message_type.into());
self
}
pub fn for_release(mut self, release: impl Into<String>) -> Self {
self.release = Some(release.into());
self
}
pub fn bail_on_first_error(mut self, bail: bool) -> Self {
self.bail_on_first_error = bail;
self
}
pub fn with_rule_fn<F>(mut self, rule: F) -> Self
where
F: for<'a> Fn(&[Segment<'a>], &ValidationRuleContext<'_>, &mut Vec<ValidationIssue>)
+ Send
+ Sync
+ 'static,
{
self.rules.push(NamedRule {
id: None,
rule: Arc::new(ClosureProfileRule(rule)),
});
self
}
pub fn with_named_rule_fn<F>(mut self, id: impl Into<Arc<str>>, rule: F) -> Self
where
F: for<'a> Fn(&[Segment<'a>], &ValidationRuleContext<'_>, &mut Vec<ValidationIssue>)
+ Send
+ Sync
+ 'static,
{
self.rules.push(NamedRule {
id: Some(id.into()),
rule: Arc::new(ClosureProfileRule(rule)),
});
self
}
pub fn with_stateless_rule_fn<F>(mut self, rule: F) -> Self
where
F: for<'a> Fn(&[Segment<'a>], &mut Vec<ValidationIssue>) + Send + Sync + 'static,
{
self.rules.push(NamedRule {
id: None,
rule: Arc::new(StatelessClosureProfileRule(rule)),
});
self
}
pub fn with_named_stateless_rule_fn<F>(mut self, id: impl Into<Arc<str>>, rule: F) -> Self
where
F: for<'a> Fn(&[Segment<'a>], &mut Vec<ValidationIssue>) + Send + Sync + 'static,
{
self.rules.push(NamedRule {
id: Some(id.into()),
rule: Arc::new(StatelessClosureProfileRule(rule)),
});
self
}
pub fn require_segment(self, tag: &'static str, rule_id: impl Into<Arc<str>>) -> Self {
let id: Arc<str> = rule_id.into();
self.with_named_stateless_rule_fn(id.clone(), move |segments, issues| {
if !segments.iter().any(|s| s.tag == tag) {
issues.push(
ValidationIssue::new(
ValidationSeverity::Error,
format!("mandatory segment {tag} is missing"),
)
.with_segment(tag)
.with_rule_id(id.as_ref()),
);
}
})
}
pub fn forbid_segment(self, tag: &'static str, rule_id: impl Into<Arc<str>>) -> Self {
let id: Arc<str> = rule_id.into();
self.with_named_stateless_rule_fn(id.clone(), move |segments, issues| {
for (occ, _s) in segments.iter().filter(|s| s.tag == tag).enumerate() {
issues.push(
ValidationIssue::new(
ValidationSeverity::Error,
format!("segment {tag} must not appear"),
)
.with_segment(tag)
.with_segment_occurrence(u16::try_from(occ).unwrap_or(u16::MAX))
.with_rule_id(id.as_ref()),
);
}
})
}
pub fn require_qualifier(
self,
tag: &'static str,
element: u8,
component: u8,
qualifier: &'static str,
rule_id: impl Into<Arc<str>>,
) -> Self {
let id: Arc<str> = rule_id.into();
self.with_named_stateless_rule_fn(id.clone(), move |segments, issues| {
for (occ, s) in segments.iter().filter(|s| s.tag == tag).enumerate() {
let actual = s
.get_element(element as usize)
.and_then(|e| e.get_component(component as usize));
if actual != Some(qualifier) {
issues.push(
ValidationIssue::new(
ValidationSeverity::Error,
format!(
"segment {tag} element {element} component {component} must be \
{qualifier:?} but found {:?}",
actual.unwrap_or("<absent>")
),
)
.with_segment(tag)
.with_element_index(element)
.with_component_index(component)
.with_segment_occurrence(u16::try_from(occ).unwrap_or(u16::MAX))
.with_rule_id(id.as_ref()),
);
}
}
})
}
pub fn with_group_rule_fn<F>(mut self, rule: F) -> Self
where
F: Fn(
&SegmentGroupIndexed,
&[Segment<'_>],
&ValidationRuleContext<'_>,
&mut Vec<ValidationIssue>,
) + Send
+ Sync
+ 'static,
{
self.group_rules.push(NamedGroupRule {
id: None,
group_scope: None,
rule: Arc::new(rule),
});
self
}
pub fn with_named_group_rule_fn<F>(mut self, id: impl Into<Arc<str>>, rule: F) -> Self
where
F: Fn(
&SegmentGroupIndexed,
&[Segment<'_>],
&ValidationRuleContext<'_>,
&mut Vec<ValidationIssue>,
) + Send
+ Sync
+ 'static,
{
self.group_rules.push(NamedGroupRule {
id: Some(id.into()),
group_scope: None,
rule: Arc::new(rule),
});
self
}
pub fn with_scoped_group_rule_fn<F>(
mut self,
group_scope: &'static str,
id: impl Into<Arc<str>>,
rule: F,
) -> Self
where
F: Fn(
&SegmentGroupIndexed,
&[Segment<'_>],
&ValidationRuleContext<'_>,
&mut Vec<ValidationIssue>,
) + Send
+ Sync
+ 'static,
{
self.group_rules.push(NamedGroupRule {
id: Some(id.into()),
group_scope: Some(group_scope),
rule: Arc::new(rule),
});
self
}
pub fn require_segment_in_group(
self,
group_scope: &'static str,
tag: &'static str,
rule_id: impl Into<Arc<str>>,
) -> Self {
let id: Arc<str> = rule_id.into();
self.with_scoped_group_rule_fn(
group_scope,
id.clone(),
move |_group, segs, _ctx, issues| {
if !segs.iter().any(|s| s.tag == tag) {
issues.push(
ValidationIssue::new(
ValidationSeverity::Error,
format!("mandatory segment {tag} is missing from group {group_scope}"),
)
.with_segment(tag)
.with_rule_id(id.as_ref()),
);
}
},
)
}
pub fn forbid_segment_in_group(
self,
group_scope: &'static str,
tag: &'static str,
rule_id: impl Into<Arc<str>>,
) -> Self {
let id: Arc<str> = rule_id.into();
self.with_scoped_group_rule_fn(
group_scope,
id.clone(),
move |_group, segs, _ctx, issues| {
for (occ, _s) in segs.iter().filter(|s| s.tag == tag).enumerate() {
issues.push(
ValidationIssue::new(
ValidationSeverity::Error,
format!("segment {tag} must not appear in group {group_scope}"),
)
.with_segment(tag)
.with_segment_occurrence(u16::try_from(occ).unwrap_or(u16::MAX))
.with_rule_id(id.as_ref()),
);
}
},
)
}
pub fn require_qualifier_in_group(
self,
group_scope: &'static str,
tag: &'static str,
element: u8,
component: u8,
qualifier: &'static str,
rule_id: impl Into<Arc<str>>,
) -> Self {
let id: Arc<str> = rule_id.into();
self.with_scoped_group_rule_fn(
group_scope,
id.clone(),
move |_group, segs, _ctx, issues| {
for (occ, s) in segs.iter().filter(|s| s.tag == tag).enumerate() {
let actual = s
.get_element(element as usize)
.and_then(|e| e.get_component(component as usize));
if actual != Some(qualifier) {
issues.push(
ValidationIssue::new(
ValidationSeverity::Error,
format!(
"segment {tag} element {element} component {component} must be \
{qualifier:?} in group {group_scope}, found {:?}",
actual.unwrap_or("<absent>")
),
)
.with_segment(tag)
.with_element_index(element)
.with_component_index(component)
.with_segment_occurrence(u16::try_from(occ).unwrap_or(u16::MAX))
.with_rule_id(id.as_ref()),
);
}
}
},
)
}
pub fn group_rule_count(&self) -> usize {
self.group_rules.len()
}
fn walk_group_tree(
&self,
group: &SegmentGroupIndexed,
all_segments: &[Segment<'_>],
report: &mut ValidationReport,
context: &ValidationRuleContext<'_>,
) {
let group_segs = all_segments.get(group.total_span.clone()).unwrap_or(&[]);
let mut rule_issues: Vec<ValidationIssue> = Vec::new();
for named in &self.group_rules {
if let Some(scope) = named.group_scope {
if group.definition != scope {
continue;
}
}
let errors_before = report.errors.len();
(named.rule)(group, group_segs, context, &mut rule_issues);
for mut issue in rule_issues.drain(..) {
if issue.segment_group.is_none() {
issue = issue.with_segment_group(group.definition);
}
match issue.severity {
ValidationSeverity::Critical | ValidationSeverity::Error => {
report.add_error(issue);
}
ValidationSeverity::Warning => {
report.add_warning(issue);
}
ValidationSeverity::Info => {
report.add_info(issue);
}
}
}
if self.bail_on_first_error && report.errors.len() > errors_before {
return;
}
}
for child in &group.children {
let errors_before_child = report.errors.len();
self.walk_group_tree(child, all_segments, report, context);
if self.bail_on_first_error && report.errors.len() > errors_before_child {
return;
}
}
}
pub fn with_rule(mut self, rule: impl ProfileRule + 'static) -> Self {
self.rules.push(NamedRule {
id: None,
rule: Arc::new(rule),
});
self
}
pub fn with_named_rule(
mut self,
id: impl Into<Arc<str>>,
rule: impl ProfileRule + 'static,
) -> Self {
self.rules.push(NamedRule {
id: Some(id.into()),
rule: Arc::new(rule),
});
self
}
pub fn extend_from(mut self, base: &ProfileRulePack) -> Result<Self, EdifactError> {
let mut combined = base.rules.clone();
combined.append(&mut self.rules);
self.rules = combined;
let mut combined_group = base.group_rules.clone();
combined_group.append(&mut self.group_rules);
self.group_rules = combined_group;
for mt in &base.message_types {
self.message_types.insert(mt.clone());
}
self.release = merge_release_scopes(self.release.take(), base.release.clone())?;
Ok(self)
}
pub fn merge_with_override(mut self, mut other: Self) -> Result<Self, EdifactError> {
let mut id_to_index: std::collections::HashMap<Arc<str>, usize> = Default::default();
for (idx, rule) in self.rules.iter().enumerate() {
if let Some(id) = &rule.id {
id_to_index.insert(id.clone(), idx);
}
}
let mut replacements: Vec<(usize, NamedRule)> = Vec::new();
let mut to_append = Vec::new();
for other_rule in other.rules.drain(..) {
if let Some(id) = &other_rule.id {
if let Some(&idx) = id_to_index.get(id) {
replacements.push((idx, other_rule));
} else {
to_append.push(other_rule);
}
} else {
to_append.push(other_rule);
}
}
for (idx, rule) in replacements {
if idx < self.rules.len() {
self.rules[idx] = rule;
}
}
self.rules.append(&mut to_append);
self.message_types.append(&mut other.message_types);
let mut group_id_to_index: std::collections::HashMap<Arc<str>, usize> = Default::default();
for (idx, rule) in self.group_rules.iter().enumerate() {
if let Some(id) = &rule.id {
group_id_to_index.insert(id.clone(), idx);
}
}
let mut group_replacements: Vec<(usize, NamedGroupRule)> = Vec::new();
let mut group_to_append = Vec::new();
for other_rule in other.group_rules.drain(..) {
if let Some(id) = &other_rule.id {
if let Some(&idx) = group_id_to_index.get(id) {
group_replacements.push((idx, other_rule));
} else {
group_to_append.push(other_rule);
}
} else {
group_to_append.push(other_rule);
}
}
for (idx, rule) in group_replacements {
if idx < self.group_rules.len() {
self.group_rules[idx] = rule;
}
}
self.group_rules.append(&mut group_to_append);
self.release = merge_release_scopes(self.release.take(), other.release.take())?;
Ok(self)
}
}
pub(super) fn merge_release_scopes(
current: Option<String>,
incoming: Option<String>,
) -> Result<Option<String>, EdifactError> {
match (current, incoming) {
(Some(x), Some(y)) if x != y => Err(EdifactError::IncompatibleReleaseScopes {
current: x,
incoming: y,
}),
(Some(x), Some(_)) => Ok(Some(x)),
(Some(x), None) => Ok(Some(x)),
(None, incoming) => Ok(incoming),
}
}
impl Validator for ProfileRulePack {
fn validate_batch(
&self,
segments: &[Segment<'_>],
report: &mut ValidationReport,
context: &ValidationRuleContext<'_>,
) {
let unh_e1_storage;
let unh_e1: Option<&crate::model::Element<'_>> = if context.message_type.is_some() {
None
} else {
unh_e1_storage = segments
.iter()
.find(|s| s.tag == "UNH")
.and_then(|s| s.get_element(1));
unh_e1_storage
};
let message_type = context
.message_type
.or_else(|| unh_e1.and_then(|e| e.get_component(0)));
if !self.message_types.is_empty()
&& !message_type.is_some_and(|mt| self.message_types.contains(mt))
{
return;
}
if let Some(bound_release) = &self.release {
let msg_association = segments
.iter()
.find(|s| s.tag == "UNH")
.and_then(|s| s.get_element(1))
.and_then(|e| e.get_component(4));
if msg_association != Some(bound_release.as_str()) {
return;
}
}
let mut rule_issues: Vec<ValidationIssue> = Vec::new();
for named in &self.rules {
let errors_before = report.errors.len();
named.rule.evaluate(segments, context, &mut rule_issues);
for issue in rule_issues.drain(..) {
match issue.severity {
ValidationSeverity::Critical | ValidationSeverity::Error => {
report.add_error(issue);
}
ValidationSeverity::Warning => {
report.add_warning(issue);
}
ValidationSeverity::Info => {
report.add_info(issue);
}
}
}
if self.bail_on_first_error && report.errors.len() > errors_before {
return;
}
}
}
fn validate_group_batch(
&self,
root: &SegmentGroupIndexed,
all_segments: &[Segment<'_>],
report: &mut ValidationReport,
context: &ValidationRuleContext<'_>,
) {
if self.group_rules.is_empty() {
return;
}
let unh_e1_storage;
let unh_e1: Option<&crate::model::Element<'_>> = if context.message_type.is_some() {
None
} else {
unh_e1_storage = all_segments
.iter()
.find(|s| s.tag == "UNH")
.and_then(|s| s.get_element(1));
unh_e1_storage
};
let message_type = context
.message_type
.or_else(|| unh_e1.and_then(|e| e.get_component(0)));
if !self.message_types.is_empty()
&& !message_type.is_some_and(|mt| self.message_types.contains(mt))
{
return;
}
if let Some(bound_release) = &self.release {
let msg_association = all_segments
.iter()
.find(|s| s.tag == "UNH")
.and_then(|s| s.get_element(1))
.and_then(|e| e.get_component(4));
if msg_association != Some(bound_release.as_str()) {
return;
}
}
self.walk_group_tree(root, all_segments, report, context);
}
fn fork(&self) -> Box<dyn Validator + Send + Sync> {
Box::new(self.clone())
}
}
impl Clone for ProfileRulePack {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
message_types: self.message_types.clone(),
release: self.release.clone(),
rules: self.rules.clone(),
group_rules: self.group_rules.clone(),
bail_on_first_error: self.bail_on_first_error,
}
}
}
impl std::fmt::Debug for ProfileRulePack {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProfileRulePack")
.field("name", &self.name)
.field("message_types", &self.message_types)
.field("release", &self.release)
.field("rule_count", &self.rules.len())
.field("group_rule_count", &self.group_rules.len())
.field("bail_on_first_error", &self.bail_on_first_error)
.finish()
}
}
impl Validator for Arc<ProfileRulePack> {
fn validate_batch(
&self,
segments: &[Segment<'_>],
report: &mut ValidationReport,
context: &ValidationRuleContext<'_>,
) {
self.as_ref().validate_batch(segments, report, context);
}
fn validate_group_batch(
&self,
root: &SegmentGroupIndexed,
all_segments: &[Segment<'_>],
report: &mut ValidationReport,
context: &ValidationRuleContext<'_>,
) {
self.as_ref()
.validate_group_batch(root, all_segments, report, context);
}
fn fork(&self) -> Box<dyn Validator + Send + Sync> {
Box::new(Arc::clone(self))
}
}