Skip to main content

edifact_mapper/
conditional_validation.rs

1//! Condition-aware PID validation.
2//!
3//! Extends the basic [`validate_pid_json`](mig_bo4e::pid_validation::validate_pid_json)
4//! by evaluating AHB condition expressions to determine whether fields are truly
5//! required based on the actual EDIFACT data context.
6
7use automapper_validation::{
8    ConditionEvaluator, ConditionExprEvaluator, ConditionResult, EvaluationContext,
9};
10use mig_bo4e::pid_requirements::{CodeValue, EntityRequirement, PidRequirements};
11use mig_bo4e::pid_validation::{PidValidationError, Severity};
12use serde_json::Value;
13
14/// Validate a BO4E JSON value against PID requirements with AHB condition awareness.
15///
16/// Unlike [`validate_pid_json`](mig_bo4e::pid_validation::validate_pid_json), this
17/// evaluates AHB condition expressions (e.g., `[1]`, `[2] ∧ [3]`) using the provided
18/// evaluator and context to determine whether conditional fields are actually required.
19///
20/// - `X`, `Muss`, `Soll` are always required (Error if missing)
21/// - `Kann`, empty string are never required (skipped)
22/// - Condition expressions are evaluated: True = Error, False = skip, Unknown = Warning
23pub fn validate_with_conditions<E: ConditionEvaluator>(
24    json: &Value,
25    requirements: &PidRequirements,
26    expr_eval: &ConditionExprEvaluator<'_, E>,
27    ctx: &EvaluationContext,
28) -> Vec<PidValidationError> {
29    let mut errors = Vec::new();
30
31    for entity_req in &requirements.entities {
32        let key = to_camel_case(&entity_req.entity);
33
34        match json.get(&key) {
35            None => {
36                if let Some(severity) =
37                    evaluate_ahb_requirement(&entity_req.ahb_status, expr_eval, ctx)
38                {
39                    errors.push(PidValidationError::MissingEntity {
40                        entity: entity_req.entity.clone(),
41                        ahb_status: entity_req.ahb_status.clone(),
42                        severity,
43                    });
44                }
45            }
46            Some(val) => {
47                if entity_req.is_array {
48                    if let Some(arr) = val.as_array() {
49                        for element in arr {
50                            validate_entity_fields_with_conditions(
51                                element,
52                                entity_req,
53                                expr_eval,
54                                ctx,
55                                &mut errors,
56                            );
57                        }
58                    }
59                } else {
60                    validate_entity_fields_with_conditions(
61                        val,
62                        entity_req,
63                        expr_eval,
64                        ctx,
65                        &mut errors,
66                    );
67                }
68            }
69        }
70    }
71
72    errors
73}
74
75/// Validate fields within a single entity using condition-aware evaluation.
76fn validate_entity_fields_with_conditions<E: ConditionEvaluator>(
77    entity_json: &Value,
78    entity_req: &EntityRequirement,
79    expr_eval: &ConditionExprEvaluator<'_, E>,
80    ctx: &EvaluationContext,
81    errors: &mut Vec<PidValidationError>,
82) {
83    for field_req in &entity_req.fields {
84        match entity_json.get(&field_req.bo4e_name) {
85            None => {
86                if let Some(severity) =
87                    evaluate_ahb_requirement(&field_req.ahb_status, expr_eval, ctx)
88                {
89                    errors.push(PidValidationError::MissingField {
90                        entity: entity_req.entity.clone(),
91                        field: field_req.bo4e_name.clone(),
92                        ahb_status: field_req.ahb_status.clone(),
93                        rust_type: field_req.enum_name.clone(),
94                        valid_values: code_values_to_tuples(&field_req.valid_codes),
95                        severity,
96                    });
97                }
98            }
99            Some(val) => {
100                // Code validation happens unconditionally — valid codes don't depend
101                // on conditions.
102                if !field_req.valid_codes.is_empty() {
103                    if let Some(value_str) = val.as_str() {
104                        let is_valid = field_req.valid_codes.iter().any(|cv| cv.code == value_str);
105                        if !is_valid {
106                            errors.push(PidValidationError::InvalidCode {
107                                entity: entity_req.entity.clone(),
108                                field: field_req.bo4e_name.clone(),
109                                value: value_str.to_string(),
110                                valid_values: code_values_to_tuples(&field_req.valid_codes),
111                            });
112                        }
113                    }
114                }
115            }
116        }
117    }
118}
119
120/// Evaluate an AHB status string to determine whether a field is required.
121///
122/// Returns:
123/// - `Some(Severity::Error)` if unconditionally required or condition evaluates to True
124/// - `Some(Severity::Warning)` if condition evaluates to Unknown
125/// - `None` if not required (Kann/empty) or condition evaluates to False
126fn evaluate_ahb_requirement<E: ConditionEvaluator>(
127    ahb_status: &str,
128    expr_eval: &ConditionExprEvaluator<'_, E>,
129    ctx: &EvaluationContext,
130) -> Option<Severity> {
131    match ahb_status {
132        "X" | "Muss" | "Soll" => Some(Severity::Error),
133        "Kann" | "" => None,
134        status => {
135            // Use the expr_eval's built-in status evaluation which handles
136            // parsing condition expressions like "[1]", "[2] ∧ [3]", etc.
137            match expr_eval.evaluate_status(status, ctx) {
138                ConditionResult::True => Some(Severity::Error),
139                ConditionResult::False => None,
140                ConditionResult::Unknown => Some(Severity::Warning),
141            }
142        }
143    }
144}
145
146/// Convert PascalCase entity name to camelCase JSON key.
147fn to_camel_case(s: &str) -> String {
148    if s.is_empty() {
149        return String::new();
150    }
151    let mut chars = s.chars();
152    let first = chars.next().unwrap();
153    let mut result = first.to_lowercase().to_string();
154    result.extend(chars);
155    result
156}
157
158/// Convert CodeValue vec to (code, meaning) tuples.
159fn code_values_to_tuples(codes: &[CodeValue]) -> Vec<(String, String)> {
160    codes
161        .iter()
162        .map(|cv| (cv.code.clone(), cv.meaning.clone()))
163        .collect()
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_to_camel_case() {
172        assert_eq!(to_camel_case("Prozessdaten"), "prozessdaten");
173        assert_eq!(
174            to_camel_case("RuhendeMarktlokation"),
175            "ruhendeMarktlokation"
176        );
177        assert_eq!(to_camel_case(""), "");
178    }
179}