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 let per_segment = candidates.iter().all(|l| l.per_segment);
557 for (index, item) in items.iter().enumerate() {
558 if !per_segment {
559 position.push((field.to_string(), index));
560 }
561 let here = position.clone();
562 let required_here = |status: &str| required(status, &here);
563 let matching: Vec<(&NestedListRequirement, &NestedElementKind)> = match chosen {
564 Some(list) => kind_of(list, item, alone)
565 .filter(|k| applies(k) != Some(false))
566 .map(|k| (list, k))
567 .into_iter()
568 .collect(),
569 None => candidates
570 .iter()
571 .flat_map(|l| {
572 l.kinds
573 .iter()
574 .filter(|k| applies(k) != Some(false))
575 .filter(|k| kind_matches(k, item))
576 .map(move |k| (*l, k))
577 })
578 .collect(),
579 };
580 let best = matching
586 .iter()
587 .map(|(l, k)| {
588 let mut findings = element_findings(entity, &label, k, item, &required_here);
589 nested_lists_at(
590 entity,
591 item,
592 &k.lists,
593 &format!("{label}[]."),
594 &mut here.clone(),
595 required,
596 variant_holds,
597 &|_, _| false,
598 &mut findings,
599 );
600 (*l, *k, findings)
601 })
602 .min_by_key(|(_, k, f)| (f.len(), k.selector.is_empty()));
603 if let Some((list, kind, findings)) = best {
604 errors.extend(
605 findings
606 .into_iter()
607 .filter(|f| !matches!(f, PidValidationError::NotAllowed { .. })),
608 );
609 let forbidden_here = |status: &str| forbidden(status, &here);
612 if forbidden_here(&kind.ahb_status) {
613 let name = kind
614 .selector
615 .first()
616 .and_then(|s| s.codes.first())
617 .map(|c| c.bo4e_value.clone().unwrap_or_else(|| c.code.clone()))
618 .unwrap_or_default();
619 errors.push(PidValidationError::NotAllowed {
620 entity: entity.to_string(),
621 field: format!("{label}[{name}]"),
622 value: None,
623 ahb_status: kind.ahb_status.clone(),
624 severity: Severity::Warning,
625 });
626 } else {
627 errors.extend(not_allowed_codes(
628 entity,
629 &format!("{label}[]."),
630 &kind.fields,
631 item,
632 &forbidden_here,
633 ));
634 }
635 nested_lists_at(
636 entity,
637 item,
638 &list.nested,
639 &format!("{label}[]."),
640 position,
641 required,
642 variant_holds,
643 forbidden,
644 errors,
645 );
646 let mut refused = Vec::new();
649 nested_lists_at(
650 entity,
651 item,
652 &kind.lists,
653 &format!("{label}[]."),
654 position,
655 &|_, _| None,
656 variant_holds,
657 forbidden,
658 &mut refused,
659 );
660 errors.extend(
661 refused
662 .into_iter()
663 .filter(|f| matches!(f, PidValidationError::NotAllowed { .. })),
664 );
665 } else if let (true, Some(list)) = (per_segment, chosen) {
666 let selector = list.kinds.iter().flat_map(|k| &k.selector).next();
668 let value = selector
669 .and_then(|s| get_nested(item, &s.field))
670 .and_then(code_field_value);
671 if let (Some(selector), Some(value)) = (selector, value) {
672 errors.push(PidValidationError::InvalidCode {
673 entity: entity.to_string(),
674 field: format!("{label}[].{}", selector.field),
675 value: value.to_string(),
676 valid_values: code_values_to_tuples(
677 &list
678 .kinds
679 .iter()
680 .flat_map(|k| &k.selector)
681 .flat_map(|s| s.codes.iter().cloned())
682 .collect::<Vec<_>>(),
683 ),
684 });
685 }
686 }
687 if !per_segment {
688 position.pop();
689 }
690 }
691 }
692}
693
694fn kind_of<'l>(
698 list: &'l NestedListRequirement,
699 item: &Value,
700 alone: bool,
701) -> Option<&'l NestedElementKind> {
702 match &list.kinds[..] {
703 [only] if alone => Some(only),
704 kinds => kinds.iter().find(|k| kind_matches(k, item)),
705 }
706}
707
708fn kind_matches(kind: &NestedElementKind, item: &Value) -> bool {
711 kind.selector
712 .iter()
713 .all(|selector| selector_matches(selector, item))
714}
715
716fn selector_matches(selector: &KindSelector, item: &Value) -> bool {
718 get_nested(item, &selector.field)
719 .and_then(code_field_value)
720 .is_some_and(|value| {
721 selector
722 .codes
723 .iter()
724 .any(|c| c.code == value || c.bo4e_value.as_deref() == Some(value))
725 })
726}
727
728pub fn not_allowed_codes(
736 entity: &str,
737 label: &str,
738 fields: &[FieldRequirement],
739 element: &Value,
740 forbidden: &dyn Fn(&str) -> bool,
741) -> Vec<PidValidationError> {
742 let mut out = Vec::new();
743 for field_req in fields {
744 let values = all_nested(element, &field_req.bo4e_name);
745 if values.is_empty() {
746 continue;
747 }
748 let name = format!("{label}{}", field_req.bo4e_name);
749 for value in values {
750 let Some(code) = code_field_value(value) else {
751 continue;
752 };
753 let status = field_req
754 .valid_codes
755 .iter()
756 .find(|cv| cv.code == code || cv.bo4e_value.as_deref() == Some(code))
757 .and_then(|cv| cv.ahb_status.as_deref());
758 if let Some(status) = status.filter(|s| forbidden(s)) {
759 out.push(PidValidationError::NotAllowed {
760 entity: entity.to_string(),
761 field: name.clone(),
762 value: Some(code.to_string()),
763 ahb_status: status.to_string(),
764 severity: Severity::Warning,
765 });
766 }
767 }
768 }
769 out
770}
771
772fn element_findings(
775 entity: &str,
776 label: &str,
777 kind: &NestedElementKind,
778 item: &Value,
779 required: &dyn Fn(&str) -> Option<Severity>,
780) -> Vec<PidValidationError> {
781 let mut out = Vec::new();
782 for field_req in &kind.fields {
783 let name = format!("{label}[].{}", field_req.bo4e_name);
784 let values = all_nested(item, &field_req.bo4e_name);
785 if values.is_empty() {
786 if let Some(severity) = required(&field_req.ahb_status) {
787 out.push(PidValidationError::MissingField {
788 entity: entity.to_string(),
789 field: name,
790 ahb_status: field_req.ahb_status.clone(),
791 rust_type: field_req.enum_name.clone(),
792 valid_values: code_values_to_tuples(&field_req.valid_codes),
793 severity,
794 });
795 }
796 continue;
797 }
798 for value in values {
799 out.extend(too_long(entity, &name, value, field_req));
800 out.extend(not_a_number(entity, &name, value, field_req));
801 if let Some(value) = invalid_code_value(value, field_req) {
802 out.push(PidValidationError::InvalidCode {
803 entity: entity.to_string(),
804 field: name.clone(),
805 value,
806 valid_values: code_values_to_tuples(&field_req.valid_codes),
807 });
808 }
809 }
810 }
811 out
812}
813
814fn all_nested<'a>(json: &'a Value, path: &str) -> Vec<&'a Value> {
817 if let Some((list, rest)) = path.split_once("[].") {
818 return get_nested(json, list)
819 .and_then(|v| v.as_array())
820 .map(|items| {
821 items
822 .iter()
823 .flat_map(|item| all_nested(item, rest))
824 .collect()
825 })
826 .unwrap_or_default();
827 }
828 get_nested(json, path)
829 .filter(|v| !v.is_null())
830 .into_iter()
831 .collect()
832}
833
834pub fn not_a_number(
839 entity: &str,
840 field: &str,
841 val: &Value,
842 field_req: &FieldRequirement,
843) -> Option<PidValidationError> {
844 if !field_req.valid_codes.is_empty() {
845 return None;
846 }
847 let format = field_req.format.as_deref()?;
848 let text = val.as_str()?;
849 if !format.trim().starts_with('n')
850 || text.is_empty()
851 || field_req
852 .free_data_codes
853 .iter()
854 .any(|c| c.code == text || c.bo4e_value.as_deref() == Some(text))
855 {
856 return None;
857 }
858 let unsigned = text.strip_prefix('-').unwrap_or(text);
859 let digits = unsigned.chars().filter(char::is_ascii_digit).count();
860 let marks = unsigned.chars().filter(|c| matches!(c, '.' | ',')).count();
861 let number = digits > 0 && marks <= 1 && digits + marks == unsigned.chars().count();
862 (!number).then(|| PidValidationError::NotANumber {
863 entity: entity.to_string(),
864 field: field.to_string(),
865 value: text.to_string(),
866 format: format.to_string(),
867 })
868}
869
870pub fn too_long(
874 entity: &str,
875 field: &str,
876 val: &Value,
877 field_req: &FieldRequirement,
878) -> Option<PidValidationError> {
879 if !field_req.valid_codes.is_empty() {
882 return None;
883 }
884 let format = field_req.format.as_deref()?;
885 let text = val.as_str()?;
886 if field_req
890 .free_data_codes
891 .iter()
892 .any(|c| c.code == text || c.bo4e_value.as_deref() == Some(text))
893 {
894 return None;
895 }
896 let digits = format
897 .trim()
898 .strip_prefix("an")
899 .or_else(|| format.trim().strip_prefix('a'))
900 .or_else(|| format.trim().strip_prefix('n'))?;
901 let max: usize = digits.strip_prefix("..").unwrap_or(digits).parse().ok()?;
902 let length = if format.trim().starts_with('n') {
905 text.chars()
906 .filter(|c| !matches!(c, '.' | ',' | '-'))
907 .count()
908 } else {
909 text.chars().count()
910 };
911 (length > max).then(|| PidValidationError::TooLong {
912 entity: entity.to_string(),
913 field: field.to_string(),
914 length,
915 format: format.to_string(),
916 })
917}
918
919fn validate_code_value(
921 val: &Value,
922 entity_req: &EntityRequirement,
923 field_req: &FieldRequirement,
924 errors: &mut Vec<PidValidationError>,
925) {
926 if let Some(value) = invalid_code_value(val, field_req) {
927 errors.push(PidValidationError::InvalidCode {
928 entity: entity_req.entity.clone(),
929 field: field_req.bo4e_name.clone(),
930 value,
931 valid_values: code_values_to_tuples(&field_req.valid_codes),
932 });
933 }
934 errors.extend(too_long(
935 &entity_req.entity,
936 &field_req.bo4e_name,
937 val,
938 field_req,
939 ));
940 errors.extend(not_a_number(
941 &entity_req.entity,
942 &field_req.bo4e_name,
943 val,
944 field_req,
945 ));
946}
947
948pub fn code_field_value(val: &Value) -> Option<&str> {
951 val.as_str()
952 .or_else(|| val.get("code").and_then(|c| c.as_str()))
953}
954
955pub fn invalid_code_value(val: &Value, field_req: &FieldRequirement) -> Option<String> {
962 if field_req.valid_codes.is_empty() {
963 return None;
964 }
965 let value = code_field_value(val)?;
966 let is_valid = field_req
967 .valid_codes
968 .iter()
969 .any(|cv| cv.code == value || cv.bo4e_value.as_deref() == Some(value));
970 (!is_valid).then(|| value.to_string())
971}
972
973pub fn variant_statuses<'a>(entity_req: &'a EntityRequirement, element: &Value) -> Vec<&'a str> {
977 let mut out: Vec<&'a str> = Vec::new();
978 for v in &entity_req.variants {
979 if v.ahb_status.is_empty() {
980 continue;
981 }
982 let value = get_nested(element, &v.discriminator_field).and_then(code_field_value);
983 if value.is_some_and(|s| v.code == s || v.bo4e_value.as_deref() == Some(s))
984 && !out.contains(&v.ahb_status.as_str())
985 {
986 out.push(v.ahb_status.as_str());
987 }
988 }
989 out
990}
991
992pub fn effective_field_requirements<'a>(
1007 entity_req: &'a EntityRequirement,
1008 element: &Value,
1009) -> Cow<'a, [FieldRequirement]> {
1010 if entity_req.variants.is_empty() {
1011 return Cow::Borrowed(&entity_req.fields);
1012 }
1013
1014 let mut by_field: BTreeMap<&str, Vec<&EntityVariantRequirement>> = BTreeMap::new();
1016 for v in &entity_req.variants {
1017 by_field
1018 .entry(v.discriminator_field.as_str())
1019 .or_default()
1020 .push(v);
1021 }
1022 let mut candidates: Vec<&EntityVariantRequirement> = Vec::new();
1023 for (field, group) in by_field {
1024 let value = get_nested(element, field).and_then(code_field_value);
1025 let matched: Vec<&EntityVariantRequirement> = group
1026 .iter()
1027 .copied()
1028 .filter(|v| value.is_some_and(|s| v.code == s || v.bo4e_value.as_deref() == Some(s)))
1029 .collect();
1030 candidates.extend(if matched.is_empty() { group } else { matched });
1031 }
1032
1033 let mut owned: Vec<&str> = Vec::new();
1035 let mut owned_set: BTreeSet<&str> = BTreeSet::new();
1036 for v in &entity_req.variants {
1037 for f in &v.fields {
1038 if owned_set.insert(f.bo4e_name.as_str()) {
1039 owned.push(f.bo4e_name.as_str());
1040 }
1041 }
1042 }
1043
1044 let mut combined: BTreeMap<&str, FieldRequirement> = BTreeMap::new();
1045 for name in owned {
1046 let reqs: Vec<&FieldRequirement> = candidates
1047 .iter()
1048 .filter_map(|v| v.fields.iter().find(|f| f.bo4e_name == name))
1049 .collect();
1050 let Some((first, rest)) = reqs.split_first() else {
1051 continue; };
1053 let mut field = (*first).clone();
1054 let mut statuses_agree = reqs.len() == candidates.len();
1055 for r in rest {
1056 if r.ahb_status != field.ahb_status {
1057 statuses_agree = false;
1058 }
1059 for cv in &r.valid_codes {
1060 if !field.valid_codes.iter().any(|c| c.code == cv.code) {
1061 field.valid_codes.push(cv.clone());
1062 }
1063 }
1064 }
1065 if !statuses_agree {
1066 field.ahb_status = String::new();
1067 }
1068 combined.insert(name, field);
1069 }
1070
1071 let mut result: Vec<FieldRequirement> = Vec::with_capacity(entity_req.fields.len());
1072 for f in &entity_req.fields {
1073 if owned_set.contains(f.bo4e_name.as_str()) {
1074 if let Some(c) = combined.remove(f.bo4e_name.as_str()) {
1075 result.push(c);
1076 }
1077 } else {
1078 result.push(f.clone());
1079 }
1080 }
1081 result.extend(combined.into_values());
1082 Cow::Owned(result)
1083}
1084
1085pub fn absent_groups<'a>(
1088 entity_req: &'a EntityRequirement,
1089 element: &'a Value,
1090) -> impl Iterator<Item = &'a EntityGroupRequirement> + 'a {
1091 entity_req.groups.iter().filter(move |g| {
1092 !g.fields
1093 .iter()
1094 .any(|f| get_nested(element, f).is_some_and(|v| !v.is_null()))
1095 })
1096}
1097
1098pub fn fields_of_unrequired_absent_groups<'a>(
1107 entity_req: &'a EntityRequirement,
1108 element: &'a Value,
1109 mut group_required: impl FnMut(&str) -> bool,
1110) -> BTreeSet<&'a str> {
1111 let exempt: Vec<&EntityGroupRequirement> = absent_groups(entity_req, element)
1112 .filter(|g| !group_required(&g.ahb_status))
1113 .collect();
1114 let mut fields = BTreeSet::new();
1115 for g in &exempt {
1116 for f in &g.fields {
1117 let kept_elsewhere = entity_req.groups.iter().any(|other| {
1120 other.fields.contains(f) && !exempt.iter().any(|e| std::ptr::eq(*e, other))
1121 });
1122 if !kept_elsewhere {
1123 fields.insert(f.as_str());
1124 }
1125 }
1126 }
1127 fields
1128}
1129
1130fn code_values_to_tuples(codes: &[CodeValue]) -> Vec<(String, String)> {
1132 codes
1133 .iter()
1134 .map(|cv| (cv.code.clone(), cv.meaning.clone()))
1135 .collect()
1136}
1137
1138fn to_camel_case(s: &str) -> String {
1144 if s.is_empty() {
1145 return String::new();
1146 }
1147 let mut chars = s.chars();
1148 let first = chars.next().unwrap();
1149 let mut result = first.to_lowercase().to_string();
1150 result.extend(chars);
1151 result
1152}
1153
1154fn snake_to_camel_case(s: &str) -> String {
1165 let mut result = String::with_capacity(s.len());
1166 let mut capitalize_next = false;
1167 for ch in s.chars() {
1168 if ch == '_' {
1169 capitalize_next = true;
1170 } else if capitalize_next {
1171 result.extend(ch.to_uppercase());
1172 capitalize_next = false;
1173 } else {
1174 result.push(ch);
1175 }
1176 }
1177 result
1178}
1179
1180fn is_unconditionally_required(ahb_status: &str) -> bool {
1182 matches!(ahb_status, "X" | "Muss" | "Soll")
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187
1188 #[test]
1193 fn a_numeric_field_holds_a_number() {
1194 let field = FieldRequirement {
1195 bo4e_name: "geplantesPaket".to_string(),
1196 ahb_status: "X [914] ∧ [937]".to_string(),
1197 field_type: "data".to_string(),
1198 format: Some("n..10".to_string()),
1199 enum_name: None,
1200 free_data_codes: vec![],
1201 element_status: String::new(),
1202 context_status: String::new(),
1203 segment: None,
1204 valid_codes: vec![],
1205 ref_type: crate::pid_requirements::Bo4eRefType::Unknown,
1206 };
1207 let check = |v: &str| not_a_number("Prozessdaten", "geplantesPaket", &json!(v), &field);
1208 assert!(check("O5D2DXBG1A").is_some());
1209 assert!(check("1e3").is_some());
1210 assert!(check("1.2.3").is_some());
1211 assert!(check("12").is_none());
1212 assert!(check("-0,5").is_none());
1213 assert!(check("").is_none());
1214 }
1215
1216 #[test]
1217 fn too_long_skips_codes_of_a_free_data_field() {
1218 let field = FieldRequirement {
1219 bo4e_name: "merkmal.code".to_string(),
1220 ahb_status: "X".to_string(),
1221 field_type: "data".to_string(),
1222 format: Some("an..3".to_string()),
1223 enum_name: None,
1224 free_data_codes: vec![CodeValue {
1225 code: "ZA9".to_string(),
1226 meaning: "Übertragungsnetzbetreiber".to_string(),
1227 enum_name: None,
1228 bo4e_value: Some("uebertragungsnetzbetreiber".to_string()),
1229 ahb_status: None,
1230 }],
1231 element_status: String::new(),
1232 context_status: String::new(),
1233 segment: None,
1234 valid_codes: vec![],
1235 ref_type: crate::pid_requirements::Bo4eRefType::Unknown,
1236 };
1237 let check = |v: &str| too_long("E", "zuordnungen[].merkmal.code", &json!(v), &field);
1238 assert!(check("uebertragungsnetzbetreiber").is_none());
1239 assert!(check("ZA9").is_none());
1240 assert!(check("Z1").is_none());
1241 assert!(matches!(
1242 check("ZDNBNAPG56ZM"),
1243 Some(PidValidationError::TooLong { length: 12, .. })
1244 ));
1245 }
1246 use super::*;
1247
1248 #[test]
1252 fn a_variant_bound_kind_is_demanded_only_in_its_variant() {
1253 use crate::pid_requirements::{NestedElementKind, NestedListRequirement, VariantQualifier};
1254 let lists = vec![NestedListRequirement {
1255 field: "zuordnungen".to_string(),
1256 bo4e_type: "Zuordnung".to_string(),
1257 source_path: "sg4.sg8_z22.sg10".to_string(),
1258 kinds: vec![NestedElementKind {
1259 ahb_status: "Muss".to_string(),
1260 selector: vec![],
1261 fields: vec![],
1262 transaction_variant: Some(VariantQualifier {
1263 group: "SG4".to_string(),
1264 segment: "IDE".to_string(),
1265 codes: vec!["24".to_string()],
1266 }),
1267 lists: vec![],
1268 }],
1269 parent_selector: vec![],
1270 nested: vec![],
1271 per_segment: false,
1272 }];
1273 let demanded = |holds: Option<bool>| {
1274 let mut errors = Vec::new();
1275 validate_nested_lists(
1276 "SummenzeitreihenDaten",
1277 &serde_json::json!({}),
1278 &lists,
1279 "",
1280 &|_, _| Some(Severity::Error),
1281 &|_| holds,
1282 &|_, _| false,
1283 &mut errors,
1284 );
1285 errors.len()
1286 };
1287 assert_eq!(demanded(Some(true)), 1);
1288 assert_eq!(demanded(Some(false)), 0);
1289 assert_eq!(demanded(None), 0);
1290 }
1291 use crate::pid_requirements::{
1292 Bo4eRefType, Cardinality, CodeValue, EntityRequirement, FieldRequirement, PidRequirements,
1293 };
1294 use serde_json::json;
1295
1296 fn sample_requirements() -> PidRequirements {
1297 PidRequirements {
1298 transaktion_wiederholbar: false,
1299 pid: "55001".to_string(),
1300 beschreibung: "Anmeldung verb. MaLo".to_string(),
1301 entities: vec![
1302 EntityRequirement {
1303 entity: "Prozessdaten".to_string(),
1304 ref_type: Bo4eRefType::Object {
1305 type_name: "Prozessdaten".to_string(),
1306
1307 cardinality: Cardinality::REQUIRED,
1308 },
1309
1310 ahb_status: "Muss".to_string(),
1311 map_key: None,
1312 scope: EntityScope::Transaction,
1313 variants: vec![],
1314 groups: vec![],
1315 fields: vec![
1316 FieldRequirement {
1317 bo4e_name: "vorgangId".to_string(),
1318 ahb_status: "X".to_string(),
1319 field_type: "data".to_string(),
1320 format: None,
1321 enum_name: None,
1322 free_data_codes: Vec::new(),
1323 element_status: String::new(),
1324 context_status: String::new(),
1325 segment: None,
1326 valid_codes: vec![],
1327 ref_type: Bo4eRefType::Unknown,
1328 },
1329 FieldRequirement {
1330 bo4e_name: "transaktionsgrund".to_string(),
1331 ahb_status: "X".to_string(),
1332 field_type: "code".to_string(),
1333 format: None,
1334 enum_name: Some("Transaktionsgrund".to_string()),
1335 free_data_codes: Vec::new(),
1336 element_status: String::new(),
1337 context_status: String::new(),
1338 segment: None,
1339 valid_codes: vec![
1340 CodeValue {
1341 code: "E01".to_string(),
1342 meaning: "Ein-/Auszug (Einzug)".to_string(),
1343 enum_name: None,
1344 bo4e_value: None,
1345 ahb_status: None,
1346 },
1347 CodeValue {
1348 code: "E03".to_string(),
1349 meaning: "Wechsel".to_string(),
1350 enum_name: None,
1351 bo4e_value: None,
1352 ahb_status: None,
1353 },
1354 ],
1355 ref_type: Bo4eRefType::Unknown,
1356 },
1357 ],
1358 nested: Vec::new(),
1359 },
1360 EntityRequirement {
1361 entity: "Marktlokation".to_string(),
1362 ref_type: Bo4eRefType::Object {
1363 type_name: "Marktlokation".to_string(),
1364
1365 cardinality: Cardinality::REQUIRED,
1366 },
1367
1368 ahb_status: "Muss".to_string(),
1369 map_key: None,
1370 scope: EntityScope::Transaction,
1371 variants: vec![],
1372 groups: vec![],
1373 fields: vec![
1374 FieldRequirement {
1375 bo4e_name: "marktlokationsId".to_string(),
1376 ahb_status: "X".to_string(),
1377 field_type: "data".to_string(),
1378 format: None,
1379 enum_name: None,
1380 free_data_codes: Vec::new(),
1381 element_status: String::new(),
1382 context_status: String::new(),
1383 segment: None,
1384 valid_codes: vec![],
1385 ref_type: Bo4eRefType::Unknown,
1386 },
1387 FieldRequirement {
1388 bo4e_name: "haushaltskunde".to_string(),
1389 ahb_status: "X".to_string(),
1390 field_type: "code".to_string(),
1391 format: None,
1392 enum_name: Some("Haushaltskunde".to_string()),
1393 free_data_codes: Vec::new(),
1394 element_status: String::new(),
1395 context_status: String::new(),
1396 segment: None,
1397 valid_codes: vec![
1398 CodeValue {
1399 code: "Z15".to_string(),
1400 meaning: "Ja".to_string(),
1401 enum_name: None,
1402 bo4e_value: None,
1403 ahb_status: None,
1404 },
1405 CodeValue {
1406 code: "Z18".to_string(),
1407 meaning: "Nein".to_string(),
1408 enum_name: None,
1409 bo4e_value: None,
1410 ahb_status: None,
1411 },
1412 ],
1413 ref_type: Bo4eRefType::Unknown,
1414 },
1415 ],
1416 nested: Vec::new(),
1417 },
1418 EntityRequirement {
1419 entity: "Geschaeftspartner".to_string(),
1420 ref_type: Bo4eRefType::Object {
1421 type_name: "Geschaeftspartner".to_string(),
1422
1423 cardinality: Cardinality {
1424 min: 1,
1425 max: Some(7),
1426 },
1427 },
1428
1429 ahb_status: "Muss".to_string(),
1430 map_key: None,
1431 scope: EntityScope::Transaction,
1432 variants: vec![],
1433 groups: vec![],
1434 fields: vec![FieldRequirement {
1435 bo4e_name: "identifikation".to_string(),
1436 ahb_status: "X".to_string(),
1437 field_type: "data".to_string(),
1438 format: None,
1439 enum_name: None,
1440 free_data_codes: Vec::new(),
1441 element_status: String::new(),
1442 context_status: String::new(),
1443 segment: None,
1444 valid_codes: vec![],
1445 ref_type: Bo4eRefType::Unknown,
1446 }],
1447 nested: Vec::new(),
1448 },
1449 ],
1450 }
1451 }
1452
1453 #[test]
1454 fn test_validate_complete_json() {
1455 let reqs = sample_requirements();
1456 let json = json!({
1457 "prozessdaten": {
1458 "vorgangId": "ABC123",
1459 "transaktionsgrund": "E01"
1460 },
1461 "marktlokation": {
1462 "marktlokationsId": "51234567890",
1463 "haushaltskunde": "Z15"
1464 },
1465 "geschaeftspartner": [
1466 { "identifikation": "9900000000003" }
1467 ]
1468 });
1469
1470 let errors = validate_pid_json(&json, &reqs);
1471 assert!(errors.is_empty(), "Expected no errors, got: {errors:?}");
1472 }
1473
1474 #[test]
1475 fn test_validate_missing_entity() {
1476 let reqs = sample_requirements();
1477 let json = json!({
1478 "prozessdaten": {
1479 "vorgangId": "ABC123",
1480 "transaktionsgrund": "E01"
1481 },
1482 "geschaeftspartner": [
1483 { "identifikation": "9900000000003" }
1484 ]
1485 });
1486 let errors = validate_pid_json(&json, &reqs);
1489 assert_eq!(errors.len(), 1);
1490 match &errors[0] {
1491 PidValidationError::MissingEntity {
1492 entity,
1493 ahb_status,
1494 severity,
1495 } => {
1496 assert_eq!(entity, "Marktlokation");
1497 assert_eq!(ahb_status, "Muss");
1498 assert_eq!(severity, &Severity::Error);
1499 }
1500 other => panic!("Expected MissingEntity, got: {other:?}"),
1501 }
1502
1503 let msg = errors[0].to_string();
1505 assert!(msg.contains("ERROR"));
1506 assert!(msg.contains("Marktlokation"));
1507 assert!(msg.contains("Muss"));
1508 }
1509
1510 #[test]
1511 fn test_validate_missing_field() {
1512 let reqs = sample_requirements();
1513 let json = json!({
1514 "prozessdaten": {
1515 "transaktionsgrund": "E01"
1516 },
1518 "marktlokation": {
1519 "marktlokationsId": "51234567890",
1520 "haushaltskunde": "Z15"
1521 },
1522 "geschaeftspartner": [
1523 { "identifikation": "9900000000003" }
1524 ]
1525 });
1526
1527 let errors = validate_pid_json(&json, &reqs);
1528 assert_eq!(errors.len(), 1);
1529 match &errors[0] {
1530 PidValidationError::MissingField {
1531 entity,
1532 field,
1533 ahb_status,
1534 severity,
1535 ..
1536 } => {
1537 assert_eq!(entity, "Prozessdaten");
1538 assert_eq!(field, "vorgangId");
1539 assert_eq!(ahb_status, "X");
1540 assert_eq!(severity, &Severity::Error);
1541 }
1542 other => panic!("Expected MissingField, got: {other:?}"),
1543 }
1544
1545 let msg = errors[0].to_string();
1546 assert!(msg.contains("ERROR"));
1547 assert!(msg.contains("Prozessdaten.vorgangId"));
1548 }
1549
1550 #[test]
1551 fn test_validate_invalid_code() {
1552 let reqs = sample_requirements();
1553 let json = json!({
1554 "prozessdaten": {
1555 "vorgangId": "ABC123",
1556 "transaktionsgrund": "E01"
1557 },
1558 "marktlokation": {
1559 "marktlokationsId": "51234567890",
1560 "haushaltskunde": "Z99" },
1562 "geschaeftspartner": [
1563 { "identifikation": "9900000000003" }
1564 ]
1565 });
1566
1567 let errors = validate_pid_json(&json, &reqs);
1568 assert_eq!(errors.len(), 1);
1569 match &errors[0] {
1570 PidValidationError::InvalidCode {
1571 entity,
1572 field,
1573 value,
1574 valid_values,
1575 } => {
1576 assert_eq!(entity, "Marktlokation");
1577 assert_eq!(field, "haushaltskunde");
1578 assert_eq!(value, "Z99");
1579 assert_eq!(valid_values.len(), 2);
1580 assert!(valid_values.iter().any(|(c, _)| c == "Z15"));
1581 assert!(valid_values.iter().any(|(c, _)| c == "Z18"));
1582 }
1583 other => panic!("Expected InvalidCode, got: {other:?}"),
1584 }
1585
1586 let msg = errors[0].to_string();
1587 assert!(msg.contains("INVALID"));
1588 assert!(msg.contains("Z99"));
1589 assert!(msg.contains("Z15"));
1590 }
1591
1592 #[test]
1593 fn test_validate_array_entity() {
1594 let reqs = sample_requirements();
1595 let json = json!({
1596 "prozessdaten": {
1597 "vorgangId": "ABC123",
1598 "transaktionsgrund": "E01"
1599 },
1600 "marktlokation": {
1601 "marktlokationsId": "51234567890",
1602 "haushaltskunde": "Z15"
1603 },
1604 "geschaeftspartner": [
1605 { "identifikation": "9900000000003" },
1606 { } ]
1608 });
1609
1610 let errors = validate_pid_json(&json, &reqs);
1611 assert_eq!(errors.len(), 1);
1612 match &errors[0] {
1613 PidValidationError::MissingField { entity, field, .. } => {
1614 assert_eq!(entity, "Geschaeftspartner");
1615 assert_eq!(field, "identifikation");
1616 }
1617 other => panic!("Expected MissingField, got: {other:?}"),
1618 }
1619 }
1620
1621 #[test]
1622 fn test_to_camel_case() {
1623 assert_eq!(to_camel_case("Prozessdaten"), "prozessdaten");
1624 assert_eq!(
1625 to_camel_case("RuhendeMarktlokation"),
1626 "ruhendeMarktlokation"
1627 );
1628 assert_eq!(to_camel_case("Marktlokation"), "marktlokation");
1629 assert_eq!(to_camel_case(""), "");
1630 }
1631
1632 #[test]
1633 fn test_snake_to_camel_case() {
1634 assert_eq!(snake_to_camel_case("code_codepflege"), "codeCodepflege");
1635 assert_eq!(snake_to_camel_case("vorgang_id"), "vorgangId");
1636 assert_eq!(snake_to_camel_case("marktlokation"), "marktlokation");
1637 assert_eq!(snake_to_camel_case(""), "");
1638 assert_eq!(snake_to_camel_case("a_b_c"), "aBC");
1639 }
1640
1641 #[test]
1644 fn test_camel_case_fallback_for_snake_case_bo4e_name() {
1645 let reqs = PidRequirements {
1646 transaktion_wiederholbar: false,
1647 pid: "55077".to_string(),
1648 beschreibung: "Test camelCase fallback".to_string(),
1649 entities: vec![EntityRequirement {
1650 entity: "Zuordnung".to_string(),
1651 ref_type: Bo4eRefType::Object {
1652 type_name: "Zuordnung".to_string(),
1653
1654 cardinality: Cardinality::REQUIRED,
1655 },
1656
1657 ahb_status: "Muss".to_string(),
1658 map_key: None,
1659 scope: EntityScope::Transaction,
1660 variants: vec![],
1661 groups: vec![],
1662 fields: vec![
1663 FieldRequirement {
1664 bo4e_name: "code_codepflege".to_string(),
1666 ahb_status: "X".to_string(),
1667 field_type: "data".to_string(),
1668 format: None,
1669 enum_name: None,
1670 free_data_codes: Vec::new(),
1671 element_status: String::new(),
1672 context_status: String::new(),
1673 segment: None,
1674 valid_codes: vec![],
1675 ref_type: Bo4eRefType::Unknown,
1676 },
1677 FieldRequirement {
1678 bo4e_name: "codeliste".to_string(),
1679 ahb_status: "X".to_string(),
1680 field_type: "data".to_string(),
1681 format: None,
1682 enum_name: None,
1683 free_data_codes: Vec::new(),
1684 element_status: String::new(),
1685 context_status: String::new(),
1686 segment: None,
1687 valid_codes: vec![],
1688 ref_type: Bo4eRefType::Unknown,
1689 },
1690 ],
1691 nested: Vec::new(),
1692 }],
1693 };
1694
1695 let json_camel = json!({
1698 "zuordnung": {
1699 "codeCodepflege": "DE_BDEW",
1700 "codeliste": "6"
1701 }
1702 });
1703
1704 let errors = validate_pid_json(&json_camel, &reqs);
1705 assert!(
1706 errors.is_empty(),
1707 "Expected no errors when field is present under camelCase key, got: {errors:?}"
1708 );
1709
1710 let json_snake = json!({
1712 "zuordnung": {
1713 "code_codepflege": "DE_BDEW",
1714 "codeliste": "6"
1715 }
1716 });
1717
1718 let errors = validate_pid_json(&json_snake, &reqs);
1719 assert!(
1720 errors.is_empty(),
1721 "Expected no errors when field is present under snake_case key, got: {errors:?}"
1722 );
1723
1724 let json_missing = json!({
1726 "zuordnung": {
1727 "codeliste": "6"
1728 }
1729 });
1730
1731 let errors = validate_pid_json(&json_missing, &reqs);
1732 assert_eq!(errors.len(), 1);
1733 match &errors[0] {
1734 PidValidationError::MissingField { field, .. } => {
1735 assert_eq!(field, "code_codepflege");
1736 }
1737 other => panic!("Expected MissingField, got: {other:?}"),
1738 }
1739 }
1740
1741 #[test]
1742 fn test_is_unconditionally_required() {
1743 assert!(is_unconditionally_required("X"));
1744 assert!(is_unconditionally_required("Muss"));
1745 assert!(is_unconditionally_required("Soll"));
1746 assert!(!is_unconditionally_required("Kann"));
1747 assert!(!is_unconditionally_required("[1]"));
1748 assert!(!is_unconditionally_required(""));
1749 }
1750
1751 #[test]
1752 fn test_validation_report_display() {
1753 let errors = vec![
1754 PidValidationError::MissingEntity {
1755 entity: "Marktlokation".to_string(),
1756 ahb_status: "Muss".to_string(),
1757 severity: Severity::Error,
1758 },
1759 PidValidationError::MissingField {
1760 entity: "Prozessdaten".to_string(),
1761 field: "vorgangId".to_string(),
1762 ahb_status: "X".to_string(),
1763 rust_type: None,
1764 valid_values: vec![],
1765 severity: Severity::Error,
1766 },
1767 ];
1768 let report = ValidationReport(errors);
1769 assert!(report.has_errors());
1770 assert_eq!(report.len(), 2);
1771 assert!(!report.is_empty());
1772
1773 let display = report.to_string();
1774 assert!(display.contains("missing entity 'Marktlokation'"));
1775 assert!(display.contains("missing Prozessdaten.vorgangId"));
1776 }
1777
1778 #[test]
1779 fn test_missing_field_with_type_and_values_display() {
1780 let err = PidValidationError::MissingField {
1781 entity: "Marktlokation".to_string(),
1782 field: "haushaltskunde".to_string(),
1783 ahb_status: "Muss".to_string(),
1784 rust_type: Some("Haushaltskunde".to_string()),
1785 valid_values: vec![
1786 ("Z15".to_string(), "Ja".to_string()),
1787 ("Z18".to_string(), "Nein".to_string()),
1788 ],
1789 severity: Severity::Error,
1790 };
1791 let msg = err.to_string();
1792 assert!(msg.contains("type: Haushaltskunde"));
1793 assert!(msg.contains("valid: Z15 (Ja), Z18 (Nein)"));
1794 }
1795
1796 #[test]
1797 fn test_optional_fields_not_flagged() {
1798 let reqs = PidRequirements {
1799 transaktion_wiederholbar: false,
1800 pid: "99999".to_string(),
1801 beschreibung: "Test".to_string(),
1802 entities: vec![EntityRequirement {
1803 entity: "Test".to_string(),
1804 ref_type: Bo4eRefType::Object {
1805 type_name: "Test".to_string(),
1806
1807 cardinality: Cardinality::OPTIONAL,
1808 },
1809
1810 ahb_status: "Kann".to_string(),
1811 map_key: None,
1812 scope: EntityScope::Transaction,
1813 variants: vec![],
1814 groups: vec![],
1815 fields: vec![FieldRequirement {
1816 bo4e_name: "optionalField".to_string(),
1817 ahb_status: "Kann".to_string(),
1818 field_type: "data".to_string(),
1819 format: None,
1820 enum_name: None,
1821 free_data_codes: Vec::new(),
1822 element_status: String::new(),
1823 context_status: String::new(),
1824 segment: None,
1825 valid_codes: vec![],
1826 ref_type: Bo4eRefType::Unknown,
1827 }],
1828 nested: Vec::new(),
1829 }],
1830 };
1831
1832 let errors = validate_pid_json(&json!({}), &reqs);
1834 assert!(errors.is_empty());
1835
1836 let errors = validate_pid_json(&json!({ "test": {} }), &reqs);
1838 assert!(errors.is_empty());
1839 }
1840
1841 #[test]
1844 fn test_nested_dot_path_fields_not_falsely_missing() {
1845 let reqs = PidRequirements {
1846 transaktion_wiederholbar: false,
1847 pid: "55001".to_string(),
1848 beschreibung: "Test nested paths".to_string(),
1849 entities: vec![EntityRequirement {
1850 entity: "ProduktpaketDaten".to_string(),
1851 ref_type: Bo4eRefType::Object {
1852 type_name: "ProduktpaketDaten".to_string(),
1853
1854 cardinality: Cardinality {
1855 min: 1,
1856 max: Some(99999),
1857 },
1858 },
1859
1860 ahb_status: "Muss".to_string(),
1861 map_key: None,
1862 scope: EntityScope::Transaction,
1863 variants: vec![],
1864 groups: vec![],
1865 fields: vec![
1866 FieldRequirement {
1867 bo4e_name: "produktIdentifikation.funktion".to_string(),
1868 ahb_status: "X".to_string(),
1869 field_type: "code".to_string(),
1870 format: None,
1871 enum_name: Some("Produktidentifikation".to_string()),
1872 free_data_codes: Vec::new(),
1873 element_status: String::new(),
1874 context_status: String::new(),
1875 segment: None,
1876 valid_codes: vec![CodeValue {
1877 code: "5".to_string(),
1878 meaning: "Produktidentifikation".to_string(),
1879 enum_name: None,
1880 bo4e_value: None,
1881 ahb_status: None,
1882 }],
1883 ref_type: Bo4eRefType::Unknown,
1884 },
1885 FieldRequirement {
1886 bo4e_name: "produktMerkmal.code".to_string(),
1887 ahb_status: "X".to_string(),
1888 field_type: "code".to_string(),
1889 format: None,
1890 enum_name: None,
1891 free_data_codes: Vec::new(),
1892 element_status: String::new(),
1893 context_status: String::new(),
1894 segment: None,
1895 valid_codes: vec![],
1896 ref_type: Bo4eRefType::Unknown,
1897 },
1898 ],
1899 nested: Vec::new(),
1900 }],
1901 };
1902
1903 let json = json!({
1905 "produktpaketDaten": [{
1906 "produktIdentifikation": { "funktion": "5", "id": "9991000002082", "typ": "Z11" },
1907 "produktMerkmal": { "code": "ZH9" }
1908 }]
1909 });
1910
1911 let errors = validate_pid_json(&json, &reqs);
1912 assert!(
1913 errors.is_empty(),
1914 "Nested dot-path fields should be found (issue #48), got: {errors:?}"
1915 );
1916 }
1917
1918 #[test]
1919 fn test_nested_dot_path_truly_missing() {
1920 let reqs = PidRequirements {
1921 transaktion_wiederholbar: false,
1922 pid: "55001".to_string(),
1923 beschreibung: "Test nested paths missing".to_string(),
1924 entities: vec![EntityRequirement {
1925 entity: "ProduktpaketDaten".to_string(),
1926 ref_type: Bo4eRefType::Object {
1927 type_name: "ProduktpaketDaten".to_string(),
1928
1929 cardinality: Cardinality {
1930 min: 1,
1931 max: Some(99999),
1932 },
1933 },
1934
1935 ahb_status: "Muss".to_string(),
1936 map_key: None,
1937 scope: EntityScope::Transaction,
1938 variants: vec![],
1939 groups: vec![],
1940 fields: vec![FieldRequirement {
1941 bo4e_name: "produktIdentifikation.funktion".to_string(),
1942 ahb_status: "X".to_string(),
1943 field_type: "data".to_string(),
1944 format: None,
1945 enum_name: None,
1946 free_data_codes: Vec::new(),
1947 element_status: String::new(),
1948 context_status: String::new(),
1949 segment: None,
1950 valid_codes: vec![],
1951 ref_type: Bo4eRefType::Unknown,
1952 }],
1953 nested: Vec::new(),
1954 }],
1955 };
1956
1957 let json = json!({
1959 "produktpaketDaten": [{
1960 "produktIdentifikation": { "id": "123" }
1961 }]
1962 });
1963
1964 let errors = validate_pid_json(&json, &reqs);
1965 assert_eq!(errors.len(), 1, "Should report missing nested field");
1966 match &errors[0] {
1967 PidValidationError::MissingField { field, .. } => {
1968 assert_eq!(field, "produktIdentifikation.funktion");
1969 }
1970 other => panic!("Expected MissingField, got: {other:?}"),
1971 }
1972 }
1973
1974 fn field(name: &str, status: &str, codes: &[(&str, &str)]) -> FieldRequirement {
1975 FieldRequirement {
1976 bo4e_name: name.to_string(),
1977 ahb_status: status.to_string(),
1978 field_type: if codes.is_empty() { "data" } else { "code" }.to_string(),
1979 format: None,
1980 enum_name: None,
1981 free_data_codes: Vec::new(),
1982 element_status: String::new(),
1983 context_status: String::new(),
1984 segment: None,
1985 valid_codes: codes
1986 .iter()
1987 .map(|(code, mapped)| CodeValue {
1988 code: code.to_string(),
1989 meaning: String::new(),
1990 enum_name: None,
1991 bo4e_value: Some(mapped.to_string()),
1992 ahb_status: None,
1993 })
1994 .collect(),
1995 ref_type: Bo4eRefType::Unknown,
1996 }
1997 }
1998
1999 fn multi_variant_requirements() -> PidRequirements {
2002 let z03 = ("Z03", "messlokationsadresse");
2003 let z07 = ("Z07", "kundeMsb");
2004 PidRequirements {
2005 transaktion_wiederholbar: false,
2006 pid: "55042".to_string(),
2007 beschreibung: String::new(),
2008 entities: vec![EntityRequirement {
2009 entity: "Geschaeftspartner".to_string(),
2010 ref_type: Bo4eRefType::Object {
2011 type_name: "Geschaeftspartner".to_string(),
2012 cardinality: Cardinality {
2013 min: 1,
2014 max: Some(99),
2015 },
2016 },
2017 ahb_status: "Muss".to_string(),
2018 fields: vec![
2020 field("adresse.ort", "X", &[]),
2021 field("name1", "X", &[]),
2022 field("partnerrolle", "X", &[z03, z07]),
2023 ],
2024 map_key: None,
2025 scope: EntityScope::Transaction,
2026 variants: vec![
2027 EntityVariantRequirement {
2028 discriminator_field: "partnerrolle".to_string(),
2029 code: "Z03".to_string(),
2030 bo4e_value: Some("messlokationsadresse".to_string()),
2031 source_paths: vec!["sg4.sg12_z03".to_string()],
2032 fields: vec![
2033 field("adresse.ort", "X", &[]),
2034 field("partnerrolle", "X", &[z03]),
2035 ],
2036 ahb_status: String::new(),
2037 },
2038 EntityVariantRequirement {
2039 discriminator_field: "partnerrolle".to_string(),
2040 code: "Z07".to_string(),
2041 bo4e_value: Some("kundeMsb".to_string()),
2042 source_paths: vec!["sg4.sg12_z07".to_string()],
2043 fields: vec![field("name1", "X", &[]), field("partnerrolle", "X", &[z07])],
2044 ahb_status: String::new(),
2045 },
2046 ],
2047 groups: vec![],
2048 nested: Vec::new(),
2049 }],
2050 }
2051 }
2052
2053 fn multi_group_requirements(z22_status: &str) -> PidRequirements {
2055 let group = |path: &str, status: &str, fields: &[&str]| EntityGroupRequirement {
2056 source_path: path.to_string(),
2057 ahb_status: status.to_string(),
2058 fields: fields.iter().map(|f| f.to_string()).collect(),
2059 };
2060 PidRequirements {
2061 transaktion_wiederholbar: false,
2062 pid: "55043".to_string(),
2063 beschreibung: String::new(),
2064 entities: vec![EntityRequirement {
2065 entity: "Marktlokation".to_string(),
2066 ref_type: Bo4eRefType::Object {
2067 type_name: "Marktlokation".to_string(),
2068 cardinality: Cardinality::REQUIRED,
2069 },
2070 ahb_status: "Muss".to_string(),
2071 fields: vec![
2072 field("marktlokationsId", "X", &[]),
2073 field("ruhendeMarktlokationsId", "X", &[]),
2074 field("ruhendeMarktlokationZeitraumId", "Kann", &[]),
2075 ],
2076 map_key: None,
2077 scope: EntityScope::Transaction,
2078 variants: vec![],
2079 groups: vec![
2080 group("sg4.sg5_z16", "Muss", &["marktlokationsId"]),
2081 group(
2082 "sg4.sg5_z22",
2083 z22_status,
2084 &["ruhendeMarktlokationsId", "ruhendeMarktlokationZeitraumId"],
2085 ),
2086 ],
2087 nested: Vec::new(),
2088 }],
2089 }
2090 }
2091
2092 fn missing_fields(errors: &[PidValidationError]) -> Vec<&str> {
2093 errors
2094 .iter()
2095 .filter_map(|e| match e {
2096 PidValidationError::MissingField { field, .. } => Some(field.as_str()),
2097 _ => None,
2098 })
2099 .collect()
2100 }
2101
2102 #[test]
2103 fn an_absent_optional_groups_fields_are_not_demanded() {
2104 let reqs = multi_group_requirements("Soll [2003]");
2105 let errors = validate_pid_json(&json!({ "marktlokation": {} }), &reqs);
2106 assert_eq!(missing_fields(&errors), vec!["marktlokationsId"]);
2108 }
2109
2110 #[test]
2111 fn a_filled_groups_fields_are_demanded() {
2112 let reqs = multi_group_requirements("Soll [2003]");
2113 let json = json!({ "marktlokation": {
2114 "marktlokationsId": "51238696781",
2115 "ruhendeMarktlokationZeitraumId": "1"
2116 }});
2117 let errors = validate_pid_json(&json, &reqs);
2118 assert_eq!(missing_fields(&errors), vec!["ruhendeMarktlokationsId"]);
2119 }
2120
2121 #[test]
2122 fn an_absent_required_groups_fields_are_demanded() {
2123 let reqs = multi_group_requirements("Muss");
2124 let json = json!({ "marktlokation": { "marktlokationsId": "51238696781" } });
2125 let errors = validate_pid_json(&json, &reqs);
2126 assert_eq!(missing_fields(&errors), vec!["ruhendeMarktlokationsId"]);
2127 }
2128
2129 #[test]
2130 fn multi_variant_entity_uses_the_elements_own_variant() {
2131 let reqs = multi_variant_requirements();
2132 let json = json!({
2134 "geschaeftspartner": [
2135 { "partnerrolle": "Z03", "adresse": { "ort": "Berlin" } },
2136 { "partnerrolle": "kundeMsb", "name1": "Muster" },
2137 { "partnerrolle": { "code": "messlokationsadresse", "meaning": "x" },
2138 "adresse": { "ort": "Köln" } },
2139 { "partnerrolle": { "code": "Z07" }, "name1": "Beispiel" },
2140 ]
2141 });
2142 let errors = validate_pid_json(&json, &reqs);
2143 assert!(errors.is_empty(), "{}", ValidationReport(errors));
2144 }
2145
2146 #[test]
2147 fn multi_variant_entity_reports_variant_required_fields() {
2148 let reqs = multi_variant_requirements();
2149 let json = json!({ "geschaeftspartner": [{ "partnerrolle": "kundeMsb" }] });
2150 let errors = validate_pid_json(&json, &reqs);
2151 assert_eq!(errors.len(), 1, "{}", ValidationReport(errors));
2152 assert!(matches!(
2153 &errors[0],
2154 PidValidationError::MissingField { field, .. } if field == "name1"
2155 ));
2156 }
2157
2158 #[test]
2159 fn multi_variant_entity_unknown_qualifier_is_invalid_code_only() {
2160 let reqs = multi_variant_requirements();
2161 for bad in [json!("Z99"), json!("bogus"), json!({ "code": "Z99" })] {
2162 let json = json!({ "geschaeftspartner": [{ "partnerrolle": bad }] });
2163 let errors = validate_pid_json(&json, &reqs);
2164 assert_eq!(errors.len(), 1, "{bad}: {}", ValidationReport(errors));
2166 match &errors[0] {
2167 PidValidationError::InvalidCode {
2168 field,
2169 valid_values,
2170 ..
2171 } => {
2172 assert_eq!(field, "partnerrolle");
2173 let codes: Vec<&str> = valid_values.iter().map(|(c, _)| c.as_str()).collect();
2174 assert_eq!(codes, ["Z03", "Z07"]);
2175 }
2176 other => panic!("expected InvalidCode, got {other:?}"),
2177 }
2178 }
2179 }
2180
2181 #[test]
2182 fn code_objects_and_enum_mapped_names_are_code_checked() {
2183 let f = field("partnerrolle", "X", &[("Z07", "kundeMsb")]);
2184 assert_eq!(invalid_code_value(&json!("Z07"), &f), None);
2185 assert_eq!(invalid_code_value(&json!("kundeMsb"), &f), None);
2186 assert_eq!(invalid_code_value(&json!({ "code": "kundeMsb" }), &f), None);
2187 assert_eq!(invalid_code_value(&json!({ "code": "Z07" }), &f), None);
2188 assert_eq!(
2189 invalid_code_value(&json!({ "code": "Z99", "meaning": null }), &f),
2190 Some("Z99".to_string())
2191 );
2192 assert_eq!(
2193 invalid_code_value(&json!("kundeLf"), &f),
2194 Some("kundeLf".to_string())
2195 );
2196 assert_eq!(invalid_code_value(&json!(7), &f), None);
2198 assert_eq!(invalid_code_value(&json!({ "meaning": "x" }), &f), None);
2199 }
2200}