edifact-mapper 0.1.66

EDIFACT to BO4E bidirectional conversion for the German energy market
Documentation
//! Condition-aware PID validation.
//!
//! Extends the basic [`validate_pid_json`](mig_bo4e::pid_validation::validate_pid_json)
//! by evaluating AHB condition expressions to determine whether fields are truly
//! required based on the actual EDIFACT data context.

use automapper_validation::{
    ConditionEvaluator, ConditionExprEvaluator, ConditionResult, EvaluationContext,
};
use mig_bo4e::pid_requirements::{CodeValue, EntityRequirement, PidRequirements};
use mig_bo4e::pid_validation::{PidValidationError, Severity};
use serde_json::Value;

/// Validate a BO4E JSON value against PID requirements with AHB condition awareness.
///
/// Unlike [`validate_pid_json`](mig_bo4e::pid_validation::validate_pid_json), this
/// evaluates AHB condition expressions (e.g., `[1]`, `[2] ∧ [3]`) using the provided
/// evaluator and context to determine whether conditional fields are actually required.
///
/// - `X`, `Muss`, `Soll` are always required (Error if missing)
/// - `Kann`, empty string are never required (skipped)
/// - Condition expressions are evaluated: True = Error, False = skip, Unknown = Warning
pub fn validate_with_conditions<E: ConditionEvaluator>(
    json: &Value,
    requirements: &PidRequirements,
    expr_eval: &ConditionExprEvaluator<'_, E>,
    ctx: &EvaluationContext,
) -> Vec<PidValidationError> {
    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 entity_req.cardinality().is_list() {
                    if let Some(arr) = val.as_array() {
                        for element in arr {
                            validate_entity_fields_with_conditions(
                                element,
                                entity_req,
                                expr_eval,
                                ctx,
                                &mut errors,
                            );
                        }
                    } else {
                        // Single object where array is expected — validate as
                        // a single rep rather than silently skipping fields.
                        validate_entity_fields_with_conditions(
                            val,
                            entity_req,
                            expr_eval,
                            ctx,
                            &mut errors,
                        );
                    }
                } else {
                    validate_entity_fields_with_conditions(
                        val,
                        entity_req,
                        expr_eval,
                        ctx,
                        &mut errors,
                    );
                }
            }
        }
    }

    errors
}

/// Validate fields within a single entity using condition-aware evaluation.
fn validate_entity_fields_with_conditions<E: ConditionEvaluator>(
    entity_json: &Value,
    entity_req: &EntityRequirement,
    expr_eval: &ConditionExprEvaluator<'_, E>,
    ctx: &EvaluationContext,
    errors: &mut Vec<PidValidationError>,
) {
    for field_req in &entity_req.fields {
        match entity_json.get(&field_req.bo4e_name) {
            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) => {
                // Code validation happens unconditionally — valid codes don't depend
                // on conditions.
                if !field_req.valid_codes.is_empty() {
                    if let Some(value_str) = val.as_str() {
                        let is_valid = field_req.valid_codes.iter().any(|cv| cv.code == value_str);
                        if !is_valid {
                            errors.push(PidValidationError::InvalidCode {
                                entity: entity_req.entity.clone(),
                                field: field_req.bo4e_name.clone(),
                                value: value_str.to_string(),
                                valid_values: code_values_to_tuples(&field_req.valid_codes),
                            });
                        }
                    }
                }
            }
        }
    }
}

/// Evaluate an AHB status string to determine whether a field is required.
///
/// Returns:
/// - `Some(Severity::Error)` if unconditionally required or condition evaluates to True
/// - `Some(Severity::Warning)` if condition evaluates to Unknown
/// - `None` if not required (Kann/empty) or condition evaluates to False
fn evaluate_ahb_requirement<E: ConditionEvaluator>(
    ahb_status: &str,
    expr_eval: &ConditionExprEvaluator<'_, E>,
    ctx: &EvaluationContext,
) -> Option<Severity> {
    match ahb_status {
        "X" | "Muss" | "Soll" => Some(Severity::Error),
        "Kann" | "" => None,
        status => {
            // Use the expr_eval's built-in status evaluation which handles
            // parsing condition expressions like "[1]", "[2] ∧ [3]", etc.
            match expr_eval.evaluate_status(status, ctx) {
                ConditionResult::True => Some(Severity::Error),
                ConditionResult::False => None,
                ConditionResult::Unknown => Some(Severity::Warning),
            }
        }
    }
}

/// Convert PascalCase entity name to camelCase JSON key.
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
}

/// Convert CodeValue vec to (code, meaning) tuples.
fn code_values_to_tuples(codes: &[CodeValue]) -> Vec<(String, String)> {
    codes
        .iter()
        .map(|cv| (cv.code.clone(), cv.meaning.clone()))
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[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(""), "");
    }
}