use automapper_validation::eval::GroupScope;
use automapper_validation::{packages_allow_none, requiredness};
use automapper_validation::{
refuses_presence, AbsentTarget, ConditionEvaluator, ConditionExprEvaluator, ConditionResult,
EvaluationContext,
};
use mig_bo4e::pid_requirements::{
CodeValue, EntityRequirement, FieldRequirement, FieldSegment, PidRequirements,
};
use mig_bo4e::pid_validation::{
code_field_value, effective_field_requirements, fields_of_unrequired_absent_groups, get_nested,
invalid_code_value, not_allowed_codes, PidValidationError, Severity,
};
use mig_types::segment::OwnedSegment;
use serde_json::Value;
use std::collections::HashMap;
use crate::element_scopes::{ElementKey, InstancePath, NestedKey};
pub fn validate_with_conditions<E: ConditionEvaluator>(
json: &Value,
requirements: &PidRequirements,
expr_eval: &ConditionExprEvaluator<'_, E>,
ctx: &EvaluationContext,
) -> Vec<PidValidationError> {
validate(json, requirements, expr_eval, ctx, None, None)
}
fn validate<E: ConditionEvaluator>(
json: &Value,
requirements: &PidRequirements,
expr_eval: &ConditionExprEvaluator<'_, E>,
ctx: &EvaluationContext,
scopes: Option<&HashMap<ElementKey, InstancePath>>,
nested: Option<&HashMap<NestedKey, InstancePath>>,
) -> Vec<PidValidationError> {
let present = expr_eval;
let absent = AbsentTarget(expr_eval.evaluator());
let expr_eval = &ConditionExprEvaluator::new(&absent);
let mut errors = Vec::new();
for entity_req in &requirements.entities {
let key = to_camel_case(&entity_req.entity);
let judge_values = !(requirements.transaktion_wiederholbar
&& entity_req.scope == mig_bo4e::pid_requirements::EntityScope::Message);
match json.get(&key) {
None => {
if let Some(severity) =
evaluate_ahb_requirement(&entity_req.ahb_status, expr_eval, ctx)
{
errors.push(PidValidationError::MissingEntity {
entity: entity_req.entity.clone(),
ahb_status: entity_req.ahb_status.clone(),
severity,
});
}
}
Some(val) => {
if let Some(arr) = val.as_array() {
if let Some((once, _)) = automapper_validation::once_per_transaction(
expr_eval.evaluator().message_type(),
) {
errors.extend(repeated_in_transaction(entity_req, arr, once));
}
for (index, element) in arr.iter().enumerate() {
let path = scope_of(scopes, &key, Some(index));
let at = nested_scope_of(nested, &key, Some(index));
in_scope(ctx, path.as_deref(), |ctx| {
validate_entity_fields_with_conditions(
element,
entity_req,
expr_eval,
present,
ctx,
&at,
judge_values,
&mut errors,
)
});
}
} else {
let path = scope_of(scopes, &key, None);
let at = nested_scope_of(nested, &key, None);
in_scope(ctx, path.as_deref(), |ctx| {
validate_entity_fields_with_conditions(
val,
entity_req,
expr_eval,
present,
ctx,
&at,
judge_values,
&mut errors,
)
});
}
}
}
}
errors
}
fn repeated_in_transaction(
entity_req: &EntityRequirement,
elements: &[Value],
once: u32,
) -> Vec<PidValidationError> {
let mut counts: std::collections::BTreeMap<&str, usize> = std::collections::BTreeMap::new();
for element in elements {
let statuses = element_statuses(entity_req, element);
for status in statuses {
if automapper_validation::binds_once(status, once) {
*counts.entry(status).or_insert(0) += 1;
}
}
}
counts
.into_iter()
.filter(|(_, count)| *count > 1)
.map(
|(status, count)| PidValidationError::RepeatedInTransaction {
entity: entity_req.entity.clone(),
count,
ahb_status: status.to_string(),
},
)
.collect()
}
fn scope_of<'s>(
scopes: Option<&'s HashMap<ElementKey, InstancePath>>,
key: &str,
index: Option<usize>,
) -> Option<Vec<(&'s str, usize)>> {
scopes?
.get(&(key.to_string(), index))
.map(|path| path.iter().map(|(g, i)| (g.as_str(), *i)).collect())
}
fn nested_scope_of<'s>(
nested: Option<&'s HashMap<NestedKey, InstancePath>>,
key: &str,
index: Option<usize>,
) -> impl Fn(&[(String, usize)]) -> Option<&'s InstancePath> + 's {
let element: ElementKey = (key.to_string(), index);
move |position: &[(String, usize)]| nested?.get(&(element.clone(), position.to_vec()))
}
fn in_scope<R>(
ctx: &EvaluationContext,
path: Option<&[(&str, usize)]>,
f: impl FnOnce(&EvaluationContext) -> R,
) -> R {
match path {
Some(path) => f(&ctx.with_scope(GroupScope { path })),
None => f(ctx),
}
}
pub fn validate_with_conditions_in_scopes<E: ConditionEvaluator>(
json: &Value,
requirements: &PidRequirements,
expr_eval: &ConditionExprEvaluator<'_, E>,
ctx: &EvaluationContext,
scopes: &HashMap<ElementKey, InstancePath>,
nested: &HashMap<NestedKey, InstancePath>,
) -> Vec<PidValidationError> {
validate(
json,
requirements,
expr_eval,
ctx,
Some(scopes),
Some(nested),
)
}
#[allow(clippy::too_many_arguments)]
fn validate_entity_fields_with_conditions<'s, E: ConditionEvaluator, P: ConditionEvaluator>(
entity_json: &Value,
entity_req: &EntityRequirement,
expr_eval: &ConditionExprEvaluator<'_, E>,
present: &ConditionExprEvaluator<'_, P>,
ctx: &EvaluationContext,
nested_at: &dyn Fn(&[(String, usize)]) -> Option<&'s InstancePath>,
judge_values: bool,
errors: &mut Vec<PidValidationError>,
) {
if ctx.scope.is_some() && judges_presence(ctx) {
let forbidden = |status: &str| not_allowed(status, present, ctx);
let statuses = element_statuses(entity_req, entity_json);
let refused: Vec<&str> = statuses.into_iter().filter(|s| forbidden(s)).collect();
for status in &refused {
errors.push(PidValidationError::NotAllowed {
entity: entity_req.entity.clone(),
field: String::new(),
value: None,
ahb_status: status.to_string(),
severity: Severity::Warning,
});
}
if refused.is_empty() {
let fields = effective_field_requirements(entity_req, entity_json);
errors.extend(not_allowed_codes(
&entity_req.entity,
"",
&fields,
entity_json,
&forbidden,
));
for field_req in fields.iter() {
let present =
get_nested(entity_json, &field_req.bo4e_name).is_some_and(|v| !v.is_null());
if present
&& field_req.ahb_status != field_req.element_status
&& forbidden(&field_req.ahb_status)
&& !forbidden(&field_req.element_status)
{
errors.push(PidValidationError::NotAllowed {
entity: entity_req.entity.clone(),
field: field_req.bo4e_name.clone(),
value: None,
ahb_status: field_req.ahb_status.clone(),
severity: Severity::Warning,
});
}
}
}
}
let fields = effective_field_requirements(entity_req, entity_json);
let exempt = fields_of_unrequired_absent_groups(entity_req, entity_json, |status| {
evaluate_ahb_requirement(status, expr_eval, ctx) == Some(Severity::Error)
});
let group_applies = element_statuses(entity_req, entity_json)
.iter()
.all(|status| present.evaluate_status(status, ctx) == ConditionResult::True);
for field_req in fields.iter() {
match get_nested(entity_json, &field_req.bo4e_name).filter(|v| !v.is_null()) {
None if exempt.contains(field_req.bo4e_name.as_str()) => {}
None => {
if let Some(severity) =
evaluate_ahb_requirement(&field_req.ahb_status, expr_eval, ctx)
{
errors.push(PidValidationError::MissingField {
entity: entity_req.entity.clone(),
field: field_req.bo4e_name.clone(),
ahb_status: field_req.ahb_status.clone(),
rust_type: field_req.enum_name.clone(),
valid_values: code_values_to_tuples(&field_req.valid_codes),
severity,
});
}
}
Some(val) => {
if let Some(value) = invalid_code_value(val, field_req) {
errors.push(PidValidationError::InvalidCode {
entity: entity_req.entity.clone(),
field: field_req.bo4e_name.clone(),
value,
valid_values: code_values_to_tuples(&field_req.valid_codes),
});
} else if group_applies
&& judge_values
&& breaks_its_condition(field_req, val, present, ctx)
{
errors.push(PidValidationError::ConditionNotSatisfied {
entity: entity_req.entity.clone(),
field: field_req.bo4e_name.clone(),
value: code_field_value(val).unwrap_or_default().to_string(),
ahb_status: field_req.element_status.clone(),
});
}
errors.extend(mig_bo4e::pid_validation::too_long(
&entity_req.entity,
&field_req.bo4e_name,
val,
field_req,
));
errors.extend(mig_bo4e::pid_validation::not_a_number(
&entity_req.entity,
&field_req.bo4e_name,
val,
field_req,
));
}
}
}
mig_bo4e::pid_validation::validate_nested_lists(
&entity_req.entity,
entity_json,
&entity_req.nested,
"",
&|status, position| match nested_at(position).filter(|_| judges_presence(ctx)) {
Some(path) => {
let path: Vec<(&str, usize)> = path.iter().map(|(g, i)| (g.as_str(), *i)).collect();
evaluate_ahb_requirement(
status,
expr_eval,
&ctx.with_scope(GroupScope { path: &path }),
)
}
None => evaluate_ahb_requirement(status, expr_eval, ctx),
},
&|variant| variant_holds(ctx, variant),
&|status, position| match nested_at(position).filter(|_| judges_presence(ctx)) {
Some(path) => {
let path: Vec<(&str, usize)> = path.iter().map(|(g, i)| (g.as_str(), *i)).collect();
not_allowed(status, present, &ctx.with_scope(GroupScope { path: &path }))
}
None => false,
},
errors,
);
}
fn element_statuses<'r>(entity_req: &'r EntityRequirement, entity_json: &Value) -> Vec<&'r str> {
let variants = mig_bo4e::pid_validation::variant_statuses(entity_req, entity_json);
if entity_req.groups.is_empty() && !variants.is_empty() {
variants
} else if entity_req.groups.is_empty() {
vec![entity_req.ahb_status.as_str()]
} else {
entity_req
.groups
.iter()
.filter(|g| {
g.fields.iter().any(|f| {
entity_req
.groups
.iter()
.filter(|other| other.source_path != g.source_path)
.all(|other| !other.fields.contains(f))
&& get_nested(entity_json, f).is_some_and(|v| !v.is_null())
})
})
.map(|g| g.ahb_status.as_str())
.collect()
}
}
fn judges_presence(ctx: &EvaluationContext) -> bool {
!ctx.find_segments("BGM").is_empty()
}
fn not_allowed<E: ConditionEvaluator>(
status: &str,
eval: &ConditionExprEvaluator<'_, E>,
ctx: &EvaluationContext,
) -> bool {
refuses_presence(status, eval.evaluator(), ctx, &Default::default())
}
fn evaluate_ahb_requirement<E: ConditionEvaluator>(
ahb_status: &str,
expr_eval: &ConditionExprEvaluator<'_, E>,
ctx: &EvaluationContext,
) -> Option<Severity> {
match ahb_status.trim() {
"X" | "Muss" | "Soll" => Some(Severity::Error),
"Kann" | "K" | "" => None,
status if status.starts_with("Kann") || status.starts_with("K ") => None,
status if status.contains('\n') => severity_of(expr_eval.evaluate_status(status, ctx)),
status => {
let Ok(Some(expr)) = automapper_validation::ConditionParser::parse(status) else {
return severity_of(expr_eval.evaluate_status(status, ctx));
};
if packages_allow_none(&expr) {
return None;
}
match requiredness(&expr) {
Some(expr) => severity_of(expr_eval.evaluate(&expr, ctx)),
None if matches!(status_keyword(status), "X" | "Muss" | "M" | "Soll" | "S") => {
Some(Severity::Error)
}
None => None,
}
}
}
}
fn severity_of(result: ConditionResult) -> Option<Severity> {
match result {
ConditionResult::True => Some(Severity::Error),
ConditionResult::False => None,
ConditionResult::Unknown => Some(Severity::Warning),
}
}
fn status_keyword(status: &str) -> &str {
let s = status.trim_start();
&s[..s.find(|c: char| !c.is_alphabetic()).unwrap_or(s.len())]
}
fn breaks_its_condition<E: ConditionEvaluator>(
field_req: &FieldRequirement,
val: &Value,
eval: &ConditionExprEvaluator<'_, E>,
ctx: &EvaluationContext,
) -> bool {
let context = field_req.context_status.trim();
if context.contains('\n')
|| (context.contains('[') && eval.evaluate_status(context, ctx) != ConditionResult::True)
{
return false;
}
let Some(value) = code_field_value(val) else {
return false;
};
let raw = field_req
.valid_codes
.iter()
.find(|cv| cv.bo4e_value.as_deref() == Some(value))
.map_or(value, |cv| cv.code.as_str());
let status = status_of_the_code_sent(field_req, raw);
let package = status.split('[').skip(1).any(|term| {
term.trim_start_matches(|c: char| c.is_ascii_digit())
.starts_with('P')
});
if field_req.bo4e_name.contains("[]")
|| status.contains('\n')
|| !status.contains('[')
|| package
|| !matches!(status_keyword(status), "X" | "Muss")
|| !matches!(
status_keyword(&field_req.ahb_status),
"X" | "Muss" | "M" | "Soll" | "S"
)
{
return false;
}
let segment = field_req
.segment
.as_ref()
.and_then(|at| segment_of(at, ctx));
let at_value = EvaluationContext {
resolved_value: Some(raw),
resolved_segment: segment.as_ref().map(|s| s.elements.as_slice()),
..*ctx
};
eval.evaluate_status(status, &at_value) == ConditionResult::False
}
fn status_of_the_code_sent<'r>(field_req: &'r FieldRequirement, raw: &str) -> &'r str {
let status = field_req.element_status.trim();
let first = field_req
.valid_codes
.first()
.and_then(|cv| cv.ahb_status.as_deref());
if !first.is_some_and(|s| s.trim() == status) {
return status;
}
field_req
.valid_codes
.iter()
.find(|cv| cv.code == raw)
.map_or(status, |cv| cv.ahb_status.as_deref().unwrap_or("").trim())
}
fn segment_of(at: &FieldSegment, ctx: &EvaluationContext) -> Option<OwnedSegment> {
let selected = |s: &OwnedSegment| {
at.codes.is_empty()
|| at
.codes
.iter()
.any(|c| s.get_component(at.site.0, at.site.1) == c)
};
let pick =
|segments: Vec<OwnedSegment>| segments.into_iter().filter(selected).nth(at.occurrence);
match (ctx.scope, ctx.navigator()) {
(Some(scope), Some(nav)) => pick(nav.find_segments_in_instance(&at.tag, scope.path))
.or_else(|| pick(nav.find_segments_in_subtree(&at.tag, scope.path))),
_ => pick(ctx.find_segments(&at.tag).into_iter().cloned().collect()),
}
}
fn to_camel_case(s: &str) -> String {
if s.is_empty() {
return String::new();
}
let mut chars = s.chars();
let first = chars.next().unwrap();
let mut result = first.to_lowercase().to_string();
result.extend(chars);
result
}
fn code_values_to_tuples(codes: &[CodeValue]) -> Vec<(String, String)> {
codes
.iter()
.map(|cv| (cv.code.clone(), cv.meaning.clone()))
.collect()
}
fn variant_holds(
ctx: &EvaluationContext,
variant: &mig_bo4e::pid_requirements::VariantQualifier,
) -> Option<bool> {
let scope = ctx.scope?;
let level = scope
.path
.iter()
.position(|(group, _)| group.eq_ignore_ascii_case(&variant.group))?;
let entry = ctx
.find_segments_at(&scope.path[..=level], &variant.segment)
.into_iter()
.next()?;
Some(
variant
.codes
.iter()
.any(|code| code == entry.get_element(0)),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn optional_statuses_are_never_required() {
use automapper_validation::MapExternalProvider;
struct NoConditions;
impl ConditionEvaluator for NoConditions {
fn evaluate(&self, _: u32, _: &EvaluationContext) -> ConditionResult {
ConditionResult::True
}
fn is_external(&self, _: u32) -> bool {
false
}
fn message_type(&self) -> &str {
"UTILMD"
}
fn format_version(&self) -> &str {
"FV2604"
}
}
let evaluator = NoConditions;
let expr_eval = ConditionExprEvaluator::new(&evaluator);
let external = MapExternalProvider::new(Default::default());
let ctx = EvaluationContext::new("55042", &external, &[]);
for status in ["K", "Kann", "", "Kann [1]", "K [1]"] {
assert_eq!(
evaluate_ahb_requirement(status, &expr_eval, &ctx),
None,
"{status:?} must be optional"
);
}
assert_eq!(
evaluate_ahb_requirement("X", &expr_eval, &ctx),
Some(Severity::Error)
);
}
#[test]
fn test_to_camel_case() {
assert_eq!(to_camel_case("Prozessdaten"), "prozessdaten");
assert_eq!(
to_camel_case("RuhendeMarktlokation"),
"ruhendeMarktlokation"
);
assert_eq!(to_camel_case(""), "");
}
}