use automapper_validation::eval::GroupScope;
use automapper_validation::{
refuses_presence, AbsentTarget, ConditionEvaluator, ConditionExprEvaluator, ConditionResult,
EvaluationContext,
};
use mig_bo4e::pid_requirements::{CodeValue, EntityRequirement, PidRequirements};
use mig_bo4e::pid_validation::{
effective_field_requirements, fields_of_unrequired_absent_groups, get_nested,
invalid_code_value, not_allowed_codes, PidValidationError, Severity,
};
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);
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() {
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,
&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,
&mut errors,
)
});
}
}
}
}
errors
}
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>,
errors: &mut Vec<PidValidationError>,
) {
if ctx.scope.is_some() && judges_presence(ctx) {
let forbidden = |status: &str| not_allowed(status, present, ctx);
let statuses: Vec<&str> = 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()
};
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() {
errors.extend(not_allowed_codes(
&entity_req.entity,
"",
&effective_field_requirements(entity_req, entity_json),
entity_json,
&forbidden,
));
}
}
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)
});
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),
});
}
}
}
}
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 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 => {
match expr_eval.evaluate_status(status, ctx) {
ConditionResult::True => Some(Severity::Error),
ConditionResult::False => None,
ConditionResult::Unknown => Some(Severity::Warning),
}
}
}
}
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(""), "");
}
}