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