1use std::borrow::Cow;
4use std::collections::{BTreeMap, BTreeSet};
5use std::fmt;
6
7use serde_json::Value;
8
9use crate::pid_requirements::{
10 CodeValue, EntityGroupRequirement, EntityRequirement, EntityScope, EntityVariantRequirement,
11 FieldRequirement, KindSelector, NestedElementKind, NestedListRequirement, PidRequirements,
12 VariantQualifier,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum Severity {
18 Error,
20 Warning,
22}
23
24#[derive(Debug, Clone)]
26pub enum PidValidationError {
27 MissingEntity {
29 entity: String,
30 ahb_status: String,
31 severity: Severity,
32 },
33 MissingField {
35 entity: String,
36 field: String,
37 ahb_status: String,
38 rust_type: Option<String>,
39 valid_values: Vec<(String, String)>,
40 severity: Severity,
41 },
42 InvalidCode {
44 entity: String,
45 field: String,
46 value: String,
47 valid_values: Vec<(String, String)>,
48 },
49 NotAllowed {
55 entity: String,
56 field: String,
57 value: Option<String>,
58 ahb_status: String,
59 severity: Severity,
60 },
61 ConditionNotSatisfied {
67 entity: String,
68 field: String,
69 value: String,
70 ahb_status: String,
71 },
72 RepeatedInTransaction {
75 entity: String,
76 count: usize,
77 ahb_status: String,
78 },
79 NotANumber {
82 entity: String,
83 field: String,
84 value: String,
85 format: String,
86 },
87 TooLong {
90 entity: String,
91 field: String,
92 length: usize,
93 format: String,
94 },
95}
96
97impl PidValidationError {
98 pub fn severity(&self) -> &Severity {
99 match self {
100 Self::MissingEntity { severity, .. } => severity,
101 Self::MissingField { severity, .. } => severity,
102 Self::NotAllowed { severity, .. } => severity,
103 Self::InvalidCode { .. }
104 | Self::TooLong { .. }
105 | Self::ConditionNotSatisfied { .. }
106 | Self::RepeatedInTransaction { .. }
107 | Self::NotANumber { .. } => &Severity::Error,
108 }
109 }
110
111 pub fn is_error(&self) -> bool {
112 matches!(self.severity(), Severity::Error)
113 }
114}
115
116impl fmt::Display for PidValidationError {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 match self {
119 PidValidationError::MissingEntity {
120 entity,
121 ahb_status,
122 severity,
123 } => {
124 let label = severity_label(severity);
125 write!(
126 f,
127 "{label}: missing entity '{entity}' (required: {ahb_status})"
128 )
129 }
130 PidValidationError::MissingField {
131 entity,
132 field,
133 ahb_status,
134 rust_type,
135 valid_values,
136 severity,
137 } => {
138 let label = severity_label(severity);
139 write!(
140 f,
141 "{label}: missing {entity}.{field} (required: {ahb_status})"
142 )?;
143 if let Some(rt) = rust_type {
144 write!(f, "\n → type: {rt}")?;
145 }
146 if !valid_values.is_empty() {
147 let codes: Vec<String> = valid_values
148 .iter()
149 .map(|(code, meaning)| {
150 if meaning.is_empty() {
151 code.clone()
152 } else {
153 format!("{code} ({meaning})")
154 }
155 })
156 .collect();
157 write!(f, "\n → valid: {}", codes.join(", "))?;
158 }
159 Ok(())
160 }
161 PidValidationError::NotANumber {
162 entity,
163 field,
164 value,
165 format,
166 } => write!(
167 f,
168 "INVALID: {entity}.{field} = \"{value}\" is not a number\n → format: {format}"
169 ),
170 PidValidationError::TooLong {
171 entity,
172 field,
173 length,
174 format,
175 } => write!(
176 f,
177 "INVALID: {entity}.{field} has {length} characters\n → format: {format}"
178 ),
179 PidValidationError::InvalidCode {
180 entity,
181 field,
182 value,
183 valid_values,
184 } => {
185 write!(f, "INVALID: {entity}.{field} = \"{value}\"")?;
186 if !valid_values.is_empty() {
187 let codes: Vec<String> = valid_values.iter().map(|(c, _)| c.clone()).collect();
188 write!(f, "\n → valid: {}", codes.join(", "))?;
189 }
190 Ok(())
191 }
192 PidValidationError::RepeatedInTransaction {
193 entity,
194 count,
195 ahb_status,
196 } => write!(
197 f,
198 "ERROR: {entity} is sent {count} times in one transaction; its condition allows it once ({ahb_status})"
199 ),
200 PidValidationError::ConditionNotSatisfied {
201 entity,
202 field,
203 value,
204 ahb_status,
205 } => write!(
206 f,
207 "ERROR: {entity}.{field} = \"{value}\" does not satisfy its condition ({ahb_status})"
208 ),
209 PidValidationError::NotAllowed {
210 entity,
211 field,
212 value,
213 ahb_status,
214 severity,
215 } => {
216 write!(f, "{}: {entity}", severity_label(severity))?;
217 if !field.is_empty() {
218 write!(f, ".{field}")?;
219 }
220 if let Some(value) = value {
221 write!(f, " = \"{value}\"")?;
222 }
223 write!(
224 f,
225 " present although its condition does not hold ({ahb_status})"
226 )
227 }
228 }
229 }
230}
231
232fn severity_label(severity: &Severity) -> &'static str {
233 match severity {
234 Severity::Error => "ERROR",
235 Severity::Warning => "WARNING",
236 }
237}
238
239pub struct ValidationReport(pub Vec<PidValidationError>);
241
242impl ValidationReport {
243 pub fn has_errors(&self) -> bool {
245 self.0.iter().any(|e| e.is_error())
246 }
247
248 pub fn errors(&self) -> Vec<&PidValidationError> {
250 self.0.iter().filter(|e| e.is_error()).collect()
251 }
252
253 pub fn is_empty(&self) -> bool {
255 self.0.is_empty()
256 }
257
258 pub fn len(&self) -> usize {
260 self.0.len()
261 }
262}
263
264impl fmt::Display for ValidationReport {
265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266 for (i, err) in self.0.iter().enumerate() {
267 if i > 0 {
268 writeln!(f)?;
269 }
270 write!(f, "{err}")?;
271 }
272 Ok(())
273 }
274}
275
276pub fn validate_pid_json(json: &Value, requirements: &PidRequirements) -> Vec<PidValidationError> {
288 validate_entities(json, &requirements.entities, None)
289}
290
291pub fn validate_pid_json_transaction(
297 json: &Value,
298 requirements: &PidRequirements,
299) -> Vec<PidValidationError> {
300 validate_entities(json, &requirements.entities, Some(EntityScope::Transaction))
301}
302
303fn validate_entities(
305 json: &Value,
306 entities: &[EntityRequirement],
307 scope_filter: Option<EntityScope>,
308) -> Vec<PidValidationError> {
309 let mut errors = Vec::new();
310
311 for entity_req in entities {
312 if let Some(ref scope) = scope_filter {
314 if &entity_req.scope != scope {
315 continue;
316 }
317 }
318
319 let key = to_camel_case(&entity_req.entity);
320
321 match json.get(&key) {
322 None | Some(serde_json::Value::Null) => {
323 if is_unconditionally_required(&entity_req.ahb_status) {
324 errors.push(PidValidationError::MissingEntity {
325 entity: entity_req.entity.clone(),
326 ahb_status: entity_req.ahb_status.clone(),
327 severity: Severity::Error,
328 });
329 }
330 }
331 Some(val) => {
332 if let Some(arr) = val.as_array() {
335 for element in arr {
336 validate_entity_fields(element, entity_req, &mut errors);
337 }
338 } else {
339 validate_entity_fields(val, entity_req, &mut errors);
340 }
341 }
342 }
343 }
344
345 errors
346}
347
348pub fn get_nested<'a>(json: &'a Value, path: &str) -> Option<&'a Value> {
351 if let Some((list, rest)) = path.split_once("[].") {
354 return get_nested(json, list)?
355 .as_array()?
356 .iter()
357 .find_map(|item| get_nested(item, rest).filter(|v| !v.is_null()));
358 }
359 let mut current = json;
360 for part in path.split('.') {
361 current = current.get(part).or_else(|| {
362 if part.contains('_') {
363 current.get(snake_to_camel_case(part))
364 } else {
365 None
366 }
367 })?;
368 }
369 Some(current)
370}
371
372fn validate_entity_fields(
374 entity_json: &Value,
375 entity_req: &EntityRequirement,
376 errors: &mut Vec<PidValidationError>,
377) {
378 let fields = effective_field_requirements(entity_req, entity_json);
379 let exempt =
380 fields_of_unrequired_absent_groups(entity_req, entity_json, is_unconditionally_required);
381 for field_req in fields.iter() {
382 let val = get_nested(entity_json, &field_req.bo4e_name);
385
386 let val = val.filter(|v| !v.is_null());
388
389 match val {
390 None if exempt.contains(field_req.bo4e_name.as_str()) => {}
391 None => {
392 if is_unconditionally_required(&field_req.ahb_status) {
393 errors.push(PidValidationError::MissingField {
394 entity: entity_req.entity.clone(),
395 field: field_req.bo4e_name.clone(),
396 ahb_status: field_req.ahb_status.clone(),
397 rust_type: field_req.enum_name.clone(),
398 valid_values: code_values_to_tuples(&field_req.valid_codes),
399 severity: Severity::Error,
400 });
401 }
402 }
403 Some(val) => {
404 validate_code_value(val, entity_req, field_req, errors);
405 }
406 }
407 }
408 validate_nested_lists(
409 &entity_req.entity,
410 entity_json,
411 &entity_req.nested,
412 "",
413 &|status, _| is_unconditionally_required(status).then_some(Severity::Error),
414 &|_| None,
415 &|_, _| false,
417 errors,
418 );
419}
420
421pub fn validate_nested_lists(
431 entity: &str,
432 element: &Value,
433 lists: &[NestedListRequirement],
434 prefix: &str,
435 required: &dyn Fn(&str, &[(String, usize)]) -> Option<Severity>,
436 variant_holds: &dyn Fn(&VariantQualifier) -> Option<bool>,
437 forbidden: &dyn Fn(&str, &[(String, usize)]) -> bool,
438 errors: &mut Vec<PidValidationError>,
439) {
440 nested_lists_at(
441 entity,
442 element,
443 lists,
444 prefix,
445 &mut Vec::new(),
446 required,
447 variant_holds,
448 forbidden,
449 errors,
450 );
451}
452
453#[allow(clippy::too_many_arguments)]
455fn nested_lists_at(
456 entity: &str,
457 element: &Value,
458 lists: &[NestedListRequirement],
459 prefix: &str,
460 position: &mut Vec<(String, usize)>,
461 required: &dyn Fn(&str, &[(String, usize)]) -> Option<Severity>,
462 variant_holds: &dyn Fn(&VariantQualifier) -> Option<bool>,
463 forbidden: &dyn Fn(&str, &[(String, usize)]) -> bool,
464 errors: &mut Vec<PidValidationError>,
465) {
466 let applies = |kind: &NestedElementKind| {
469 kind.transaction_variant
470 .as_ref()
471 .map_or(Some(true), variant_holds)
472 };
473 let mut fields: Vec<&str> = lists.iter().map(|l| l.field.as_str()).collect();
476 fields.sort_unstable();
477 fields.dedup();
478 for field in fields {
479 let candidates: Vec<&NestedListRequirement> = lists
482 .iter()
483 .filter(|l| l.field == field)
484 .filter(|l| {
485 l.parent_selector
486 .iter()
487 .all(|selector| selector_matches(selector, element))
488 })
489 .collect();
490 if candidates.is_empty() {
491 continue;
492 }
493 let items: &[Value] = get_nested(element, field)
494 .and_then(|v| v.as_array())
495 .map_or(&[], |a| a.as_slice());
496 let label = format!("{prefix}{field}");
497 let alone = candidates.len() == 1;
498
499 let chosen = match &candidates[..] {
503 [only] => Some(*only),
504 _ if items.is_empty() => None,
505 _ => {
506 let fitting: Vec<&&NestedListRequirement> = candidates
507 .iter()
508 .filter(|l| items.iter().all(|item| kind_of(l, item, false).is_some()))
509 .collect();
510 match fitting[..] {
511 [one] => Some(*one),
512 _ => None,
513 }
514 }
515 };
516
517 if let Some(list) = chosen {
518 for kind in &list.kinds {
519 if applies(kind) != Some(true) {
520 continue;
521 }
522 let present = if alone && list.kinds.len() == 1 {
523 !items.is_empty()
524 } else {
525 items.iter().any(|item| kind_matches(kind, item))
526 };
527 if present {
528 continue;
529 }
530 if let Some(severity) = required(&kind.ahb_status, position) {
531 let codes = kind.selector.first().map_or(&[][..], |s| &s.codes[..]);
532 let name = if alone && list.kinds.len() == 1 {
535 String::new()
536 } else {
537 codes
538 .first()
539 .map(|c| c.bo4e_value.clone().unwrap_or_else(|| c.code.clone()))
540 .unwrap_or_default()
541 };
542 errors.push(PidValidationError::MissingField {
543 entity: entity.to_string(),
544 field: format!("{label}[{name}]"),
545 ahb_status: kind.ahb_status.clone(),
546 rust_type: None,
547 valid_values: code_values_to_tuples(codes),
548 severity,
549 });
550 }
551 }
552 }
553
554 for (index, item) in items.iter().enumerate() {
555 position.push((field.to_string(), index));
556 let here = position.clone();
557 let required_here = |status: &str| required(status, &here);
558 let matching: Vec<(&NestedListRequirement, &NestedElementKind)> = match chosen {
559 Some(list) => kind_of(list, item, alone)
560 .filter(|k| applies(k) != Some(false))
561 .map(|k| (list, k))
562 .into_iter()
563 .collect(),
564 None => candidates
565 .iter()
566 .flat_map(|l| {
567 l.kinds
568 .iter()
569 .filter(|k| applies(k) != Some(false))
570 .filter(|k| kind_matches(k, item))
571 .map(move |k| (*l, k))
572 })
573 .collect(),
574 };
575 let best = matching
579 .iter()
580 .map(|(l, k)| {
581 (
582 *l,
583 *k,
584 element_findings(entity, &label, k, item, &required_here),
585 )
586 })
587 .min_by_key(|(_, k, f)| (f.len(), k.selector.is_empty()));
588 if let Some((list, kind, findings)) = best {
589 errors.extend(findings);
590 let forbidden_here = |status: &str| forbidden(status, &here);
593 if forbidden_here(&kind.ahb_status) {
594 let name = kind
595 .selector
596 .first()
597 .and_then(|s| s.codes.first())
598 .map(|c| c.bo4e_value.clone().unwrap_or_else(|| c.code.clone()))
599 .unwrap_or_default();
600 errors.push(PidValidationError::NotAllowed {
601 entity: entity.to_string(),
602 field: format!("{label}[{name}]"),
603 value: None,
604 ahb_status: kind.ahb_status.clone(),
605 severity: Severity::Warning,
606 });
607 } else {
608 errors.extend(not_allowed_codes(
609 entity,
610 &format!("{label}[]."),
611 &kind.fields,
612 item,
613 &forbidden_here,
614 ));
615 }
616 nested_lists_at(
617 entity,
618 item,
619 &list.nested,
620 &format!("{label}[]."),
621 position,
622 required,
623 variant_holds,
624 forbidden,
625 errors,
626 );
627 }
628 position.pop();
629 }
630 }
631}
632
633fn kind_of<'l>(
637 list: &'l NestedListRequirement,
638 item: &Value,
639 alone: bool,
640) -> Option<&'l NestedElementKind> {
641 match &list.kinds[..] {
642 [only] if alone => Some(only),
643 kinds => kinds.iter().find(|k| kind_matches(k, item)),
644 }
645}
646
647fn kind_matches(kind: &NestedElementKind, item: &Value) -> bool {
650 kind.selector
651 .iter()
652 .all(|selector| selector_matches(selector, item))
653}
654
655fn selector_matches(selector: &KindSelector, item: &Value) -> bool {
657 get_nested(item, &selector.field)
658 .and_then(code_field_value)
659 .is_some_and(|value| {
660 selector
661 .codes
662 .iter()
663 .any(|c| c.code == value || c.bo4e_value.as_deref() == Some(value))
664 })
665}
666
667pub fn not_allowed_codes(
675 entity: &str,
676 label: &str,
677 fields: &[FieldRequirement],
678 element: &Value,
679 forbidden: &dyn Fn(&str) -> bool,
680) -> Vec<PidValidationError> {
681 let mut out = Vec::new();
682 for field_req in fields {
683 let values = all_nested(element, &field_req.bo4e_name);
684 if values.is_empty() {
685 continue;
686 }
687 let name = format!("{label}{}", field_req.bo4e_name);
688 for value in values {
689 let Some(code) = code_field_value(value) else {
690 continue;
691 };
692 let status = field_req
693 .valid_codes
694 .iter()
695 .find(|cv| cv.code == code || cv.bo4e_value.as_deref() == Some(code))
696 .and_then(|cv| cv.ahb_status.as_deref());
697 if let Some(status) = status.filter(|s| forbidden(s)) {
698 out.push(PidValidationError::NotAllowed {
699 entity: entity.to_string(),
700 field: name.clone(),
701 value: Some(code.to_string()),
702 ahb_status: status.to_string(),
703 severity: Severity::Warning,
704 });
705 }
706 }
707 }
708 out
709}
710
711fn element_findings(
714 entity: &str,
715 label: &str,
716 kind: &NestedElementKind,
717 item: &Value,
718 required: &dyn Fn(&str) -> Option<Severity>,
719) -> Vec<PidValidationError> {
720 let mut out = Vec::new();
721 for field_req in &kind.fields {
722 let name = format!("{label}[].{}", field_req.bo4e_name);
723 let values = all_nested(item, &field_req.bo4e_name);
724 if values.is_empty() {
725 if let Some(severity) = required(&field_req.ahb_status) {
726 out.push(PidValidationError::MissingField {
727 entity: entity.to_string(),
728 field: name,
729 ahb_status: field_req.ahb_status.clone(),
730 rust_type: field_req.enum_name.clone(),
731 valid_values: code_values_to_tuples(&field_req.valid_codes),
732 severity,
733 });
734 }
735 continue;
736 }
737 for value in values {
738 out.extend(too_long(entity, &name, value, field_req));
739 out.extend(not_a_number(entity, &name, value, field_req));
740 if let Some(value) = invalid_code_value(value, field_req) {
741 out.push(PidValidationError::InvalidCode {
742 entity: entity.to_string(),
743 field: name.clone(),
744 value,
745 valid_values: code_values_to_tuples(&field_req.valid_codes),
746 });
747 }
748 }
749 }
750 out
751}
752
753fn all_nested<'a>(json: &'a Value, path: &str) -> Vec<&'a Value> {
756 if let Some((list, rest)) = path.split_once("[].") {
757 return get_nested(json, list)
758 .and_then(|v| v.as_array())
759 .map(|items| {
760 items
761 .iter()
762 .flat_map(|item| all_nested(item, rest))
763 .collect()
764 })
765 .unwrap_or_default();
766 }
767 get_nested(json, path)
768 .filter(|v| !v.is_null())
769 .into_iter()
770 .collect()
771}
772
773pub fn not_a_number(
778 entity: &str,
779 field: &str,
780 val: &Value,
781 field_req: &FieldRequirement,
782) -> Option<PidValidationError> {
783 if !field_req.valid_codes.is_empty() {
784 return None;
785 }
786 let format = field_req.format.as_deref()?;
787 let text = val.as_str()?;
788 if !format.trim().starts_with('n')
789 || text.is_empty()
790 || field_req
791 .free_data_codes
792 .iter()
793 .any(|c| c.code == text || c.bo4e_value.as_deref() == Some(text))
794 {
795 return None;
796 }
797 let unsigned = text.strip_prefix('-').unwrap_or(text);
798 let digits = unsigned.chars().filter(char::is_ascii_digit).count();
799 let marks = unsigned.chars().filter(|c| matches!(c, '.' | ',')).count();
800 let number = digits > 0 && marks <= 1 && digits + marks == unsigned.chars().count();
801 (!number).then(|| PidValidationError::NotANumber {
802 entity: entity.to_string(),
803 field: field.to_string(),
804 value: text.to_string(),
805 format: format.to_string(),
806 })
807}
808
809pub fn too_long(
813 entity: &str,
814 field: &str,
815 val: &Value,
816 field_req: &FieldRequirement,
817) -> Option<PidValidationError> {
818 if !field_req.valid_codes.is_empty() {
821 return None;
822 }
823 let format = field_req.format.as_deref()?;
824 let text = val.as_str()?;
825 if field_req
829 .free_data_codes
830 .iter()
831 .any(|c| c.code == text || c.bo4e_value.as_deref() == Some(text))
832 {
833 return None;
834 }
835 let digits = format
836 .trim()
837 .strip_prefix("an")
838 .or_else(|| format.trim().strip_prefix('a'))
839 .or_else(|| format.trim().strip_prefix('n'))?;
840 let max: usize = digits.strip_prefix("..").unwrap_or(digits).parse().ok()?;
841 let length = if format.trim().starts_with('n') {
844 text.chars()
845 .filter(|c| !matches!(c, '.' | ',' | '-'))
846 .count()
847 } else {
848 text.chars().count()
849 };
850 (length > max).then(|| PidValidationError::TooLong {
851 entity: entity.to_string(),
852 field: field.to_string(),
853 length,
854 format: format.to_string(),
855 })
856}
857
858fn validate_code_value(
860 val: &Value,
861 entity_req: &EntityRequirement,
862 field_req: &FieldRequirement,
863 errors: &mut Vec<PidValidationError>,
864) {
865 if let Some(value) = invalid_code_value(val, field_req) {
866 errors.push(PidValidationError::InvalidCode {
867 entity: entity_req.entity.clone(),
868 field: field_req.bo4e_name.clone(),
869 value,
870 valid_values: code_values_to_tuples(&field_req.valid_codes),
871 });
872 }
873 errors.extend(too_long(
874 &entity_req.entity,
875 &field_req.bo4e_name,
876 val,
877 field_req,
878 ));
879 errors.extend(not_a_number(
880 &entity_req.entity,
881 &field_req.bo4e_name,
882 val,
883 field_req,
884 ));
885}
886
887pub fn code_field_value(val: &Value) -> Option<&str> {
890 val.as_str()
891 .or_else(|| val.get("code").and_then(|c| c.as_str()))
892}
893
894pub fn invalid_code_value(val: &Value, field_req: &FieldRequirement) -> Option<String> {
901 if field_req.valid_codes.is_empty() {
902 return None;
903 }
904 let value = code_field_value(val)?;
905 let is_valid = field_req
906 .valid_codes
907 .iter()
908 .any(|cv| cv.code == value || cv.bo4e_value.as_deref() == Some(value));
909 (!is_valid).then(|| value.to_string())
910}
911
912pub fn variant_statuses<'a>(entity_req: &'a EntityRequirement, element: &Value) -> Vec<&'a str> {
916 let mut out: Vec<&'a str> = Vec::new();
917 for v in &entity_req.variants {
918 if v.ahb_status.is_empty() {
919 continue;
920 }
921 let value = get_nested(element, &v.discriminator_field).and_then(code_field_value);
922 if value.is_some_and(|s| v.code == s || v.bo4e_value.as_deref() == Some(s))
923 && !out.contains(&v.ahb_status.as_str())
924 {
925 out.push(v.ahb_status.as_str());
926 }
927 }
928 out
929}
930
931pub fn effective_field_requirements<'a>(
946 entity_req: &'a EntityRequirement,
947 element: &Value,
948) -> Cow<'a, [FieldRequirement]> {
949 if entity_req.variants.is_empty() {
950 return Cow::Borrowed(&entity_req.fields);
951 }
952
953 let mut by_field: BTreeMap<&str, Vec<&EntityVariantRequirement>> = BTreeMap::new();
955 for v in &entity_req.variants {
956 by_field
957 .entry(v.discriminator_field.as_str())
958 .or_default()
959 .push(v);
960 }
961 let mut candidates: Vec<&EntityVariantRequirement> = Vec::new();
962 for (field, group) in by_field {
963 let value = get_nested(element, field).and_then(code_field_value);
964 let matched: Vec<&EntityVariantRequirement> = group
965 .iter()
966 .copied()
967 .filter(|v| value.is_some_and(|s| v.code == s || v.bo4e_value.as_deref() == Some(s)))
968 .collect();
969 candidates.extend(if matched.is_empty() { group } else { matched });
970 }
971
972 let mut owned: Vec<&str> = Vec::new();
974 let mut owned_set: BTreeSet<&str> = BTreeSet::new();
975 for v in &entity_req.variants {
976 for f in &v.fields {
977 if owned_set.insert(f.bo4e_name.as_str()) {
978 owned.push(f.bo4e_name.as_str());
979 }
980 }
981 }
982
983 let mut combined: BTreeMap<&str, FieldRequirement> = BTreeMap::new();
984 for name in owned {
985 let reqs: Vec<&FieldRequirement> = candidates
986 .iter()
987 .filter_map(|v| v.fields.iter().find(|f| f.bo4e_name == name))
988 .collect();
989 let Some((first, rest)) = reqs.split_first() else {
990 continue; };
992 let mut field = (*first).clone();
993 let mut statuses_agree = reqs.len() == candidates.len();
994 for r in rest {
995 if r.ahb_status != field.ahb_status {
996 statuses_agree = false;
997 }
998 for cv in &r.valid_codes {
999 if !field.valid_codes.iter().any(|c| c.code == cv.code) {
1000 field.valid_codes.push(cv.clone());
1001 }
1002 }
1003 }
1004 if !statuses_agree {
1005 field.ahb_status = String::new();
1006 }
1007 combined.insert(name, field);
1008 }
1009
1010 let mut result: Vec<FieldRequirement> = Vec::with_capacity(entity_req.fields.len());
1011 for f in &entity_req.fields {
1012 if owned_set.contains(f.bo4e_name.as_str()) {
1013 if let Some(c) = combined.remove(f.bo4e_name.as_str()) {
1014 result.push(c);
1015 }
1016 } else {
1017 result.push(f.clone());
1018 }
1019 }
1020 result.extend(combined.into_values());
1021 Cow::Owned(result)
1022}
1023
1024pub fn absent_groups<'a>(
1027 entity_req: &'a EntityRequirement,
1028 element: &'a Value,
1029) -> impl Iterator<Item = &'a EntityGroupRequirement> + 'a {
1030 entity_req.groups.iter().filter(move |g| {
1031 !g.fields
1032 .iter()
1033 .any(|f| get_nested(element, f).is_some_and(|v| !v.is_null()))
1034 })
1035}
1036
1037pub fn fields_of_unrequired_absent_groups<'a>(
1046 entity_req: &'a EntityRequirement,
1047 element: &'a Value,
1048 mut group_required: impl FnMut(&str) -> bool,
1049) -> BTreeSet<&'a str> {
1050 let exempt: Vec<&EntityGroupRequirement> = absent_groups(entity_req, element)
1051 .filter(|g| !group_required(&g.ahb_status))
1052 .collect();
1053 let mut fields = BTreeSet::new();
1054 for g in &exempt {
1055 for f in &g.fields {
1056 let kept_elsewhere = entity_req.groups.iter().any(|other| {
1059 other.fields.contains(f) && !exempt.iter().any(|e| std::ptr::eq(*e, other))
1060 });
1061 if !kept_elsewhere {
1062 fields.insert(f.as_str());
1063 }
1064 }
1065 }
1066 fields
1067}
1068
1069fn code_values_to_tuples(codes: &[CodeValue]) -> Vec<(String, String)> {
1071 codes
1072 .iter()
1073 .map(|cv| (cv.code.clone(), cv.meaning.clone()))
1074 .collect()
1075}
1076
1077fn to_camel_case(s: &str) -> String {
1083 if s.is_empty() {
1084 return String::new();
1085 }
1086 let mut chars = s.chars();
1087 let first = chars.next().unwrap();
1088 let mut result = first.to_lowercase().to_string();
1089 result.extend(chars);
1090 result
1091}
1092
1093fn snake_to_camel_case(s: &str) -> String {
1104 let mut result = String::with_capacity(s.len());
1105 let mut capitalize_next = false;
1106 for ch in s.chars() {
1107 if ch == '_' {
1108 capitalize_next = true;
1109 } else if capitalize_next {
1110 result.extend(ch.to_uppercase());
1111 capitalize_next = false;
1112 } else {
1113 result.push(ch);
1114 }
1115 }
1116 result
1117}
1118
1119fn is_unconditionally_required(ahb_status: &str) -> bool {
1121 matches!(ahb_status, "X" | "Muss" | "Soll")
1122}
1123
1124#[cfg(test)]
1125mod tests {
1126
1127 #[test]
1132 fn a_numeric_field_holds_a_number() {
1133 let field = FieldRequirement {
1134 bo4e_name: "geplantesPaket".to_string(),
1135 ahb_status: "X [914] ∧ [937]".to_string(),
1136 field_type: "data".to_string(),
1137 format: Some("n..10".to_string()),
1138 enum_name: None,
1139 free_data_codes: vec![],
1140 element_status: String::new(),
1141 context_status: String::new(),
1142 segment: None,
1143 valid_codes: vec![],
1144 ref_type: crate::pid_requirements::Bo4eRefType::Unknown,
1145 };
1146 let check = |v: &str| not_a_number("Prozessdaten", "geplantesPaket", &json!(v), &field);
1147 assert!(check("O5D2DXBG1A").is_some());
1148 assert!(check("1e3").is_some());
1149 assert!(check("1.2.3").is_some());
1150 assert!(check("12").is_none());
1151 assert!(check("-0,5").is_none());
1152 assert!(check("").is_none());
1153 }
1154
1155 #[test]
1156 fn too_long_skips_codes_of_a_free_data_field() {
1157 let field = FieldRequirement {
1158 bo4e_name: "merkmal.code".to_string(),
1159 ahb_status: "X".to_string(),
1160 field_type: "data".to_string(),
1161 format: Some("an..3".to_string()),
1162 enum_name: None,
1163 free_data_codes: vec![CodeValue {
1164 code: "ZA9".to_string(),
1165 meaning: "Übertragungsnetzbetreiber".to_string(),
1166 enum_name: None,
1167 bo4e_value: Some("uebertragungsnetzbetreiber".to_string()),
1168 ahb_status: None,
1169 }],
1170 element_status: String::new(),
1171 context_status: String::new(),
1172 segment: None,
1173 valid_codes: vec![],
1174 ref_type: crate::pid_requirements::Bo4eRefType::Unknown,
1175 };
1176 let check = |v: &str| too_long("E", "zuordnungen[].merkmal.code", &json!(v), &field);
1177 assert!(check("uebertragungsnetzbetreiber").is_none());
1178 assert!(check("ZA9").is_none());
1179 assert!(check("Z1").is_none());
1180 assert!(matches!(
1181 check("ZDNBNAPG56ZM"),
1182 Some(PidValidationError::TooLong { length: 12, .. })
1183 ));
1184 }
1185 use super::*;
1186
1187 #[test]
1191 fn a_variant_bound_kind_is_demanded_only_in_its_variant() {
1192 use crate::pid_requirements::{NestedElementKind, NestedListRequirement, VariantQualifier};
1193 let lists = vec![NestedListRequirement {
1194 field: "zuordnungen".to_string(),
1195 bo4e_type: "Zuordnung".to_string(),
1196 source_path: "sg4.sg8_z22.sg10".to_string(),
1197 kinds: vec![NestedElementKind {
1198 ahb_status: "Muss".to_string(),
1199 selector: vec![],
1200 fields: vec![],
1201 transaction_variant: Some(VariantQualifier {
1202 group: "SG4".to_string(),
1203 segment: "IDE".to_string(),
1204 codes: vec!["24".to_string()],
1205 }),
1206 }],
1207 parent_selector: vec![],
1208 nested: vec![],
1209 }];
1210 let demanded = |holds: Option<bool>| {
1211 let mut errors = Vec::new();
1212 validate_nested_lists(
1213 "SummenzeitreihenDaten",
1214 &serde_json::json!({}),
1215 &lists,
1216 "",
1217 &|_, _| Some(Severity::Error),
1218 &|_| holds,
1219 &|_, _| false,
1220 &mut errors,
1221 );
1222 errors.len()
1223 };
1224 assert_eq!(demanded(Some(true)), 1);
1225 assert_eq!(demanded(Some(false)), 0);
1226 assert_eq!(demanded(None), 0);
1227 }
1228 use crate::pid_requirements::{
1229 Bo4eRefType, Cardinality, CodeValue, EntityRequirement, FieldRequirement, PidRequirements,
1230 };
1231 use serde_json::json;
1232
1233 fn sample_requirements() -> PidRequirements {
1234 PidRequirements {
1235 transaktion_wiederholbar: false,
1236 pid: "55001".to_string(),
1237 beschreibung: "Anmeldung verb. MaLo".to_string(),
1238 entities: vec![
1239 EntityRequirement {
1240 entity: "Prozessdaten".to_string(),
1241 ref_type: Bo4eRefType::Object {
1242 type_name: "Prozessdaten".to_string(),
1243
1244 cardinality: Cardinality::REQUIRED,
1245 },
1246
1247 ahb_status: "Muss".to_string(),
1248 map_key: None,
1249 scope: EntityScope::Transaction,
1250 variants: vec![],
1251 groups: vec![],
1252 fields: vec![
1253 FieldRequirement {
1254 bo4e_name: "vorgangId".to_string(),
1255 ahb_status: "X".to_string(),
1256 field_type: "data".to_string(),
1257 format: None,
1258 enum_name: None,
1259 free_data_codes: Vec::new(),
1260 element_status: String::new(),
1261 context_status: String::new(),
1262 segment: None,
1263 valid_codes: vec![],
1264 ref_type: Bo4eRefType::Unknown,
1265 },
1266 FieldRequirement {
1267 bo4e_name: "transaktionsgrund".to_string(),
1268 ahb_status: "X".to_string(),
1269 field_type: "code".to_string(),
1270 format: None,
1271 enum_name: Some("Transaktionsgrund".to_string()),
1272 free_data_codes: Vec::new(),
1273 element_status: String::new(),
1274 context_status: String::new(),
1275 segment: None,
1276 valid_codes: vec![
1277 CodeValue {
1278 code: "E01".to_string(),
1279 meaning: "Ein-/Auszug (Einzug)".to_string(),
1280 enum_name: None,
1281 bo4e_value: None,
1282 ahb_status: None,
1283 },
1284 CodeValue {
1285 code: "E03".to_string(),
1286 meaning: "Wechsel".to_string(),
1287 enum_name: None,
1288 bo4e_value: None,
1289 ahb_status: None,
1290 },
1291 ],
1292 ref_type: Bo4eRefType::Unknown,
1293 },
1294 ],
1295 nested: Vec::new(),
1296 },
1297 EntityRequirement {
1298 entity: "Marktlokation".to_string(),
1299 ref_type: Bo4eRefType::Object {
1300 type_name: "Marktlokation".to_string(),
1301
1302 cardinality: Cardinality::REQUIRED,
1303 },
1304
1305 ahb_status: "Muss".to_string(),
1306 map_key: None,
1307 scope: EntityScope::Transaction,
1308 variants: vec![],
1309 groups: vec![],
1310 fields: vec![
1311 FieldRequirement {
1312 bo4e_name: "marktlokationsId".to_string(),
1313 ahb_status: "X".to_string(),
1314 field_type: "data".to_string(),
1315 format: None,
1316 enum_name: None,
1317 free_data_codes: Vec::new(),
1318 element_status: String::new(),
1319 context_status: String::new(),
1320 segment: None,
1321 valid_codes: vec![],
1322 ref_type: Bo4eRefType::Unknown,
1323 },
1324 FieldRequirement {
1325 bo4e_name: "haushaltskunde".to_string(),
1326 ahb_status: "X".to_string(),
1327 field_type: "code".to_string(),
1328 format: None,
1329 enum_name: Some("Haushaltskunde".to_string()),
1330 free_data_codes: Vec::new(),
1331 element_status: String::new(),
1332 context_status: String::new(),
1333 segment: None,
1334 valid_codes: vec![
1335 CodeValue {
1336 code: "Z15".to_string(),
1337 meaning: "Ja".to_string(),
1338 enum_name: None,
1339 bo4e_value: None,
1340 ahb_status: None,
1341 },
1342 CodeValue {
1343 code: "Z18".to_string(),
1344 meaning: "Nein".to_string(),
1345 enum_name: None,
1346 bo4e_value: None,
1347 ahb_status: None,
1348 },
1349 ],
1350 ref_type: Bo4eRefType::Unknown,
1351 },
1352 ],
1353 nested: Vec::new(),
1354 },
1355 EntityRequirement {
1356 entity: "Geschaeftspartner".to_string(),
1357 ref_type: Bo4eRefType::Object {
1358 type_name: "Geschaeftspartner".to_string(),
1359
1360 cardinality: Cardinality {
1361 min: 1,
1362 max: Some(7),
1363 },
1364 },
1365
1366 ahb_status: "Muss".to_string(),
1367 map_key: None,
1368 scope: EntityScope::Transaction,
1369 variants: vec![],
1370 groups: vec![],
1371 fields: vec![FieldRequirement {
1372 bo4e_name: "identifikation".to_string(),
1373 ahb_status: "X".to_string(),
1374 field_type: "data".to_string(),
1375 format: None,
1376 enum_name: None,
1377 free_data_codes: Vec::new(),
1378 element_status: String::new(),
1379 context_status: String::new(),
1380 segment: None,
1381 valid_codes: vec![],
1382 ref_type: Bo4eRefType::Unknown,
1383 }],
1384 nested: Vec::new(),
1385 },
1386 ],
1387 }
1388 }
1389
1390 #[test]
1391 fn test_validate_complete_json() {
1392 let reqs = sample_requirements();
1393 let json = json!({
1394 "prozessdaten": {
1395 "vorgangId": "ABC123",
1396 "transaktionsgrund": "E01"
1397 },
1398 "marktlokation": {
1399 "marktlokationsId": "51234567890",
1400 "haushaltskunde": "Z15"
1401 },
1402 "geschaeftspartner": [
1403 { "identifikation": "9900000000003" }
1404 ]
1405 });
1406
1407 let errors = validate_pid_json(&json, &reqs);
1408 assert!(errors.is_empty(), "Expected no errors, got: {errors:?}");
1409 }
1410
1411 #[test]
1412 fn test_validate_missing_entity() {
1413 let reqs = sample_requirements();
1414 let json = json!({
1415 "prozessdaten": {
1416 "vorgangId": "ABC123",
1417 "transaktionsgrund": "E01"
1418 },
1419 "geschaeftspartner": [
1420 { "identifikation": "9900000000003" }
1421 ]
1422 });
1423 let errors = validate_pid_json(&json, &reqs);
1426 assert_eq!(errors.len(), 1);
1427 match &errors[0] {
1428 PidValidationError::MissingEntity {
1429 entity,
1430 ahb_status,
1431 severity,
1432 } => {
1433 assert_eq!(entity, "Marktlokation");
1434 assert_eq!(ahb_status, "Muss");
1435 assert_eq!(severity, &Severity::Error);
1436 }
1437 other => panic!("Expected MissingEntity, got: {other:?}"),
1438 }
1439
1440 let msg = errors[0].to_string();
1442 assert!(msg.contains("ERROR"));
1443 assert!(msg.contains("Marktlokation"));
1444 assert!(msg.contains("Muss"));
1445 }
1446
1447 #[test]
1448 fn test_validate_missing_field() {
1449 let reqs = sample_requirements();
1450 let json = json!({
1451 "prozessdaten": {
1452 "transaktionsgrund": "E01"
1453 },
1455 "marktlokation": {
1456 "marktlokationsId": "51234567890",
1457 "haushaltskunde": "Z15"
1458 },
1459 "geschaeftspartner": [
1460 { "identifikation": "9900000000003" }
1461 ]
1462 });
1463
1464 let errors = validate_pid_json(&json, &reqs);
1465 assert_eq!(errors.len(), 1);
1466 match &errors[0] {
1467 PidValidationError::MissingField {
1468 entity,
1469 field,
1470 ahb_status,
1471 severity,
1472 ..
1473 } => {
1474 assert_eq!(entity, "Prozessdaten");
1475 assert_eq!(field, "vorgangId");
1476 assert_eq!(ahb_status, "X");
1477 assert_eq!(severity, &Severity::Error);
1478 }
1479 other => panic!("Expected MissingField, got: {other:?}"),
1480 }
1481
1482 let msg = errors[0].to_string();
1483 assert!(msg.contains("ERROR"));
1484 assert!(msg.contains("Prozessdaten.vorgangId"));
1485 }
1486
1487 #[test]
1488 fn test_validate_invalid_code() {
1489 let reqs = sample_requirements();
1490 let json = json!({
1491 "prozessdaten": {
1492 "vorgangId": "ABC123",
1493 "transaktionsgrund": "E01"
1494 },
1495 "marktlokation": {
1496 "marktlokationsId": "51234567890",
1497 "haushaltskunde": "Z99" },
1499 "geschaeftspartner": [
1500 { "identifikation": "9900000000003" }
1501 ]
1502 });
1503
1504 let errors = validate_pid_json(&json, &reqs);
1505 assert_eq!(errors.len(), 1);
1506 match &errors[0] {
1507 PidValidationError::InvalidCode {
1508 entity,
1509 field,
1510 value,
1511 valid_values,
1512 } => {
1513 assert_eq!(entity, "Marktlokation");
1514 assert_eq!(field, "haushaltskunde");
1515 assert_eq!(value, "Z99");
1516 assert_eq!(valid_values.len(), 2);
1517 assert!(valid_values.iter().any(|(c, _)| c == "Z15"));
1518 assert!(valid_values.iter().any(|(c, _)| c == "Z18"));
1519 }
1520 other => panic!("Expected InvalidCode, got: {other:?}"),
1521 }
1522
1523 let msg = errors[0].to_string();
1524 assert!(msg.contains("INVALID"));
1525 assert!(msg.contains("Z99"));
1526 assert!(msg.contains("Z15"));
1527 }
1528
1529 #[test]
1530 fn test_validate_array_entity() {
1531 let reqs = sample_requirements();
1532 let json = json!({
1533 "prozessdaten": {
1534 "vorgangId": "ABC123",
1535 "transaktionsgrund": "E01"
1536 },
1537 "marktlokation": {
1538 "marktlokationsId": "51234567890",
1539 "haushaltskunde": "Z15"
1540 },
1541 "geschaeftspartner": [
1542 { "identifikation": "9900000000003" },
1543 { } ]
1545 });
1546
1547 let errors = validate_pid_json(&json, &reqs);
1548 assert_eq!(errors.len(), 1);
1549 match &errors[0] {
1550 PidValidationError::MissingField { entity, field, .. } => {
1551 assert_eq!(entity, "Geschaeftspartner");
1552 assert_eq!(field, "identifikation");
1553 }
1554 other => panic!("Expected MissingField, got: {other:?}"),
1555 }
1556 }
1557
1558 #[test]
1559 fn test_to_camel_case() {
1560 assert_eq!(to_camel_case("Prozessdaten"), "prozessdaten");
1561 assert_eq!(
1562 to_camel_case("RuhendeMarktlokation"),
1563 "ruhendeMarktlokation"
1564 );
1565 assert_eq!(to_camel_case("Marktlokation"), "marktlokation");
1566 assert_eq!(to_camel_case(""), "");
1567 }
1568
1569 #[test]
1570 fn test_snake_to_camel_case() {
1571 assert_eq!(snake_to_camel_case("code_codepflege"), "codeCodepflege");
1572 assert_eq!(snake_to_camel_case("vorgang_id"), "vorgangId");
1573 assert_eq!(snake_to_camel_case("marktlokation"), "marktlokation");
1574 assert_eq!(snake_to_camel_case(""), "");
1575 assert_eq!(snake_to_camel_case("a_b_c"), "aBC");
1576 }
1577
1578 #[test]
1581 fn test_camel_case_fallback_for_snake_case_bo4e_name() {
1582 let reqs = PidRequirements {
1583 transaktion_wiederholbar: false,
1584 pid: "55077".to_string(),
1585 beschreibung: "Test camelCase fallback".to_string(),
1586 entities: vec![EntityRequirement {
1587 entity: "Zuordnung".to_string(),
1588 ref_type: Bo4eRefType::Object {
1589 type_name: "Zuordnung".to_string(),
1590
1591 cardinality: Cardinality::REQUIRED,
1592 },
1593
1594 ahb_status: "Muss".to_string(),
1595 map_key: None,
1596 scope: EntityScope::Transaction,
1597 variants: vec![],
1598 groups: vec![],
1599 fields: vec![
1600 FieldRequirement {
1601 bo4e_name: "code_codepflege".to_string(),
1603 ahb_status: "X".to_string(),
1604 field_type: "data".to_string(),
1605 format: None,
1606 enum_name: None,
1607 free_data_codes: Vec::new(),
1608 element_status: String::new(),
1609 context_status: String::new(),
1610 segment: None,
1611 valid_codes: vec![],
1612 ref_type: Bo4eRefType::Unknown,
1613 },
1614 FieldRequirement {
1615 bo4e_name: "codeliste".to_string(),
1616 ahb_status: "X".to_string(),
1617 field_type: "data".to_string(),
1618 format: None,
1619 enum_name: None,
1620 free_data_codes: Vec::new(),
1621 element_status: String::new(),
1622 context_status: String::new(),
1623 segment: None,
1624 valid_codes: vec![],
1625 ref_type: Bo4eRefType::Unknown,
1626 },
1627 ],
1628 nested: Vec::new(),
1629 }],
1630 };
1631
1632 let json_camel = json!({
1635 "zuordnung": {
1636 "codeCodepflege": "DE_BDEW",
1637 "codeliste": "6"
1638 }
1639 });
1640
1641 let errors = validate_pid_json(&json_camel, &reqs);
1642 assert!(
1643 errors.is_empty(),
1644 "Expected no errors when field is present under camelCase key, got: {errors:?}"
1645 );
1646
1647 let json_snake = json!({
1649 "zuordnung": {
1650 "code_codepflege": "DE_BDEW",
1651 "codeliste": "6"
1652 }
1653 });
1654
1655 let errors = validate_pid_json(&json_snake, &reqs);
1656 assert!(
1657 errors.is_empty(),
1658 "Expected no errors when field is present under snake_case key, got: {errors:?}"
1659 );
1660
1661 let json_missing = json!({
1663 "zuordnung": {
1664 "codeliste": "6"
1665 }
1666 });
1667
1668 let errors = validate_pid_json(&json_missing, &reqs);
1669 assert_eq!(errors.len(), 1);
1670 match &errors[0] {
1671 PidValidationError::MissingField { field, .. } => {
1672 assert_eq!(field, "code_codepflege");
1673 }
1674 other => panic!("Expected MissingField, got: {other:?}"),
1675 }
1676 }
1677
1678 #[test]
1679 fn test_is_unconditionally_required() {
1680 assert!(is_unconditionally_required("X"));
1681 assert!(is_unconditionally_required("Muss"));
1682 assert!(is_unconditionally_required("Soll"));
1683 assert!(!is_unconditionally_required("Kann"));
1684 assert!(!is_unconditionally_required("[1]"));
1685 assert!(!is_unconditionally_required(""));
1686 }
1687
1688 #[test]
1689 fn test_validation_report_display() {
1690 let errors = vec![
1691 PidValidationError::MissingEntity {
1692 entity: "Marktlokation".to_string(),
1693 ahb_status: "Muss".to_string(),
1694 severity: Severity::Error,
1695 },
1696 PidValidationError::MissingField {
1697 entity: "Prozessdaten".to_string(),
1698 field: "vorgangId".to_string(),
1699 ahb_status: "X".to_string(),
1700 rust_type: None,
1701 valid_values: vec![],
1702 severity: Severity::Error,
1703 },
1704 ];
1705 let report = ValidationReport(errors);
1706 assert!(report.has_errors());
1707 assert_eq!(report.len(), 2);
1708 assert!(!report.is_empty());
1709
1710 let display = report.to_string();
1711 assert!(display.contains("missing entity 'Marktlokation'"));
1712 assert!(display.contains("missing Prozessdaten.vorgangId"));
1713 }
1714
1715 #[test]
1716 fn test_missing_field_with_type_and_values_display() {
1717 let err = PidValidationError::MissingField {
1718 entity: "Marktlokation".to_string(),
1719 field: "haushaltskunde".to_string(),
1720 ahb_status: "Muss".to_string(),
1721 rust_type: Some("Haushaltskunde".to_string()),
1722 valid_values: vec![
1723 ("Z15".to_string(), "Ja".to_string()),
1724 ("Z18".to_string(), "Nein".to_string()),
1725 ],
1726 severity: Severity::Error,
1727 };
1728 let msg = err.to_string();
1729 assert!(msg.contains("type: Haushaltskunde"));
1730 assert!(msg.contains("valid: Z15 (Ja), Z18 (Nein)"));
1731 }
1732
1733 #[test]
1734 fn test_optional_fields_not_flagged() {
1735 let reqs = PidRequirements {
1736 transaktion_wiederholbar: false,
1737 pid: "99999".to_string(),
1738 beschreibung: "Test".to_string(),
1739 entities: vec![EntityRequirement {
1740 entity: "Test".to_string(),
1741 ref_type: Bo4eRefType::Object {
1742 type_name: "Test".to_string(),
1743
1744 cardinality: Cardinality::OPTIONAL,
1745 },
1746
1747 ahb_status: "Kann".to_string(),
1748 map_key: None,
1749 scope: EntityScope::Transaction,
1750 variants: vec![],
1751 groups: vec![],
1752 fields: vec![FieldRequirement {
1753 bo4e_name: "optionalField".to_string(),
1754 ahb_status: "Kann".to_string(),
1755 field_type: "data".to_string(),
1756 format: None,
1757 enum_name: None,
1758 free_data_codes: Vec::new(),
1759 element_status: String::new(),
1760 context_status: String::new(),
1761 segment: None,
1762 valid_codes: vec![],
1763 ref_type: Bo4eRefType::Unknown,
1764 }],
1765 nested: Vec::new(),
1766 }],
1767 };
1768
1769 let errors = validate_pid_json(&json!({}), &reqs);
1771 assert!(errors.is_empty());
1772
1773 let errors = validate_pid_json(&json!({ "test": {} }), &reqs);
1775 assert!(errors.is_empty());
1776 }
1777
1778 #[test]
1781 fn test_nested_dot_path_fields_not_falsely_missing() {
1782 let reqs = PidRequirements {
1783 transaktion_wiederholbar: false,
1784 pid: "55001".to_string(),
1785 beschreibung: "Test nested paths".to_string(),
1786 entities: vec![EntityRequirement {
1787 entity: "ProduktpaketDaten".to_string(),
1788 ref_type: Bo4eRefType::Object {
1789 type_name: "ProduktpaketDaten".to_string(),
1790
1791 cardinality: Cardinality {
1792 min: 1,
1793 max: Some(99999),
1794 },
1795 },
1796
1797 ahb_status: "Muss".to_string(),
1798 map_key: None,
1799 scope: EntityScope::Transaction,
1800 variants: vec![],
1801 groups: vec![],
1802 fields: vec![
1803 FieldRequirement {
1804 bo4e_name: "produktIdentifikation.funktion".to_string(),
1805 ahb_status: "X".to_string(),
1806 field_type: "code".to_string(),
1807 format: None,
1808 enum_name: Some("Produktidentifikation".to_string()),
1809 free_data_codes: Vec::new(),
1810 element_status: String::new(),
1811 context_status: String::new(),
1812 segment: None,
1813 valid_codes: vec![CodeValue {
1814 code: "5".to_string(),
1815 meaning: "Produktidentifikation".to_string(),
1816 enum_name: None,
1817 bo4e_value: None,
1818 ahb_status: None,
1819 }],
1820 ref_type: Bo4eRefType::Unknown,
1821 },
1822 FieldRequirement {
1823 bo4e_name: "produktMerkmal.code".to_string(),
1824 ahb_status: "X".to_string(),
1825 field_type: "code".to_string(),
1826 format: None,
1827 enum_name: None,
1828 free_data_codes: Vec::new(),
1829 element_status: String::new(),
1830 context_status: String::new(),
1831 segment: None,
1832 valid_codes: vec![],
1833 ref_type: Bo4eRefType::Unknown,
1834 },
1835 ],
1836 nested: Vec::new(),
1837 }],
1838 };
1839
1840 let json = json!({
1842 "produktpaketDaten": [{
1843 "produktIdentifikation": { "funktion": "5", "id": "9991000002082", "typ": "Z11" },
1844 "produktMerkmal": { "code": "ZH9" }
1845 }]
1846 });
1847
1848 let errors = validate_pid_json(&json, &reqs);
1849 assert!(
1850 errors.is_empty(),
1851 "Nested dot-path fields should be found (issue #48), got: {errors:?}"
1852 );
1853 }
1854
1855 #[test]
1856 fn test_nested_dot_path_truly_missing() {
1857 let reqs = PidRequirements {
1858 transaktion_wiederholbar: false,
1859 pid: "55001".to_string(),
1860 beschreibung: "Test nested paths missing".to_string(),
1861 entities: vec![EntityRequirement {
1862 entity: "ProduktpaketDaten".to_string(),
1863 ref_type: Bo4eRefType::Object {
1864 type_name: "ProduktpaketDaten".to_string(),
1865
1866 cardinality: Cardinality {
1867 min: 1,
1868 max: Some(99999),
1869 },
1870 },
1871
1872 ahb_status: "Muss".to_string(),
1873 map_key: None,
1874 scope: EntityScope::Transaction,
1875 variants: vec![],
1876 groups: vec![],
1877 fields: vec![FieldRequirement {
1878 bo4e_name: "produktIdentifikation.funktion".to_string(),
1879 ahb_status: "X".to_string(),
1880 field_type: "data".to_string(),
1881 format: None,
1882 enum_name: None,
1883 free_data_codes: Vec::new(),
1884 element_status: String::new(),
1885 context_status: String::new(),
1886 segment: None,
1887 valid_codes: vec![],
1888 ref_type: Bo4eRefType::Unknown,
1889 }],
1890 nested: Vec::new(),
1891 }],
1892 };
1893
1894 let json = json!({
1896 "produktpaketDaten": [{
1897 "produktIdentifikation": { "id": "123" }
1898 }]
1899 });
1900
1901 let errors = validate_pid_json(&json, &reqs);
1902 assert_eq!(errors.len(), 1, "Should report missing nested field");
1903 match &errors[0] {
1904 PidValidationError::MissingField { field, .. } => {
1905 assert_eq!(field, "produktIdentifikation.funktion");
1906 }
1907 other => panic!("Expected MissingField, got: {other:?}"),
1908 }
1909 }
1910
1911 fn field(name: &str, status: &str, codes: &[(&str, &str)]) -> FieldRequirement {
1912 FieldRequirement {
1913 bo4e_name: name.to_string(),
1914 ahb_status: status.to_string(),
1915 field_type: if codes.is_empty() { "data" } else { "code" }.to_string(),
1916 format: None,
1917 enum_name: None,
1918 free_data_codes: Vec::new(),
1919 element_status: String::new(),
1920 context_status: String::new(),
1921 segment: None,
1922 valid_codes: codes
1923 .iter()
1924 .map(|(code, mapped)| CodeValue {
1925 code: code.to_string(),
1926 meaning: String::new(),
1927 enum_name: None,
1928 bo4e_value: Some(mapped.to_string()),
1929 ahb_status: None,
1930 })
1931 .collect(),
1932 ref_type: Bo4eRefType::Unknown,
1933 }
1934 }
1935
1936 fn multi_variant_requirements() -> PidRequirements {
1939 let z03 = ("Z03", "messlokationsadresse");
1940 let z07 = ("Z07", "kundeMsb");
1941 PidRequirements {
1942 transaktion_wiederholbar: false,
1943 pid: "55042".to_string(),
1944 beschreibung: String::new(),
1945 entities: vec![EntityRequirement {
1946 entity: "Geschaeftspartner".to_string(),
1947 ref_type: Bo4eRefType::Object {
1948 type_name: "Geschaeftspartner".to_string(),
1949 cardinality: Cardinality {
1950 min: 1,
1951 max: Some(99),
1952 },
1953 },
1954 ahb_status: "Muss".to_string(),
1955 fields: vec![
1957 field("adresse.ort", "X", &[]),
1958 field("name1", "X", &[]),
1959 field("partnerrolle", "X", &[z03, z07]),
1960 ],
1961 map_key: None,
1962 scope: EntityScope::Transaction,
1963 variants: vec![
1964 EntityVariantRequirement {
1965 discriminator_field: "partnerrolle".to_string(),
1966 code: "Z03".to_string(),
1967 bo4e_value: Some("messlokationsadresse".to_string()),
1968 source_paths: vec!["sg4.sg12_z03".to_string()],
1969 fields: vec![
1970 field("adresse.ort", "X", &[]),
1971 field("partnerrolle", "X", &[z03]),
1972 ],
1973 ahb_status: String::new(),
1974 },
1975 EntityVariantRequirement {
1976 discriminator_field: "partnerrolle".to_string(),
1977 code: "Z07".to_string(),
1978 bo4e_value: Some("kundeMsb".to_string()),
1979 source_paths: vec!["sg4.sg12_z07".to_string()],
1980 fields: vec![field("name1", "X", &[]), field("partnerrolle", "X", &[z07])],
1981 ahb_status: String::new(),
1982 },
1983 ],
1984 groups: vec![],
1985 nested: Vec::new(),
1986 }],
1987 }
1988 }
1989
1990 fn multi_group_requirements(z22_status: &str) -> PidRequirements {
1992 let group = |path: &str, status: &str, fields: &[&str]| EntityGroupRequirement {
1993 source_path: path.to_string(),
1994 ahb_status: status.to_string(),
1995 fields: fields.iter().map(|f| f.to_string()).collect(),
1996 };
1997 PidRequirements {
1998 transaktion_wiederholbar: false,
1999 pid: "55043".to_string(),
2000 beschreibung: String::new(),
2001 entities: vec![EntityRequirement {
2002 entity: "Marktlokation".to_string(),
2003 ref_type: Bo4eRefType::Object {
2004 type_name: "Marktlokation".to_string(),
2005 cardinality: Cardinality::REQUIRED,
2006 },
2007 ahb_status: "Muss".to_string(),
2008 fields: vec![
2009 field("marktlokationsId", "X", &[]),
2010 field("ruhendeMarktlokationsId", "X", &[]),
2011 field("ruhendeMarktlokationZeitraumId", "Kann", &[]),
2012 ],
2013 map_key: None,
2014 scope: EntityScope::Transaction,
2015 variants: vec![],
2016 groups: vec![
2017 group("sg4.sg5_z16", "Muss", &["marktlokationsId"]),
2018 group(
2019 "sg4.sg5_z22",
2020 z22_status,
2021 &["ruhendeMarktlokationsId", "ruhendeMarktlokationZeitraumId"],
2022 ),
2023 ],
2024 nested: Vec::new(),
2025 }],
2026 }
2027 }
2028
2029 fn missing_fields(errors: &[PidValidationError]) -> Vec<&str> {
2030 errors
2031 .iter()
2032 .filter_map(|e| match e {
2033 PidValidationError::MissingField { field, .. } => Some(field.as_str()),
2034 _ => None,
2035 })
2036 .collect()
2037 }
2038
2039 #[test]
2040 fn an_absent_optional_groups_fields_are_not_demanded() {
2041 let reqs = multi_group_requirements("Soll [2003]");
2042 let errors = validate_pid_json(&json!({ "marktlokation": {} }), &reqs);
2043 assert_eq!(missing_fields(&errors), vec!["marktlokationsId"]);
2045 }
2046
2047 #[test]
2048 fn a_filled_groups_fields_are_demanded() {
2049 let reqs = multi_group_requirements("Soll [2003]");
2050 let json = json!({ "marktlokation": {
2051 "marktlokationsId": "51238696781",
2052 "ruhendeMarktlokationZeitraumId": "1"
2053 }});
2054 let errors = validate_pid_json(&json, &reqs);
2055 assert_eq!(missing_fields(&errors), vec!["ruhendeMarktlokationsId"]);
2056 }
2057
2058 #[test]
2059 fn an_absent_required_groups_fields_are_demanded() {
2060 let reqs = multi_group_requirements("Muss");
2061 let json = json!({ "marktlokation": { "marktlokationsId": "51238696781" } });
2062 let errors = validate_pid_json(&json, &reqs);
2063 assert_eq!(missing_fields(&errors), vec!["ruhendeMarktlokationsId"]);
2064 }
2065
2066 #[test]
2067 fn multi_variant_entity_uses_the_elements_own_variant() {
2068 let reqs = multi_variant_requirements();
2069 let json = json!({
2071 "geschaeftspartner": [
2072 { "partnerrolle": "Z03", "adresse": { "ort": "Berlin" } },
2073 { "partnerrolle": "kundeMsb", "name1": "Muster" },
2074 { "partnerrolle": { "code": "messlokationsadresse", "meaning": "x" },
2075 "adresse": { "ort": "Köln" } },
2076 { "partnerrolle": { "code": "Z07" }, "name1": "Beispiel" },
2077 ]
2078 });
2079 let errors = validate_pid_json(&json, &reqs);
2080 assert!(errors.is_empty(), "{}", ValidationReport(errors));
2081 }
2082
2083 #[test]
2084 fn multi_variant_entity_reports_variant_required_fields() {
2085 let reqs = multi_variant_requirements();
2086 let json = json!({ "geschaeftspartner": [{ "partnerrolle": "kundeMsb" }] });
2087 let errors = validate_pid_json(&json, &reqs);
2088 assert_eq!(errors.len(), 1, "{}", ValidationReport(errors));
2089 assert!(matches!(
2090 &errors[0],
2091 PidValidationError::MissingField { field, .. } if field == "name1"
2092 ));
2093 }
2094
2095 #[test]
2096 fn multi_variant_entity_unknown_qualifier_is_invalid_code_only() {
2097 let reqs = multi_variant_requirements();
2098 for bad in [json!("Z99"), json!("bogus"), json!({ "code": "Z99" })] {
2099 let json = json!({ "geschaeftspartner": [{ "partnerrolle": bad }] });
2100 let errors = validate_pid_json(&json, &reqs);
2101 assert_eq!(errors.len(), 1, "{bad}: {}", ValidationReport(errors));
2103 match &errors[0] {
2104 PidValidationError::InvalidCode {
2105 field,
2106 valid_values,
2107 ..
2108 } => {
2109 assert_eq!(field, "partnerrolle");
2110 let codes: Vec<&str> = valid_values.iter().map(|(c, _)| c.as_str()).collect();
2111 assert_eq!(codes, ["Z03", "Z07"]);
2112 }
2113 other => panic!("expected InvalidCode, got {other:?}"),
2114 }
2115 }
2116 }
2117
2118 #[test]
2119 fn code_objects_and_enum_mapped_names_are_code_checked() {
2120 let f = field("partnerrolle", "X", &[("Z07", "kundeMsb")]);
2121 assert_eq!(invalid_code_value(&json!("Z07"), &f), None);
2122 assert_eq!(invalid_code_value(&json!("kundeMsb"), &f), None);
2123 assert_eq!(invalid_code_value(&json!({ "code": "kundeMsb" }), &f), None);
2124 assert_eq!(invalid_code_value(&json!({ "code": "Z07" }), &f), None);
2125 assert_eq!(
2126 invalid_code_value(&json!({ "code": "Z99", "meaning": null }), &f),
2127 Some("Z99".to_string())
2128 );
2129 assert_eq!(
2130 invalid_code_value(&json!("kundeLf"), &f),
2131 Some("kundeLf".to_string())
2132 );
2133 assert_eq!(invalid_code_value(&json!(7), &f), None);
2135 assert_eq!(invalid_code_value(&json!({ "meaning": "x" }), &f), None);
2136 }
2137}