1use std::fmt;
4
5use serde_json::Value;
6
7use crate::pid_requirements::{CodeValue, EntityRequirement, FieldRequirement, PidRequirements};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum Severity {
12 Error,
14 Warning,
16}
17
18#[derive(Debug, Clone)]
20pub enum PidValidationError {
21 MissingEntity {
23 entity: String,
24 ahb_status: String,
25 severity: Severity,
26 },
27 MissingField {
29 entity: String,
30 field: String,
31 ahb_status: String,
32 rust_type: Option<String>,
33 valid_values: Vec<(String, String)>,
34 severity: Severity,
35 },
36 InvalidCode {
38 entity: String,
39 field: String,
40 value: String,
41 valid_values: Vec<(String, String)>,
42 },
43}
44
45impl PidValidationError {
46 pub fn severity(&self) -> &Severity {
47 match self {
48 Self::MissingEntity { severity, .. } => severity,
49 Self::MissingField { severity, .. } => severity,
50 Self::InvalidCode { .. } => &Severity::Error,
51 }
52 }
53
54 pub fn is_error(&self) -> bool {
55 matches!(self.severity(), Severity::Error)
56 }
57}
58
59impl fmt::Display for PidValidationError {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 PidValidationError::MissingEntity {
63 entity,
64 ahb_status,
65 severity,
66 } => {
67 let label = severity_label(severity);
68 write!(
69 f,
70 "{label}: missing entity '{entity}' (required: {ahb_status})"
71 )
72 }
73 PidValidationError::MissingField {
74 entity,
75 field,
76 ahb_status,
77 rust_type,
78 valid_values,
79 severity,
80 } => {
81 let label = severity_label(severity);
82 write!(
83 f,
84 "{label}: missing {entity}.{field} (required: {ahb_status})"
85 )?;
86 if let Some(rt) = rust_type {
87 write!(f, "\n → type: {rt}")?;
88 }
89 if !valid_values.is_empty() {
90 let codes: Vec<String> = valid_values
91 .iter()
92 .map(|(code, meaning)| {
93 if meaning.is_empty() {
94 code.clone()
95 } else {
96 format!("{code} ({meaning})")
97 }
98 })
99 .collect();
100 write!(f, "\n → valid: {}", codes.join(", "))?;
101 }
102 Ok(())
103 }
104 PidValidationError::InvalidCode {
105 entity,
106 field,
107 value,
108 valid_values,
109 } => {
110 write!(f, "INVALID: {entity}.{field} = \"{value}\"")?;
111 if !valid_values.is_empty() {
112 let codes: Vec<String> = valid_values.iter().map(|(c, _)| c.clone()).collect();
113 write!(f, "\n → valid: {}", codes.join(", "))?;
114 }
115 Ok(())
116 }
117 }
118 }
119}
120
121fn severity_label(severity: &Severity) -> &'static str {
122 match severity {
123 Severity::Error => "ERROR",
124 Severity::Warning => "WARNING",
125 }
126}
127
128pub struct ValidationReport(pub Vec<PidValidationError>);
130
131impl ValidationReport {
132 pub fn has_errors(&self) -> bool {
134 self.0.iter().any(|e| e.is_error())
135 }
136
137 pub fn errors(&self) -> Vec<&PidValidationError> {
139 self.0.iter().filter(|e| e.is_error()).collect()
140 }
141
142 pub fn is_empty(&self) -> bool {
144 self.0.is_empty()
145 }
146
147 pub fn len(&self) -> usize {
149 self.0.len()
150 }
151}
152
153impl fmt::Display for ValidationReport {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 for (i, err) in self.0.iter().enumerate() {
156 if i > 0 {
157 writeln!(f)?;
158 }
159 write!(f, "{err}")?;
160 }
161 Ok(())
162 }
163}
164
165pub fn validate_pid_json(json: &Value, requirements: &PidRequirements) -> Vec<PidValidationError> {
174 let mut errors = Vec::new();
175
176 for entity_req in &requirements.entities {
177 let key = to_camel_case(&entity_req.entity);
178
179 match json.get(&key) {
180 None => {
181 if is_unconditionally_required(&entity_req.ahb_status) {
182 errors.push(PidValidationError::MissingEntity {
183 entity: entity_req.entity.clone(),
184 ahb_status: entity_req.ahb_status.clone(),
185 severity: Severity::Error,
186 });
187 }
188 }
189 Some(val) => {
190 if entity_req.is_array {
191 if let Some(arr) = val.as_array() {
192 for element in arr {
193 validate_entity_fields(element, entity_req, &mut errors);
194 }
195 }
196 } else {
197 validate_entity_fields(val, entity_req, &mut errors);
198 }
199 }
200 }
201 }
202
203 errors
204}
205
206fn validate_entity_fields(
208 entity_json: &Value,
209 entity_req: &EntityRequirement,
210 errors: &mut Vec<PidValidationError>,
211) {
212 for field_req in &entity_req.fields {
213 match entity_json.get(&field_req.bo4e_name) {
214 None => {
215 if is_unconditionally_required(&field_req.ahb_status) {
216 errors.push(PidValidationError::MissingField {
217 entity: entity_req.entity.clone(),
218 field: field_req.bo4e_name.clone(),
219 ahb_status: field_req.ahb_status.clone(),
220 rust_type: field_req.enum_name.clone(),
221 valid_values: code_values_to_tuples(&field_req.valid_codes),
222 severity: Severity::Error,
223 });
224 }
225 }
226 Some(val) => {
227 if !field_req.valid_codes.is_empty() {
228 validate_code_value(val, entity_req, field_req, errors);
229 }
230 }
231 }
232 }
233}
234
235fn validate_code_value(
237 val: &Value,
238 entity_req: &EntityRequirement,
239 field_req: &FieldRequirement,
240 errors: &mut Vec<PidValidationError>,
241) {
242 let value_str = match val.as_str() {
243 Some(s) => s,
244 None => return, };
246
247 let is_valid = field_req.valid_codes.iter().any(|cv| cv.code == value_str);
248 if !is_valid {
249 errors.push(PidValidationError::InvalidCode {
250 entity: entity_req.entity.clone(),
251 field: field_req.bo4e_name.clone(),
252 value: value_str.to_string(),
253 valid_values: code_values_to_tuples(&field_req.valid_codes),
254 });
255 }
256}
257
258fn code_values_to_tuples(codes: &[CodeValue]) -> Vec<(String, String)> {
260 codes
261 .iter()
262 .map(|cv| (cv.code.clone(), cv.meaning.clone()))
263 .collect()
264}
265
266fn to_camel_case(s: &str) -> String {
272 if s.is_empty() {
273 return String::new();
274 }
275 let mut chars = s.chars();
276 let first = chars.next().unwrap();
277 let mut result = first.to_lowercase().to_string();
278 result.extend(chars);
279 result
280}
281
282fn is_unconditionally_required(ahb_status: &str) -> bool {
284 matches!(ahb_status, "X" | "Muss" | "Soll")
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::pid_requirements::{
291 CodeValue, EntityRequirement, FieldRequirement, PidRequirements,
292 };
293 use serde_json::json;
294
295 fn sample_requirements() -> PidRequirements {
296 PidRequirements {
297 pid: "55001".to_string(),
298 beschreibung: "Anmeldung verb. MaLo".to_string(),
299 entities: vec![
300 EntityRequirement {
301 entity: "Prozessdaten".to_string(),
302 bo4e_type: "Prozessdaten".to_string(),
303 companion_type: None,
304 ahb_status: "Muss".to_string(),
305 is_array: false,
306 map_key: None,
307 fields: vec![
308 FieldRequirement {
309 bo4e_name: "vorgangId".to_string(),
310 ahb_status: "X".to_string(),
311 is_companion: false,
312 field_type: "data".to_string(),
313 format: None,
314 enum_name: None,
315 valid_codes: vec![],
316 child_group: None,
317 },
318 FieldRequirement {
319 bo4e_name: "transaktionsgrund".to_string(),
320 ahb_status: "X".to_string(),
321 is_companion: false,
322 field_type: "code".to_string(),
323 format: None,
324 enum_name: Some("Transaktionsgrund".to_string()),
325 valid_codes: vec![
326 CodeValue {
327 code: "E01".to_string(),
328 meaning: "Ein-/Auszug (Einzug)".to_string(),
329 enum_name: None,
330 },
331 CodeValue {
332 code: "E03".to_string(),
333 meaning: "Wechsel".to_string(),
334 enum_name: None,
335 },
336 ],
337 child_group: None,
338 },
339 ],
340 },
341 EntityRequirement {
342 entity: "Marktlokation".to_string(),
343 bo4e_type: "Marktlokation".to_string(),
344 companion_type: Some("MarktlokationEdifact".to_string()),
345 ahb_status: "Muss".to_string(),
346 is_array: false,
347 map_key: None,
348 fields: vec![
349 FieldRequirement {
350 bo4e_name: "marktlokationsId".to_string(),
351 ahb_status: "X".to_string(),
352 is_companion: false,
353 field_type: "data".to_string(),
354 format: None,
355 enum_name: None,
356 valid_codes: vec![],
357 child_group: None,
358 },
359 FieldRequirement {
360 bo4e_name: "haushaltskunde".to_string(),
361 ahb_status: "X".to_string(),
362 is_companion: false,
363 field_type: "code".to_string(),
364 format: None,
365 enum_name: Some("Haushaltskunde".to_string()),
366 valid_codes: vec![
367 CodeValue {
368 code: "Z15".to_string(),
369 meaning: "Ja".to_string(),
370 enum_name: None,
371 },
372 CodeValue {
373 code: "Z18".to_string(),
374 meaning: "Nein".to_string(),
375 enum_name: None,
376 },
377 ],
378 child_group: None,
379 },
380 ],
381 },
382 EntityRequirement {
383 entity: "Geschaeftspartner".to_string(),
384 bo4e_type: "Geschaeftspartner".to_string(),
385 companion_type: Some("GeschaeftspartnerEdifact".to_string()),
386 ahb_status: "Muss".to_string(),
387 is_array: true,
388 map_key: None,
389 fields: vec![FieldRequirement {
390 bo4e_name: "identifikation".to_string(),
391 ahb_status: "X".to_string(),
392 is_companion: false,
393 field_type: "data".to_string(),
394 format: None,
395 enum_name: None,
396 valid_codes: vec![],
397 child_group: None,
398 }],
399 },
400 ],
401 }
402 }
403
404 #[test]
405 fn test_validate_complete_json() {
406 let reqs = sample_requirements();
407 let json = json!({
408 "prozessdaten": {
409 "vorgangId": "ABC123",
410 "transaktionsgrund": "E01"
411 },
412 "marktlokation": {
413 "marktlokationsId": "51234567890",
414 "haushaltskunde": "Z15"
415 },
416 "geschaeftspartner": [
417 { "identifikation": "9900000000003" }
418 ]
419 });
420
421 let errors = validate_pid_json(&json, &reqs);
422 assert!(errors.is_empty(), "Expected no errors, got: {errors:?}");
423 }
424
425 #[test]
426 fn test_validate_missing_entity() {
427 let reqs = sample_requirements();
428 let json = json!({
429 "prozessdaten": {
430 "vorgangId": "ABC123",
431 "transaktionsgrund": "E01"
432 },
433 "geschaeftspartner": [
434 { "identifikation": "9900000000003" }
435 ]
436 });
437 let errors = validate_pid_json(&json, &reqs);
440 assert_eq!(errors.len(), 1);
441 match &errors[0] {
442 PidValidationError::MissingEntity {
443 entity,
444 ahb_status,
445 severity,
446 } => {
447 assert_eq!(entity, "Marktlokation");
448 assert_eq!(ahb_status, "Muss");
449 assert_eq!(severity, &Severity::Error);
450 }
451 other => panic!("Expected MissingEntity, got: {other:?}"),
452 }
453
454 let msg = errors[0].to_string();
456 assert!(msg.contains("ERROR"));
457 assert!(msg.contains("Marktlokation"));
458 assert!(msg.contains("Muss"));
459 }
460
461 #[test]
462 fn test_validate_missing_field() {
463 let reqs = sample_requirements();
464 let json = json!({
465 "prozessdaten": {
466 "transaktionsgrund": "E01"
467 },
469 "marktlokation": {
470 "marktlokationsId": "51234567890",
471 "haushaltskunde": "Z15"
472 },
473 "geschaeftspartner": [
474 { "identifikation": "9900000000003" }
475 ]
476 });
477
478 let errors = validate_pid_json(&json, &reqs);
479 assert_eq!(errors.len(), 1);
480 match &errors[0] {
481 PidValidationError::MissingField {
482 entity,
483 field,
484 ahb_status,
485 severity,
486 ..
487 } => {
488 assert_eq!(entity, "Prozessdaten");
489 assert_eq!(field, "vorgangId");
490 assert_eq!(ahb_status, "X");
491 assert_eq!(severity, &Severity::Error);
492 }
493 other => panic!("Expected MissingField, got: {other:?}"),
494 }
495
496 let msg = errors[0].to_string();
497 assert!(msg.contains("ERROR"));
498 assert!(msg.contains("Prozessdaten.vorgangId"));
499 }
500
501 #[test]
502 fn test_validate_invalid_code() {
503 let reqs = sample_requirements();
504 let json = json!({
505 "prozessdaten": {
506 "vorgangId": "ABC123",
507 "transaktionsgrund": "E01"
508 },
509 "marktlokation": {
510 "marktlokationsId": "51234567890",
511 "haushaltskunde": "Z99" },
513 "geschaeftspartner": [
514 { "identifikation": "9900000000003" }
515 ]
516 });
517
518 let errors = validate_pid_json(&json, &reqs);
519 assert_eq!(errors.len(), 1);
520 match &errors[0] {
521 PidValidationError::InvalidCode {
522 entity,
523 field,
524 value,
525 valid_values,
526 } => {
527 assert_eq!(entity, "Marktlokation");
528 assert_eq!(field, "haushaltskunde");
529 assert_eq!(value, "Z99");
530 assert_eq!(valid_values.len(), 2);
531 assert!(valid_values.iter().any(|(c, _)| c == "Z15"));
532 assert!(valid_values.iter().any(|(c, _)| c == "Z18"));
533 }
534 other => panic!("Expected InvalidCode, got: {other:?}"),
535 }
536
537 let msg = errors[0].to_string();
538 assert!(msg.contains("INVALID"));
539 assert!(msg.contains("Z99"));
540 assert!(msg.contains("Z15"));
541 }
542
543 #[test]
544 fn test_validate_array_entity() {
545 let reqs = sample_requirements();
546 let json = json!({
547 "prozessdaten": {
548 "vorgangId": "ABC123",
549 "transaktionsgrund": "E01"
550 },
551 "marktlokation": {
552 "marktlokationsId": "51234567890",
553 "haushaltskunde": "Z15"
554 },
555 "geschaeftspartner": [
556 { "identifikation": "9900000000003" },
557 { } ]
559 });
560
561 let errors = validate_pid_json(&json, &reqs);
562 assert_eq!(errors.len(), 1);
563 match &errors[0] {
564 PidValidationError::MissingField { entity, field, .. } => {
565 assert_eq!(entity, "Geschaeftspartner");
566 assert_eq!(field, "identifikation");
567 }
568 other => panic!("Expected MissingField, got: {other:?}"),
569 }
570 }
571
572 #[test]
573 fn test_to_camel_case() {
574 assert_eq!(to_camel_case("Prozessdaten"), "prozessdaten");
575 assert_eq!(
576 to_camel_case("RuhendeMarktlokation"),
577 "ruhendeMarktlokation"
578 );
579 assert_eq!(to_camel_case("Marktlokation"), "marktlokation");
580 assert_eq!(to_camel_case(""), "");
581 }
582
583 #[test]
584 fn test_is_unconditionally_required() {
585 assert!(is_unconditionally_required("X"));
586 assert!(is_unconditionally_required("Muss"));
587 assert!(is_unconditionally_required("Soll"));
588 assert!(!is_unconditionally_required("Kann"));
589 assert!(!is_unconditionally_required("[1]"));
590 assert!(!is_unconditionally_required(""));
591 }
592
593 #[test]
594 fn test_validation_report_display() {
595 let errors = vec![
596 PidValidationError::MissingEntity {
597 entity: "Marktlokation".to_string(),
598 ahb_status: "Muss".to_string(),
599 severity: Severity::Error,
600 },
601 PidValidationError::MissingField {
602 entity: "Prozessdaten".to_string(),
603 field: "vorgangId".to_string(),
604 ahb_status: "X".to_string(),
605 rust_type: None,
606 valid_values: vec![],
607 severity: Severity::Error,
608 },
609 ];
610 let report = ValidationReport(errors);
611 assert!(report.has_errors());
612 assert_eq!(report.len(), 2);
613 assert!(!report.is_empty());
614
615 let display = report.to_string();
616 assert!(display.contains("missing entity 'Marktlokation'"));
617 assert!(display.contains("missing Prozessdaten.vorgangId"));
618 }
619
620 #[test]
621 fn test_missing_field_with_type_and_values_display() {
622 let err = PidValidationError::MissingField {
623 entity: "Marktlokation".to_string(),
624 field: "haushaltskunde".to_string(),
625 ahb_status: "Muss".to_string(),
626 rust_type: Some("Haushaltskunde".to_string()),
627 valid_values: vec![
628 ("Z15".to_string(), "Ja".to_string()),
629 ("Z18".to_string(), "Nein".to_string()),
630 ],
631 severity: Severity::Error,
632 };
633 let msg = err.to_string();
634 assert!(msg.contains("type: Haushaltskunde"));
635 assert!(msg.contains("valid: Z15 (Ja), Z18 (Nein)"));
636 }
637
638 #[test]
639 fn test_optional_fields_not_flagged() {
640 let reqs = PidRequirements {
641 pid: "99999".to_string(),
642 beschreibung: "Test".to_string(),
643 entities: vec![EntityRequirement {
644 entity: "Test".to_string(),
645 bo4e_type: "Test".to_string(),
646 companion_type: None,
647 ahb_status: "Kann".to_string(),
648 is_array: false,
649 map_key: None,
650 fields: vec![FieldRequirement {
651 bo4e_name: "optionalField".to_string(),
652 ahb_status: "Kann".to_string(),
653 is_companion: false,
654 field_type: "data".to_string(),
655 format: None,
656 enum_name: None,
657 valid_codes: vec![],
658 child_group: None,
659 }],
660 }],
661 };
662
663 let errors = validate_pid_json(&json!({}), &reqs);
665 assert!(errors.is_empty());
666
667 let errors = validate_pid_json(&json!({ "test": {} }), &reqs);
669 assert!(errors.is_empty());
670 }
671}