1use std::fmt;
4
5use serde_json::Value;
6
7use crate::pid_requirements::{
8 CodeValue, EntityRequirement, EntityScope, FieldRequirement, PidRequirements,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum Severity {
14 Error,
16 Warning,
18}
19
20#[derive(Debug, Clone)]
22pub enum PidValidationError {
23 MissingEntity {
25 entity: String,
26 ahb_status: String,
27 severity: Severity,
28 },
29 MissingField {
31 entity: String,
32 field: String,
33 ahb_status: String,
34 rust_type: Option<String>,
35 valid_values: Vec<(String, String)>,
36 severity: Severity,
37 },
38 InvalidCode {
40 entity: String,
41 field: String,
42 value: String,
43 valid_values: Vec<(String, String)>,
44 },
45}
46
47impl PidValidationError {
48 pub fn severity(&self) -> &Severity {
49 match self {
50 Self::MissingEntity { severity, .. } => severity,
51 Self::MissingField { severity, .. } => severity,
52 Self::InvalidCode { .. } => &Severity::Error,
53 }
54 }
55
56 pub fn is_error(&self) -> bool {
57 matches!(self.severity(), Severity::Error)
58 }
59}
60
61impl fmt::Display for PidValidationError {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 PidValidationError::MissingEntity {
65 entity,
66 ahb_status,
67 severity,
68 } => {
69 let label = severity_label(severity);
70 write!(
71 f,
72 "{label}: missing entity '{entity}' (required: {ahb_status})"
73 )
74 }
75 PidValidationError::MissingField {
76 entity,
77 field,
78 ahb_status,
79 rust_type,
80 valid_values,
81 severity,
82 } => {
83 let label = severity_label(severity);
84 write!(
85 f,
86 "{label}: missing {entity}.{field} (required: {ahb_status})"
87 )?;
88 if let Some(rt) = rust_type {
89 write!(f, "\n → type: {rt}")?;
90 }
91 if !valid_values.is_empty() {
92 let codes: Vec<String> = valid_values
93 .iter()
94 .map(|(code, meaning)| {
95 if meaning.is_empty() {
96 code.clone()
97 } else {
98 format!("{code} ({meaning})")
99 }
100 })
101 .collect();
102 write!(f, "\n → valid: {}", codes.join(", "))?;
103 }
104 Ok(())
105 }
106 PidValidationError::InvalidCode {
107 entity,
108 field,
109 value,
110 valid_values,
111 } => {
112 write!(f, "INVALID: {entity}.{field} = \"{value}\"")?;
113 if !valid_values.is_empty() {
114 let codes: Vec<String> = valid_values.iter().map(|(c, _)| c.clone()).collect();
115 write!(f, "\n → valid: {}", codes.join(", "))?;
116 }
117 Ok(())
118 }
119 }
120 }
121}
122
123fn severity_label(severity: &Severity) -> &'static str {
124 match severity {
125 Severity::Error => "ERROR",
126 Severity::Warning => "WARNING",
127 }
128}
129
130pub struct ValidationReport(pub Vec<PidValidationError>);
132
133impl ValidationReport {
134 pub fn has_errors(&self) -> bool {
136 self.0.iter().any(|e| e.is_error())
137 }
138
139 pub fn errors(&self) -> Vec<&PidValidationError> {
141 self.0.iter().filter(|e| e.is_error()).collect()
142 }
143
144 pub fn is_empty(&self) -> bool {
146 self.0.is_empty()
147 }
148
149 pub fn len(&self) -> usize {
151 self.0.len()
152 }
153}
154
155impl fmt::Display for ValidationReport {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 for (i, err) in self.0.iter().enumerate() {
158 if i > 0 {
159 writeln!(f)?;
160 }
161 write!(f, "{err}")?;
162 }
163 Ok(())
164 }
165}
166
167pub fn validate_pid_json(json: &Value, requirements: &PidRequirements) -> Vec<PidValidationError> {
179 validate_entities(json, &requirements.entities, None)
180}
181
182pub fn validate_pid_json_transaction(
188 json: &Value,
189 requirements: &PidRequirements,
190) -> Vec<PidValidationError> {
191 validate_entities(
192 json,
193 &requirements.entities,
194 Some(EntityScope::Transaction),
195 )
196}
197
198fn validate_entities(
200 json: &Value,
201 entities: &[EntityRequirement],
202 scope_filter: Option<EntityScope>,
203) -> Vec<PidValidationError> {
204 let mut errors = Vec::new();
205
206 for entity_req in entities {
207 if let Some(ref scope) = scope_filter {
209 if &entity_req.scope != scope {
210 continue;
211 }
212 }
213
214 let key = to_camel_case(&entity_req.entity);
215
216 match json.get(&key) {
217 None | Some(serde_json::Value::Null) => {
218 if is_unconditionally_required(&entity_req.ahb_status) {
219 errors.push(PidValidationError::MissingEntity {
220 entity: entity_req.entity.clone(),
221 ahb_status: entity_req.ahb_status.clone(),
222 severity: Severity::Error,
223 });
224 }
225 }
226 Some(val) => {
227 if entity_req.is_array {
228 if let Some(arr) = val.as_array() {
229 for element in arr {
230 validate_entity_fields(element, entity_req, &mut errors);
231 }
232 } else {
233 validate_entity_fields(val, entity_req, &mut errors);
238 }
239 } else {
240 validate_entity_fields(val, entity_req, &mut errors);
241 }
242 }
243 }
244 }
245
246 errors
247}
248
249fn get_nested<'a>(json: &'a Value, path: &str) -> Option<&'a Value> {
252 let mut current = json;
253 for part in path.split('.') {
254 current = current.get(part).or_else(|| {
255 if part.contains('_') {
256 current.get(snake_to_camel_case(part))
257 } else {
258 None
259 }
260 })?;
261 }
262 Some(current)
263}
264
265fn validate_entity_fields(
267 entity_json: &Value,
268 entity_req: &EntityRequirement,
269 errors: &mut Vec<PidValidationError>,
270) {
271 for field_req in &entity_req.fields {
272 let val = get_nested(entity_json, &field_req.bo4e_name).or_else(|| {
278 if field_req.is_companion {
279 if let Some(ref companion_type) = entity_req.companion_type {
280 let companion_key = to_camel_case(companion_type);
281 let companion_obj = entity_json.get(&companion_key)?;
282 get_nested(companion_obj, &field_req.bo4e_name)
283 } else {
284 None
285 }
286 } else {
287 None
288 }
289 });
290
291 let val = val.filter(|v| !v.is_null());
293
294 match val {
295 None => {
296 if is_unconditionally_required(&field_req.ahb_status) {
297 errors.push(PidValidationError::MissingField {
298 entity: entity_req.entity.clone(),
299 field: field_req.bo4e_name.clone(),
300 ahb_status: field_req.ahb_status.clone(),
301 rust_type: field_req.enum_name.clone(),
302 valid_values: code_values_to_tuples(&field_req.valid_codes),
303 severity: Severity::Error,
304 });
305 }
306 }
307 Some(val) => {
308 if !field_req.valid_codes.is_empty() {
309 validate_code_value(val, entity_req, field_req, errors);
310 }
311 }
312 }
313 }
314}
315
316fn validate_code_value(
318 val: &Value,
319 entity_req: &EntityRequirement,
320 field_req: &FieldRequirement,
321 errors: &mut Vec<PidValidationError>,
322) {
323 let value_str = match val.as_str() {
324 Some(s) => s,
325 None => return, };
327
328 let is_valid = field_req.valid_codes.iter().any(|cv| cv.code == value_str);
329 if !is_valid {
330 errors.push(PidValidationError::InvalidCode {
331 entity: entity_req.entity.clone(),
332 field: field_req.bo4e_name.clone(),
333 value: value_str.to_string(),
334 valid_values: code_values_to_tuples(&field_req.valid_codes),
335 });
336 }
337}
338
339fn code_values_to_tuples(codes: &[CodeValue]) -> Vec<(String, String)> {
341 codes
342 .iter()
343 .map(|cv| (cv.code.clone(), cv.meaning.clone()))
344 .collect()
345}
346
347fn to_camel_case(s: &str) -> String {
353 if s.is_empty() {
354 return String::new();
355 }
356 let mut chars = s.chars();
357 let first = chars.next().unwrap();
358 let mut result = first.to_lowercase().to_string();
359 result.extend(chars);
360 result
361}
362
363fn snake_to_camel_case(s: &str) -> String {
374 let mut result = String::with_capacity(s.len());
375 let mut capitalize_next = false;
376 for ch in s.chars() {
377 if ch == '_' {
378 capitalize_next = true;
379 } else if capitalize_next {
380 result.extend(ch.to_uppercase());
381 capitalize_next = false;
382 } else {
383 result.push(ch);
384 }
385 }
386 result
387}
388
389fn is_unconditionally_required(ahb_status: &str) -> bool {
391 matches!(ahb_status, "X" | "Muss" | "Soll")
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use crate::pid_requirements::{
398 CodeValue, EntityRequirement, FieldRequirement, PidRequirements,
399 };
400 use serde_json::json;
401
402 fn sample_requirements() -> PidRequirements {
403 PidRequirements {
404 pid: "55001".to_string(),
405 beschreibung: "Anmeldung verb. MaLo".to_string(),
406 entities: vec![
407 EntityRequirement {
408 entity: "Prozessdaten".to_string(),
409 bo4e_type: "Prozessdaten".to_string(),
410 companion_type: None,
411 ahb_status: "Muss".to_string(),
412 is_array: false,
413 map_key: None,
414 scope: EntityScope::Transaction,
415 fields: vec![
416 FieldRequirement {
417 bo4e_name: "vorgangId".to_string(),
418 ahb_status: "X".to_string(),
419 is_companion: false,
420 field_type: "data".to_string(),
421 format: None,
422 enum_name: None,
423 valid_codes: vec![],
424 child_group: None,
425 },
426 FieldRequirement {
427 bo4e_name: "transaktionsgrund".to_string(),
428 ahb_status: "X".to_string(),
429 is_companion: false,
430 field_type: "code".to_string(),
431 format: None,
432 enum_name: Some("Transaktionsgrund".to_string()),
433 valid_codes: vec![
434 CodeValue {
435 code: "E01".to_string(),
436 meaning: "Ein-/Auszug (Einzug)".to_string(),
437 enum_name: None,
438 },
439 CodeValue {
440 code: "E03".to_string(),
441 meaning: "Wechsel".to_string(),
442 enum_name: None,
443 },
444 ],
445 child_group: None,
446 },
447 ],
448 },
449 EntityRequirement {
450 entity: "Marktlokation".to_string(),
451 bo4e_type: "Marktlokation".to_string(),
452 companion_type: Some("MarktlokationEdifact".to_string()),
453 ahb_status: "Muss".to_string(),
454 is_array: false,
455 map_key: None,
456 scope: EntityScope::Transaction,
457 fields: vec![
458 FieldRequirement {
459 bo4e_name: "marktlokationsId".to_string(),
460 ahb_status: "X".to_string(),
461 is_companion: false,
462 field_type: "data".to_string(),
463 format: None,
464 enum_name: None,
465 valid_codes: vec![],
466 child_group: None,
467 },
468 FieldRequirement {
469 bo4e_name: "haushaltskunde".to_string(),
470 ahb_status: "X".to_string(),
471 is_companion: false,
472 field_type: "code".to_string(),
473 format: None,
474 enum_name: Some("Haushaltskunde".to_string()),
475 valid_codes: vec![
476 CodeValue {
477 code: "Z15".to_string(),
478 meaning: "Ja".to_string(),
479 enum_name: None,
480 },
481 CodeValue {
482 code: "Z18".to_string(),
483 meaning: "Nein".to_string(),
484 enum_name: None,
485 },
486 ],
487 child_group: None,
488 },
489 ],
490 },
491 EntityRequirement {
492 entity: "Geschaeftspartner".to_string(),
493 bo4e_type: "Geschaeftspartner".to_string(),
494 companion_type: Some("GeschaeftspartnerEdifact".to_string()),
495 ahb_status: "Muss".to_string(),
496 is_array: true,
497 map_key: None,
498 scope: EntityScope::Transaction,
499 fields: vec![FieldRequirement {
500 bo4e_name: "identifikation".to_string(),
501 ahb_status: "X".to_string(),
502 is_companion: false,
503 field_type: "data".to_string(),
504 format: None,
505 enum_name: None,
506 valid_codes: vec![],
507 child_group: None,
508 }],
509 },
510 ],
511 }
512 }
513
514 #[test]
515 fn test_validate_complete_json() {
516 let reqs = sample_requirements();
517 let json = json!({
518 "prozessdaten": {
519 "vorgangId": "ABC123",
520 "transaktionsgrund": "E01"
521 },
522 "marktlokation": {
523 "marktlokationsId": "51234567890",
524 "haushaltskunde": "Z15"
525 },
526 "geschaeftspartner": [
527 { "identifikation": "9900000000003" }
528 ]
529 });
530
531 let errors = validate_pid_json(&json, &reqs);
532 assert!(errors.is_empty(), "Expected no errors, got: {errors:?}");
533 }
534
535 #[test]
536 fn test_validate_missing_entity() {
537 let reqs = sample_requirements();
538 let json = json!({
539 "prozessdaten": {
540 "vorgangId": "ABC123",
541 "transaktionsgrund": "E01"
542 },
543 "geschaeftspartner": [
544 { "identifikation": "9900000000003" }
545 ]
546 });
547 let errors = validate_pid_json(&json, &reqs);
550 assert_eq!(errors.len(), 1);
551 match &errors[0] {
552 PidValidationError::MissingEntity {
553 entity,
554 ahb_status,
555 severity,
556 } => {
557 assert_eq!(entity, "Marktlokation");
558 assert_eq!(ahb_status, "Muss");
559 assert_eq!(severity, &Severity::Error);
560 }
561 other => panic!("Expected MissingEntity, got: {other:?}"),
562 }
563
564 let msg = errors[0].to_string();
566 assert!(msg.contains("ERROR"));
567 assert!(msg.contains("Marktlokation"));
568 assert!(msg.contains("Muss"));
569 }
570
571 #[test]
572 fn test_validate_missing_field() {
573 let reqs = sample_requirements();
574 let json = json!({
575 "prozessdaten": {
576 "transaktionsgrund": "E01"
577 },
579 "marktlokation": {
580 "marktlokationsId": "51234567890",
581 "haushaltskunde": "Z15"
582 },
583 "geschaeftspartner": [
584 { "identifikation": "9900000000003" }
585 ]
586 });
587
588 let errors = validate_pid_json(&json, &reqs);
589 assert_eq!(errors.len(), 1);
590 match &errors[0] {
591 PidValidationError::MissingField {
592 entity,
593 field,
594 ahb_status,
595 severity,
596 ..
597 } => {
598 assert_eq!(entity, "Prozessdaten");
599 assert_eq!(field, "vorgangId");
600 assert_eq!(ahb_status, "X");
601 assert_eq!(severity, &Severity::Error);
602 }
603 other => panic!("Expected MissingField, got: {other:?}"),
604 }
605
606 let msg = errors[0].to_string();
607 assert!(msg.contains("ERROR"));
608 assert!(msg.contains("Prozessdaten.vorgangId"));
609 }
610
611 #[test]
612 fn test_validate_invalid_code() {
613 let reqs = sample_requirements();
614 let json = json!({
615 "prozessdaten": {
616 "vorgangId": "ABC123",
617 "transaktionsgrund": "E01"
618 },
619 "marktlokation": {
620 "marktlokationsId": "51234567890",
621 "haushaltskunde": "Z99" },
623 "geschaeftspartner": [
624 { "identifikation": "9900000000003" }
625 ]
626 });
627
628 let errors = validate_pid_json(&json, &reqs);
629 assert_eq!(errors.len(), 1);
630 match &errors[0] {
631 PidValidationError::InvalidCode {
632 entity,
633 field,
634 value,
635 valid_values,
636 } => {
637 assert_eq!(entity, "Marktlokation");
638 assert_eq!(field, "haushaltskunde");
639 assert_eq!(value, "Z99");
640 assert_eq!(valid_values.len(), 2);
641 assert!(valid_values.iter().any(|(c, _)| c == "Z15"));
642 assert!(valid_values.iter().any(|(c, _)| c == "Z18"));
643 }
644 other => panic!("Expected InvalidCode, got: {other:?}"),
645 }
646
647 let msg = errors[0].to_string();
648 assert!(msg.contains("INVALID"));
649 assert!(msg.contains("Z99"));
650 assert!(msg.contains("Z15"));
651 }
652
653 #[test]
654 fn test_validate_array_entity() {
655 let reqs = sample_requirements();
656 let json = json!({
657 "prozessdaten": {
658 "vorgangId": "ABC123",
659 "transaktionsgrund": "E01"
660 },
661 "marktlokation": {
662 "marktlokationsId": "51234567890",
663 "haushaltskunde": "Z15"
664 },
665 "geschaeftspartner": [
666 { "identifikation": "9900000000003" },
667 { } ]
669 });
670
671 let errors = validate_pid_json(&json, &reqs);
672 assert_eq!(errors.len(), 1);
673 match &errors[0] {
674 PidValidationError::MissingField { entity, field, .. } => {
675 assert_eq!(entity, "Geschaeftspartner");
676 assert_eq!(field, "identifikation");
677 }
678 other => panic!("Expected MissingField, got: {other:?}"),
679 }
680 }
681
682 #[test]
683 fn test_to_camel_case() {
684 assert_eq!(to_camel_case("Prozessdaten"), "prozessdaten");
685 assert_eq!(
686 to_camel_case("RuhendeMarktlokation"),
687 "ruhendeMarktlokation"
688 );
689 assert_eq!(to_camel_case("Marktlokation"), "marktlokation");
690 assert_eq!(to_camel_case(""), "");
691 }
692
693 #[test]
694 fn test_snake_to_camel_case() {
695 assert_eq!(snake_to_camel_case("code_codepflege"), "codeCodepflege");
696 assert_eq!(snake_to_camel_case("vorgang_id"), "vorgangId");
697 assert_eq!(snake_to_camel_case("marktlokation"), "marktlokation");
698 assert_eq!(snake_to_camel_case(""), "");
699 assert_eq!(snake_to_camel_case("a_b_c"), "aBC");
700 }
701
702 #[test]
705 fn test_camel_case_fallback_for_snake_case_bo4e_name() {
706 let reqs = PidRequirements {
707 pid: "55077".to_string(),
708 beschreibung: "Test camelCase fallback".to_string(),
709 entities: vec![EntityRequirement {
710 entity: "Zuordnung".to_string(),
711 bo4e_type: "Zuordnung".to_string(),
712 companion_type: None,
713 ahb_status: "Muss".to_string(),
714 is_array: false,
715 map_key: None,
716 scope: EntityScope::Transaction,
717 fields: vec![
718 FieldRequirement {
719 bo4e_name: "code_codepflege".to_string(),
721 ahb_status: "X".to_string(),
722 is_companion: false,
723 field_type: "data".to_string(),
724 format: None,
725 enum_name: None,
726 valid_codes: vec![],
727 child_group: None,
728 },
729 FieldRequirement {
730 bo4e_name: "codeliste".to_string(),
731 ahb_status: "X".to_string(),
732 is_companion: false,
733 field_type: "data".to_string(),
734 format: None,
735 enum_name: None,
736 valid_codes: vec![],
737 child_group: None,
738 },
739 ],
740 }],
741 };
742
743 let json_camel = json!({
746 "zuordnung": {
747 "codeCodepflege": "DE_BDEW",
748 "codeliste": "6"
749 }
750 });
751
752 let errors = validate_pid_json(&json_camel, &reqs);
753 assert!(
754 errors.is_empty(),
755 "Expected no errors when field is present under camelCase key, got: {errors:?}"
756 );
757
758 let json_snake = json!({
760 "zuordnung": {
761 "code_codepflege": "DE_BDEW",
762 "codeliste": "6"
763 }
764 });
765
766 let errors = validate_pid_json(&json_snake, &reqs);
767 assert!(
768 errors.is_empty(),
769 "Expected no errors when field is present under snake_case key, got: {errors:?}"
770 );
771
772 let json_missing = json!({
774 "zuordnung": {
775 "codeliste": "6"
776 }
777 });
778
779 let errors = validate_pid_json(&json_missing, &reqs);
780 assert_eq!(errors.len(), 1);
781 match &errors[0] {
782 PidValidationError::MissingField { field, .. } => {
783 assert_eq!(field, "code_codepflege");
784 }
785 other => panic!("Expected MissingField, got: {other:?}"),
786 }
787 }
788
789 #[test]
790 fn test_is_unconditionally_required() {
791 assert!(is_unconditionally_required("X"));
792 assert!(is_unconditionally_required("Muss"));
793 assert!(is_unconditionally_required("Soll"));
794 assert!(!is_unconditionally_required("Kann"));
795 assert!(!is_unconditionally_required("[1]"));
796 assert!(!is_unconditionally_required(""));
797 }
798
799 #[test]
800 fn test_validation_report_display() {
801 let errors = vec![
802 PidValidationError::MissingEntity {
803 entity: "Marktlokation".to_string(),
804 ahb_status: "Muss".to_string(),
805 severity: Severity::Error,
806 },
807 PidValidationError::MissingField {
808 entity: "Prozessdaten".to_string(),
809 field: "vorgangId".to_string(),
810 ahb_status: "X".to_string(),
811 rust_type: None,
812 valid_values: vec![],
813 severity: Severity::Error,
814 },
815 ];
816 let report = ValidationReport(errors);
817 assert!(report.has_errors());
818 assert_eq!(report.len(), 2);
819 assert!(!report.is_empty());
820
821 let display = report.to_string();
822 assert!(display.contains("missing entity 'Marktlokation'"));
823 assert!(display.contains("missing Prozessdaten.vorgangId"));
824 }
825
826 #[test]
827 fn test_missing_field_with_type_and_values_display() {
828 let err = PidValidationError::MissingField {
829 entity: "Marktlokation".to_string(),
830 field: "haushaltskunde".to_string(),
831 ahb_status: "Muss".to_string(),
832 rust_type: Some("Haushaltskunde".to_string()),
833 valid_values: vec![
834 ("Z15".to_string(), "Ja".to_string()),
835 ("Z18".to_string(), "Nein".to_string()),
836 ],
837 severity: Severity::Error,
838 };
839 let msg = err.to_string();
840 assert!(msg.contains("type: Haushaltskunde"));
841 assert!(msg.contains("valid: Z15 (Ja), Z18 (Nein)"));
842 }
843
844 #[test]
845 fn test_optional_fields_not_flagged() {
846 let reqs = PidRequirements {
847 pid: "99999".to_string(),
848 beschreibung: "Test".to_string(),
849 entities: vec![EntityRequirement {
850 entity: "Test".to_string(),
851 bo4e_type: "Test".to_string(),
852 companion_type: None,
853 ahb_status: "Kann".to_string(),
854 is_array: false,
855 map_key: None,
856 scope: EntityScope::Transaction,
857 fields: vec![FieldRequirement {
858 bo4e_name: "optionalField".to_string(),
859 ahb_status: "Kann".to_string(),
860 is_companion: false,
861 field_type: "data".to_string(),
862 format: None,
863 enum_name: None,
864 valid_codes: vec![],
865 child_group: None,
866 }],
867 }],
868 };
869
870 let errors = validate_pid_json(&json!({}), &reqs);
872 assert!(errors.is_empty());
873
874 let errors = validate_pid_json(&json!({ "test": {} }), &reqs);
876 assert!(errors.is_empty());
877 }
878
879 #[test]
882 fn test_nested_dot_path_fields_not_falsely_missing() {
883 let reqs = PidRequirements {
884 pid: "55001".to_string(),
885 beschreibung: "Test nested paths".to_string(),
886 entities: vec![EntityRequirement {
887 entity: "ProduktpaketDaten".to_string(),
888 bo4e_type: "ProduktpaketDaten".to_string(),
889 companion_type: None,
890 ahb_status: "Muss".to_string(),
891 is_array: true,
892 map_key: None,
893 scope: EntityScope::Transaction,
894 fields: vec![
895 FieldRequirement {
896 bo4e_name: "produktIdentifikation.funktion".to_string(),
897 ahb_status: "X".to_string(),
898 is_companion: false,
899 field_type: "code".to_string(),
900 format: None,
901 enum_name: Some("Produktidentifikation".to_string()),
902 valid_codes: vec![CodeValue {
903 code: "5".to_string(),
904 meaning: "Produktidentifikation".to_string(),
905 enum_name: None,
906 }],
907 child_group: None,
908 },
909 FieldRequirement {
910 bo4e_name: "produktMerkmal.code".to_string(),
911 ahb_status: "X".to_string(),
912 is_companion: false,
913 field_type: "code".to_string(),
914 format: None,
915 enum_name: None,
916 valid_codes: vec![],
917 child_group: None,
918 },
919 ],
920 }],
921 };
922
923 let json = json!({
925 "produktpaketDaten": [{
926 "produktIdentifikation": { "funktion": "5", "id": "9991000002082", "typ": "Z11" },
927 "produktMerkmal": { "code": "ZH9" }
928 }]
929 });
930
931 let errors = validate_pid_json(&json, &reqs);
932 assert!(
933 errors.is_empty(),
934 "Nested dot-path fields should be found (issue #48), got: {errors:?}"
935 );
936 }
937
938 #[test]
939 fn test_nested_dot_path_truly_missing() {
940 let reqs = PidRequirements {
941 pid: "55001".to_string(),
942 beschreibung: "Test nested paths missing".to_string(),
943 entities: vec![EntityRequirement {
944 entity: "ProduktpaketDaten".to_string(),
945 bo4e_type: "ProduktpaketDaten".to_string(),
946 companion_type: None,
947 ahb_status: "Muss".to_string(),
948 is_array: true,
949 map_key: None,
950 scope: EntityScope::Transaction,
951 fields: vec![FieldRequirement {
952 bo4e_name: "produktIdentifikation.funktion".to_string(),
953 ahb_status: "X".to_string(),
954 is_companion: false,
955 field_type: "data".to_string(),
956 format: None,
957 enum_name: None,
958 valid_codes: vec![],
959 child_group: None,
960 }],
961 }],
962 };
963
964 let json = json!({
966 "produktpaketDaten": [{
967 "produktIdentifikation": { "id": "123" }
968 }]
969 });
970
971 let errors = validate_pid_json(&json, &reqs);
972 assert_eq!(errors.len(), 1, "Should report missing nested field");
973 match &errors[0] {
974 PidValidationError::MissingField { field, .. } => {
975 assert_eq!(field, "produktIdentifikation.funktion");
976 }
977 other => panic!("Expected MissingField, got: {other:?}"),
978 }
979 }
980}