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 TooLong {
64 entity: String,
65 field: String,
66 length: usize,
67 format: String,
68 },
69}
70
71impl PidValidationError {
72 pub fn severity(&self) -> &Severity {
73 match self {
74 Self::MissingEntity { severity, .. } => severity,
75 Self::MissingField { severity, .. } => severity,
76 Self::NotAllowed { severity, .. } => severity,
77 Self::InvalidCode { .. } | Self::TooLong { .. } => &Severity::Error,
78 }
79 }
80
81 pub fn is_error(&self) -> bool {
82 matches!(self.severity(), Severity::Error)
83 }
84}
85
86impl fmt::Display for PidValidationError {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 match self {
89 PidValidationError::MissingEntity {
90 entity,
91 ahb_status,
92 severity,
93 } => {
94 let label = severity_label(severity);
95 write!(
96 f,
97 "{label}: missing entity '{entity}' (required: {ahb_status})"
98 )
99 }
100 PidValidationError::MissingField {
101 entity,
102 field,
103 ahb_status,
104 rust_type,
105 valid_values,
106 severity,
107 } => {
108 let label = severity_label(severity);
109 write!(
110 f,
111 "{label}: missing {entity}.{field} (required: {ahb_status})"
112 )?;
113 if let Some(rt) = rust_type {
114 write!(f, "\n → type: {rt}")?;
115 }
116 if !valid_values.is_empty() {
117 let codes: Vec<String> = valid_values
118 .iter()
119 .map(|(code, meaning)| {
120 if meaning.is_empty() {
121 code.clone()
122 } else {
123 format!("{code} ({meaning})")
124 }
125 })
126 .collect();
127 write!(f, "\n → valid: {}", codes.join(", "))?;
128 }
129 Ok(())
130 }
131 PidValidationError::TooLong {
132 entity,
133 field,
134 length,
135 format,
136 } => write!(
137 f,
138 "INVALID: {entity}.{field} has {length} characters\n → format: {format}"
139 ),
140 PidValidationError::InvalidCode {
141 entity,
142 field,
143 value,
144 valid_values,
145 } => {
146 write!(f, "INVALID: {entity}.{field} = \"{value}\"")?;
147 if !valid_values.is_empty() {
148 let codes: Vec<String> = valid_values.iter().map(|(c, _)| c.clone()).collect();
149 write!(f, "\n → valid: {}", codes.join(", "))?;
150 }
151 Ok(())
152 }
153 PidValidationError::NotAllowed {
154 entity,
155 field,
156 value,
157 ahb_status,
158 severity,
159 } => {
160 write!(f, "{}: {entity}", severity_label(severity))?;
161 if !field.is_empty() {
162 write!(f, ".{field}")?;
163 }
164 if let Some(value) = value {
165 write!(f, " = \"{value}\"")?;
166 }
167 write!(
168 f,
169 " present although its condition does not hold ({ahb_status})"
170 )
171 }
172 }
173 }
174}
175
176fn severity_label(severity: &Severity) -> &'static str {
177 match severity {
178 Severity::Error => "ERROR",
179 Severity::Warning => "WARNING",
180 }
181}
182
183pub struct ValidationReport(pub Vec<PidValidationError>);
185
186impl ValidationReport {
187 pub fn has_errors(&self) -> bool {
189 self.0.iter().any(|e| e.is_error())
190 }
191
192 pub fn errors(&self) -> Vec<&PidValidationError> {
194 self.0.iter().filter(|e| e.is_error()).collect()
195 }
196
197 pub fn is_empty(&self) -> bool {
199 self.0.is_empty()
200 }
201
202 pub fn len(&self) -> usize {
204 self.0.len()
205 }
206}
207
208impl fmt::Display for ValidationReport {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 for (i, err) in self.0.iter().enumerate() {
211 if i > 0 {
212 writeln!(f)?;
213 }
214 write!(f, "{err}")?;
215 }
216 Ok(())
217 }
218}
219
220pub fn validate_pid_json(json: &Value, requirements: &PidRequirements) -> Vec<PidValidationError> {
232 validate_entities(json, &requirements.entities, None)
233}
234
235pub fn validate_pid_json_transaction(
241 json: &Value,
242 requirements: &PidRequirements,
243) -> Vec<PidValidationError> {
244 validate_entities(json, &requirements.entities, Some(EntityScope::Transaction))
245}
246
247fn validate_entities(
249 json: &Value,
250 entities: &[EntityRequirement],
251 scope_filter: Option<EntityScope>,
252) -> Vec<PidValidationError> {
253 let mut errors = Vec::new();
254
255 for entity_req in entities {
256 if let Some(ref scope) = scope_filter {
258 if &entity_req.scope != scope {
259 continue;
260 }
261 }
262
263 let key = to_camel_case(&entity_req.entity);
264
265 match json.get(&key) {
266 None | Some(serde_json::Value::Null) => {
267 if is_unconditionally_required(&entity_req.ahb_status) {
268 errors.push(PidValidationError::MissingEntity {
269 entity: entity_req.entity.clone(),
270 ahb_status: entity_req.ahb_status.clone(),
271 severity: Severity::Error,
272 });
273 }
274 }
275 Some(val) => {
276 if let Some(arr) = val.as_array() {
279 for element in arr {
280 validate_entity_fields(element, entity_req, &mut errors);
281 }
282 } else {
283 validate_entity_fields(val, entity_req, &mut errors);
284 }
285 }
286 }
287 }
288
289 errors
290}
291
292pub fn get_nested<'a>(json: &'a Value, path: &str) -> Option<&'a Value> {
295 if let Some((list, rest)) = path.split_once("[].") {
298 return get_nested(json, list)?
299 .as_array()?
300 .iter()
301 .find_map(|item| get_nested(item, rest).filter(|v| !v.is_null()));
302 }
303 let mut current = json;
304 for part in path.split('.') {
305 current = current.get(part).or_else(|| {
306 if part.contains('_') {
307 current.get(snake_to_camel_case(part))
308 } else {
309 None
310 }
311 })?;
312 }
313 Some(current)
314}
315
316fn validate_entity_fields(
318 entity_json: &Value,
319 entity_req: &EntityRequirement,
320 errors: &mut Vec<PidValidationError>,
321) {
322 let fields = effective_field_requirements(entity_req, entity_json);
323 let exempt =
324 fields_of_unrequired_absent_groups(entity_req, entity_json, is_unconditionally_required);
325 for field_req in fields.iter() {
326 let val = get_nested(entity_json, &field_req.bo4e_name);
329
330 let val = val.filter(|v| !v.is_null());
332
333 match val {
334 None if exempt.contains(field_req.bo4e_name.as_str()) => {}
335 None => {
336 if is_unconditionally_required(&field_req.ahb_status) {
337 errors.push(PidValidationError::MissingField {
338 entity: entity_req.entity.clone(),
339 field: field_req.bo4e_name.clone(),
340 ahb_status: field_req.ahb_status.clone(),
341 rust_type: field_req.enum_name.clone(),
342 valid_values: code_values_to_tuples(&field_req.valid_codes),
343 severity: Severity::Error,
344 });
345 }
346 }
347 Some(val) => {
348 validate_code_value(val, entity_req, field_req, errors);
349 }
350 }
351 }
352 validate_nested_lists(
353 &entity_req.entity,
354 entity_json,
355 &entity_req.nested,
356 "",
357 &|status, _| is_unconditionally_required(status).then_some(Severity::Error),
358 &|_| None,
359 &|_, _| false,
361 errors,
362 );
363}
364
365pub fn validate_nested_lists(
375 entity: &str,
376 element: &Value,
377 lists: &[NestedListRequirement],
378 prefix: &str,
379 required: &dyn Fn(&str, &[(String, usize)]) -> Option<Severity>,
380 variant_holds: &dyn Fn(&VariantQualifier) -> Option<bool>,
381 forbidden: &dyn Fn(&str, &[(String, usize)]) -> bool,
382 errors: &mut Vec<PidValidationError>,
383) {
384 nested_lists_at(
385 entity,
386 element,
387 lists,
388 prefix,
389 &mut Vec::new(),
390 required,
391 variant_holds,
392 forbidden,
393 errors,
394 );
395}
396
397#[allow(clippy::too_many_arguments)]
399fn nested_lists_at(
400 entity: &str,
401 element: &Value,
402 lists: &[NestedListRequirement],
403 prefix: &str,
404 position: &mut Vec<(String, usize)>,
405 required: &dyn Fn(&str, &[(String, usize)]) -> Option<Severity>,
406 variant_holds: &dyn Fn(&VariantQualifier) -> Option<bool>,
407 forbidden: &dyn Fn(&str, &[(String, usize)]) -> bool,
408 errors: &mut Vec<PidValidationError>,
409) {
410 let applies = |kind: &NestedElementKind| {
413 kind.transaction_variant
414 .as_ref()
415 .map_or(Some(true), variant_holds)
416 };
417 let mut fields: Vec<&str> = lists.iter().map(|l| l.field.as_str()).collect();
420 fields.sort_unstable();
421 fields.dedup();
422 for field in fields {
423 let candidates: Vec<&NestedListRequirement> = lists
426 .iter()
427 .filter(|l| l.field == field)
428 .filter(|l| {
429 l.parent_selector
430 .iter()
431 .all(|selector| selector_matches(selector, element))
432 })
433 .collect();
434 if candidates.is_empty() {
435 continue;
436 }
437 let items: &[Value] = get_nested(element, field)
438 .and_then(|v| v.as_array())
439 .map_or(&[], |a| a.as_slice());
440 let label = format!("{prefix}{field}");
441 let alone = candidates.len() == 1;
442
443 let chosen = match &candidates[..] {
447 [only] => Some(*only),
448 _ if items.is_empty() => None,
449 _ => {
450 let fitting: Vec<&&NestedListRequirement> = candidates
451 .iter()
452 .filter(|l| items.iter().all(|item| kind_of(l, item, false).is_some()))
453 .collect();
454 match fitting[..] {
455 [one] => Some(*one),
456 _ => None,
457 }
458 }
459 };
460
461 if let Some(list) = chosen {
462 for kind in &list.kinds {
463 if applies(kind) != Some(true) {
464 continue;
465 }
466 let present = if alone && list.kinds.len() == 1 {
467 !items.is_empty()
468 } else {
469 items.iter().any(|item| kind_matches(kind, item))
470 };
471 if present {
472 continue;
473 }
474 if let Some(severity) = required(&kind.ahb_status, position) {
475 let codes = kind.selector.first().map_or(&[][..], |s| &s.codes[..]);
476 let name = if alone && list.kinds.len() == 1 {
479 String::new()
480 } else {
481 codes
482 .first()
483 .map(|c| c.bo4e_value.clone().unwrap_or_else(|| c.code.clone()))
484 .unwrap_or_default()
485 };
486 errors.push(PidValidationError::MissingField {
487 entity: entity.to_string(),
488 field: format!("{label}[{name}]"),
489 ahb_status: kind.ahb_status.clone(),
490 rust_type: None,
491 valid_values: code_values_to_tuples(codes),
492 severity,
493 });
494 }
495 }
496 }
497
498 for (index, item) in items.iter().enumerate() {
499 position.push((field.to_string(), index));
500 let here = position.clone();
501 let required_here = |status: &str| required(status, &here);
502 let matching: Vec<(&NestedListRequirement, &NestedElementKind)> = match chosen {
503 Some(list) => kind_of(list, item, alone)
504 .filter(|k| applies(k) != Some(false))
505 .map(|k| (list, k))
506 .into_iter()
507 .collect(),
508 None => candidates
509 .iter()
510 .flat_map(|l| {
511 l.kinds
512 .iter()
513 .filter(|k| applies(k) != Some(false))
514 .filter(|k| kind_matches(k, item))
515 .map(move |k| (*l, k))
516 })
517 .collect(),
518 };
519 let best = matching
523 .iter()
524 .map(|(l, k)| {
525 (
526 *l,
527 *k,
528 element_findings(entity, &label, k, item, &required_here),
529 )
530 })
531 .min_by_key(|(_, k, f)| (f.len(), k.selector.is_empty()));
532 if let Some((list, kind, findings)) = best {
533 errors.extend(findings);
534 let forbidden_here = |status: &str| forbidden(status, &here);
537 if forbidden_here(&kind.ahb_status) {
538 let name = kind
539 .selector
540 .first()
541 .and_then(|s| s.codes.first())
542 .map(|c| c.bo4e_value.clone().unwrap_or_else(|| c.code.clone()))
543 .unwrap_or_default();
544 errors.push(PidValidationError::NotAllowed {
545 entity: entity.to_string(),
546 field: format!("{label}[{name}]"),
547 value: None,
548 ahb_status: kind.ahb_status.clone(),
549 severity: Severity::Warning,
550 });
551 } else {
552 errors.extend(not_allowed_codes(
553 entity,
554 &format!("{label}[]."),
555 &kind.fields,
556 item,
557 &forbidden_here,
558 ));
559 }
560 nested_lists_at(
561 entity,
562 item,
563 &list.nested,
564 &format!("{label}[]."),
565 position,
566 required,
567 variant_holds,
568 forbidden,
569 errors,
570 );
571 }
572 position.pop();
573 }
574 }
575}
576
577fn kind_of<'l>(
581 list: &'l NestedListRequirement,
582 item: &Value,
583 alone: bool,
584) -> Option<&'l NestedElementKind> {
585 match &list.kinds[..] {
586 [only] if alone => Some(only),
587 kinds => kinds.iter().find(|k| kind_matches(k, item)),
588 }
589}
590
591fn kind_matches(kind: &NestedElementKind, item: &Value) -> bool {
594 kind.selector
595 .iter()
596 .all(|selector| selector_matches(selector, item))
597}
598
599fn selector_matches(selector: &KindSelector, item: &Value) -> bool {
601 get_nested(item, &selector.field)
602 .and_then(code_field_value)
603 .is_some_and(|value| {
604 selector
605 .codes
606 .iter()
607 .any(|c| c.code == value || c.bo4e_value.as_deref() == Some(value))
608 })
609}
610
611pub fn not_allowed_codes(
619 entity: &str,
620 label: &str,
621 fields: &[FieldRequirement],
622 element: &Value,
623 forbidden: &dyn Fn(&str) -> bool,
624) -> Vec<PidValidationError> {
625 let mut out = Vec::new();
626 for field_req in fields {
627 let values = all_nested(element, &field_req.bo4e_name);
628 if values.is_empty() {
629 continue;
630 }
631 let name = format!("{label}{}", field_req.bo4e_name);
632 for value in values {
633 let Some(code) = code_field_value(value) else {
634 continue;
635 };
636 let status = field_req
637 .valid_codes
638 .iter()
639 .find(|cv| cv.code == code || cv.bo4e_value.as_deref() == Some(code))
640 .and_then(|cv| cv.ahb_status.as_deref());
641 if let Some(status) = status.filter(|s| forbidden(s)) {
642 out.push(PidValidationError::NotAllowed {
643 entity: entity.to_string(),
644 field: name.clone(),
645 value: Some(code.to_string()),
646 ahb_status: status.to_string(),
647 severity: Severity::Warning,
648 });
649 }
650 }
651 }
652 out
653}
654
655fn element_findings(
658 entity: &str,
659 label: &str,
660 kind: &NestedElementKind,
661 item: &Value,
662 required: &dyn Fn(&str) -> Option<Severity>,
663) -> Vec<PidValidationError> {
664 let mut out = Vec::new();
665 for field_req in &kind.fields {
666 let name = format!("{label}[].{}", field_req.bo4e_name);
667 let values = all_nested(item, &field_req.bo4e_name);
668 if values.is_empty() {
669 if let Some(severity) = required(&field_req.ahb_status) {
670 out.push(PidValidationError::MissingField {
671 entity: entity.to_string(),
672 field: name,
673 ahb_status: field_req.ahb_status.clone(),
674 rust_type: field_req.enum_name.clone(),
675 valid_values: code_values_to_tuples(&field_req.valid_codes),
676 severity,
677 });
678 }
679 continue;
680 }
681 for value in values {
682 out.extend(too_long(entity, &name, value, field_req));
683 if let Some(value) = invalid_code_value(value, field_req) {
684 out.push(PidValidationError::InvalidCode {
685 entity: entity.to_string(),
686 field: name.clone(),
687 value,
688 valid_values: code_values_to_tuples(&field_req.valid_codes),
689 });
690 }
691 }
692 }
693 out
694}
695
696fn all_nested<'a>(json: &'a Value, path: &str) -> Vec<&'a Value> {
699 if let Some((list, rest)) = path.split_once("[].") {
700 return get_nested(json, list)
701 .and_then(|v| v.as_array())
702 .map(|items| {
703 items
704 .iter()
705 .flat_map(|item| all_nested(item, rest))
706 .collect()
707 })
708 .unwrap_or_default();
709 }
710 get_nested(json, path)
711 .filter(|v| !v.is_null())
712 .into_iter()
713 .collect()
714}
715
716pub fn too_long(
720 entity: &str,
721 field: &str,
722 val: &Value,
723 field_req: &FieldRequirement,
724) -> Option<PidValidationError> {
725 if !field_req.valid_codes.is_empty() {
728 return None;
729 }
730 let format = field_req.format.as_deref()?;
731 let text = val.as_str()?;
732 if field_req
736 .free_data_codes
737 .iter()
738 .any(|c| c.code == text || c.bo4e_value.as_deref() == Some(text))
739 {
740 return None;
741 }
742 let digits = format
743 .trim()
744 .strip_prefix("an")
745 .or_else(|| format.trim().strip_prefix('a'))
746 .or_else(|| format.trim().strip_prefix('n'))?;
747 let max: usize = digits.strip_prefix("..").unwrap_or(digits).parse().ok()?;
748 let length = if format.trim().starts_with('n') {
751 text.chars()
752 .filter(|c| !matches!(c, '.' | ',' | '-'))
753 .count()
754 } else {
755 text.chars().count()
756 };
757 (length > max).then(|| PidValidationError::TooLong {
758 entity: entity.to_string(),
759 field: field.to_string(),
760 length,
761 format: format.to_string(),
762 })
763}
764
765fn validate_code_value(
767 val: &Value,
768 entity_req: &EntityRequirement,
769 field_req: &FieldRequirement,
770 errors: &mut Vec<PidValidationError>,
771) {
772 if let Some(value) = invalid_code_value(val, field_req) {
773 errors.push(PidValidationError::InvalidCode {
774 entity: entity_req.entity.clone(),
775 field: field_req.bo4e_name.clone(),
776 value,
777 valid_values: code_values_to_tuples(&field_req.valid_codes),
778 });
779 }
780 errors.extend(too_long(
781 &entity_req.entity,
782 &field_req.bo4e_name,
783 val,
784 field_req,
785 ));
786}
787
788pub fn code_field_value(val: &Value) -> Option<&str> {
791 val.as_str()
792 .or_else(|| val.get("code").and_then(|c| c.as_str()))
793}
794
795pub fn invalid_code_value(val: &Value, field_req: &FieldRequirement) -> Option<String> {
802 if field_req.valid_codes.is_empty() {
803 return None;
804 }
805 let value = code_field_value(val)?;
806 let is_valid = field_req
807 .valid_codes
808 .iter()
809 .any(|cv| cv.code == value || cv.bo4e_value.as_deref() == Some(value));
810 (!is_valid).then(|| value.to_string())
811}
812
813pub fn variant_statuses<'a>(entity_req: &'a EntityRequirement, element: &Value) -> Vec<&'a str> {
817 let mut out: Vec<&'a str> = Vec::new();
818 for v in &entity_req.variants {
819 if v.ahb_status.is_empty() {
820 continue;
821 }
822 let value = get_nested(element, &v.discriminator_field).and_then(code_field_value);
823 if value.is_some_and(|s| v.code == s || v.bo4e_value.as_deref() == Some(s))
824 && !out.contains(&v.ahb_status.as_str())
825 {
826 out.push(v.ahb_status.as_str());
827 }
828 }
829 out
830}
831
832pub fn effective_field_requirements<'a>(
847 entity_req: &'a EntityRequirement,
848 element: &Value,
849) -> Cow<'a, [FieldRequirement]> {
850 if entity_req.variants.is_empty() {
851 return Cow::Borrowed(&entity_req.fields);
852 }
853
854 let mut by_field: BTreeMap<&str, Vec<&EntityVariantRequirement>> = BTreeMap::new();
856 for v in &entity_req.variants {
857 by_field
858 .entry(v.discriminator_field.as_str())
859 .or_default()
860 .push(v);
861 }
862 let mut candidates: Vec<&EntityVariantRequirement> = Vec::new();
863 for (field, group) in by_field {
864 let value = get_nested(element, field).and_then(code_field_value);
865 let matched: Vec<&EntityVariantRequirement> = group
866 .iter()
867 .copied()
868 .filter(|v| value.is_some_and(|s| v.code == s || v.bo4e_value.as_deref() == Some(s)))
869 .collect();
870 candidates.extend(if matched.is_empty() { group } else { matched });
871 }
872
873 let mut owned: Vec<&str> = Vec::new();
875 let mut owned_set: BTreeSet<&str> = BTreeSet::new();
876 for v in &entity_req.variants {
877 for f in &v.fields {
878 if owned_set.insert(f.bo4e_name.as_str()) {
879 owned.push(f.bo4e_name.as_str());
880 }
881 }
882 }
883
884 let mut combined: BTreeMap<&str, FieldRequirement> = BTreeMap::new();
885 for name in owned {
886 let reqs: Vec<&FieldRequirement> = candidates
887 .iter()
888 .filter_map(|v| v.fields.iter().find(|f| f.bo4e_name == name))
889 .collect();
890 let Some((first, rest)) = reqs.split_first() else {
891 continue; };
893 let mut field = (*first).clone();
894 let mut statuses_agree = reqs.len() == candidates.len();
895 for r in rest {
896 if r.ahb_status != field.ahb_status {
897 statuses_agree = false;
898 }
899 for cv in &r.valid_codes {
900 if !field.valid_codes.iter().any(|c| c.code == cv.code) {
901 field.valid_codes.push(cv.clone());
902 }
903 }
904 }
905 if !statuses_agree {
906 field.ahb_status = String::new();
907 }
908 combined.insert(name, field);
909 }
910
911 let mut result: Vec<FieldRequirement> = Vec::with_capacity(entity_req.fields.len());
912 for f in &entity_req.fields {
913 if owned_set.contains(f.bo4e_name.as_str()) {
914 if let Some(c) = combined.remove(f.bo4e_name.as_str()) {
915 result.push(c);
916 }
917 } else {
918 result.push(f.clone());
919 }
920 }
921 result.extend(combined.into_values());
922 Cow::Owned(result)
923}
924
925pub fn absent_groups<'a>(
928 entity_req: &'a EntityRequirement,
929 element: &'a Value,
930) -> impl Iterator<Item = &'a EntityGroupRequirement> + 'a {
931 entity_req.groups.iter().filter(move |g| {
932 !g.fields
933 .iter()
934 .any(|f| get_nested(element, f).is_some_and(|v| !v.is_null()))
935 })
936}
937
938pub fn fields_of_unrequired_absent_groups<'a>(
947 entity_req: &'a EntityRequirement,
948 element: &'a Value,
949 mut group_required: impl FnMut(&str) -> bool,
950) -> BTreeSet<&'a str> {
951 let exempt: Vec<&EntityGroupRequirement> = absent_groups(entity_req, element)
952 .filter(|g| !group_required(&g.ahb_status))
953 .collect();
954 let mut fields = BTreeSet::new();
955 for g in &exempt {
956 for f in &g.fields {
957 let kept_elsewhere = entity_req.groups.iter().any(|other| {
960 other.fields.contains(f) && !exempt.iter().any(|e| std::ptr::eq(*e, other))
961 });
962 if !kept_elsewhere {
963 fields.insert(f.as_str());
964 }
965 }
966 }
967 fields
968}
969
970fn code_values_to_tuples(codes: &[CodeValue]) -> Vec<(String, String)> {
972 codes
973 .iter()
974 .map(|cv| (cv.code.clone(), cv.meaning.clone()))
975 .collect()
976}
977
978fn to_camel_case(s: &str) -> String {
984 if s.is_empty() {
985 return String::new();
986 }
987 let mut chars = s.chars();
988 let first = chars.next().unwrap();
989 let mut result = first.to_lowercase().to_string();
990 result.extend(chars);
991 result
992}
993
994fn snake_to_camel_case(s: &str) -> String {
1005 let mut result = String::with_capacity(s.len());
1006 let mut capitalize_next = false;
1007 for ch in s.chars() {
1008 if ch == '_' {
1009 capitalize_next = true;
1010 } else if capitalize_next {
1011 result.extend(ch.to_uppercase());
1012 capitalize_next = false;
1013 } else {
1014 result.push(ch);
1015 }
1016 }
1017 result
1018}
1019
1020fn is_unconditionally_required(ahb_status: &str) -> bool {
1022 matches!(ahb_status, "X" | "Muss" | "Soll")
1023}
1024
1025#[cfg(test)]
1026mod tests {
1027
1028 #[test]
1033 fn too_long_skips_codes_of_a_free_data_field() {
1034 let field = FieldRequirement {
1035 bo4e_name: "merkmal.code".to_string(),
1036 ahb_status: "X".to_string(),
1037 field_type: "data".to_string(),
1038 format: Some("an..3".to_string()),
1039 enum_name: None,
1040 free_data_codes: vec![CodeValue {
1041 code: "ZA9".to_string(),
1042 meaning: "Übertragungsnetzbetreiber".to_string(),
1043 enum_name: None,
1044 bo4e_value: Some("uebertragungsnetzbetreiber".to_string()),
1045 ahb_status: None,
1046 }],
1047 valid_codes: vec![],
1048 ref_type: crate::pid_requirements::Bo4eRefType::Unknown,
1049 };
1050 let check = |v: &str| too_long("E", "zuordnungen[].merkmal.code", &json!(v), &field);
1051 assert!(check("uebertragungsnetzbetreiber").is_none());
1052 assert!(check("ZA9").is_none());
1053 assert!(check("Z1").is_none());
1054 assert!(matches!(
1055 check("ZDNBNAPG56ZM"),
1056 Some(PidValidationError::TooLong { length: 12, .. })
1057 ));
1058 }
1059 use super::*;
1060
1061 #[test]
1065 fn a_variant_bound_kind_is_demanded_only_in_its_variant() {
1066 use crate::pid_requirements::{NestedElementKind, NestedListRequirement, VariantQualifier};
1067 let lists = vec![NestedListRequirement {
1068 field: "zuordnungen".to_string(),
1069 bo4e_type: "Zuordnung".to_string(),
1070 source_path: "sg4.sg8_z22.sg10".to_string(),
1071 kinds: vec![NestedElementKind {
1072 ahb_status: "Muss".to_string(),
1073 selector: vec![],
1074 fields: vec![],
1075 transaction_variant: Some(VariantQualifier {
1076 group: "SG4".to_string(),
1077 segment: "IDE".to_string(),
1078 codes: vec!["24".to_string()],
1079 }),
1080 }],
1081 parent_selector: vec![],
1082 nested: vec![],
1083 }];
1084 let demanded = |holds: Option<bool>| {
1085 let mut errors = Vec::new();
1086 validate_nested_lists(
1087 "SummenzeitreihenDaten",
1088 &serde_json::json!({}),
1089 &lists,
1090 "",
1091 &|_, _| Some(Severity::Error),
1092 &|_| holds,
1093 &|_, _| false,
1094 &mut errors,
1095 );
1096 errors.len()
1097 };
1098 assert_eq!(demanded(Some(true)), 1);
1099 assert_eq!(demanded(Some(false)), 0);
1100 assert_eq!(demanded(None), 0);
1101 }
1102 use crate::pid_requirements::{
1103 Bo4eRefType, Cardinality, CodeValue, EntityRequirement, FieldRequirement, PidRequirements,
1104 };
1105 use serde_json::json;
1106
1107 fn sample_requirements() -> PidRequirements {
1108 PidRequirements {
1109 transaktion_wiederholbar: false,
1110 pid: "55001".to_string(),
1111 beschreibung: "Anmeldung verb. MaLo".to_string(),
1112 entities: vec![
1113 EntityRequirement {
1114 entity: "Prozessdaten".to_string(),
1115 ref_type: Bo4eRefType::Object {
1116 type_name: "Prozessdaten".to_string(),
1117
1118 cardinality: Cardinality::REQUIRED,
1119 },
1120
1121 ahb_status: "Muss".to_string(),
1122 map_key: None,
1123 scope: EntityScope::Transaction,
1124 variants: vec![],
1125 groups: vec![],
1126 fields: vec![
1127 FieldRequirement {
1128 bo4e_name: "vorgangId".to_string(),
1129 ahb_status: "X".to_string(),
1130 field_type: "data".to_string(),
1131 format: None,
1132 enum_name: None,
1133 free_data_codes: Vec::new(),
1134 valid_codes: vec![],
1135 ref_type: Bo4eRefType::Unknown,
1136 },
1137 FieldRequirement {
1138 bo4e_name: "transaktionsgrund".to_string(),
1139 ahb_status: "X".to_string(),
1140 field_type: "code".to_string(),
1141 format: None,
1142 enum_name: Some("Transaktionsgrund".to_string()),
1143 free_data_codes: Vec::new(),
1144 valid_codes: vec![
1145 CodeValue {
1146 code: "E01".to_string(),
1147 meaning: "Ein-/Auszug (Einzug)".to_string(),
1148 enum_name: None,
1149 bo4e_value: None,
1150 ahb_status: None,
1151 },
1152 CodeValue {
1153 code: "E03".to_string(),
1154 meaning: "Wechsel".to_string(),
1155 enum_name: None,
1156 bo4e_value: None,
1157 ahb_status: None,
1158 },
1159 ],
1160 ref_type: Bo4eRefType::Unknown,
1161 },
1162 ],
1163 nested: Vec::new(),
1164 },
1165 EntityRequirement {
1166 entity: "Marktlokation".to_string(),
1167 ref_type: Bo4eRefType::Object {
1168 type_name: "Marktlokation".to_string(),
1169
1170 cardinality: Cardinality::REQUIRED,
1171 },
1172
1173 ahb_status: "Muss".to_string(),
1174 map_key: None,
1175 scope: EntityScope::Transaction,
1176 variants: vec![],
1177 groups: vec![],
1178 fields: vec![
1179 FieldRequirement {
1180 bo4e_name: "marktlokationsId".to_string(),
1181 ahb_status: "X".to_string(),
1182 field_type: "data".to_string(),
1183 format: None,
1184 enum_name: None,
1185 free_data_codes: Vec::new(),
1186 valid_codes: vec![],
1187 ref_type: Bo4eRefType::Unknown,
1188 },
1189 FieldRequirement {
1190 bo4e_name: "haushaltskunde".to_string(),
1191 ahb_status: "X".to_string(),
1192 field_type: "code".to_string(),
1193 format: None,
1194 enum_name: Some("Haushaltskunde".to_string()),
1195 free_data_codes: Vec::new(),
1196 valid_codes: vec![
1197 CodeValue {
1198 code: "Z15".to_string(),
1199 meaning: "Ja".to_string(),
1200 enum_name: None,
1201 bo4e_value: None,
1202 ahb_status: None,
1203 },
1204 CodeValue {
1205 code: "Z18".to_string(),
1206 meaning: "Nein".to_string(),
1207 enum_name: None,
1208 bo4e_value: None,
1209 ahb_status: None,
1210 },
1211 ],
1212 ref_type: Bo4eRefType::Unknown,
1213 },
1214 ],
1215 nested: Vec::new(),
1216 },
1217 EntityRequirement {
1218 entity: "Geschaeftspartner".to_string(),
1219 ref_type: Bo4eRefType::Object {
1220 type_name: "Geschaeftspartner".to_string(),
1221
1222 cardinality: Cardinality {
1223 min: 1,
1224 max: Some(7),
1225 },
1226 },
1227
1228 ahb_status: "Muss".to_string(),
1229 map_key: None,
1230 scope: EntityScope::Transaction,
1231 variants: vec![],
1232 groups: vec![],
1233 fields: vec![FieldRequirement {
1234 bo4e_name: "identifikation".to_string(),
1235 ahb_status: "X".to_string(),
1236 field_type: "data".to_string(),
1237 format: None,
1238 enum_name: None,
1239 free_data_codes: Vec::new(),
1240 valid_codes: vec![],
1241 ref_type: Bo4eRefType::Unknown,
1242 }],
1243 nested: Vec::new(),
1244 },
1245 ],
1246 }
1247 }
1248
1249 #[test]
1250 fn test_validate_complete_json() {
1251 let reqs = sample_requirements();
1252 let json = json!({
1253 "prozessdaten": {
1254 "vorgangId": "ABC123",
1255 "transaktionsgrund": "E01"
1256 },
1257 "marktlokation": {
1258 "marktlokationsId": "51234567890",
1259 "haushaltskunde": "Z15"
1260 },
1261 "geschaeftspartner": [
1262 { "identifikation": "9900000000003" }
1263 ]
1264 });
1265
1266 let errors = validate_pid_json(&json, &reqs);
1267 assert!(errors.is_empty(), "Expected no errors, got: {errors:?}");
1268 }
1269
1270 #[test]
1271 fn test_validate_missing_entity() {
1272 let reqs = sample_requirements();
1273 let json = json!({
1274 "prozessdaten": {
1275 "vorgangId": "ABC123",
1276 "transaktionsgrund": "E01"
1277 },
1278 "geschaeftspartner": [
1279 { "identifikation": "9900000000003" }
1280 ]
1281 });
1282 let errors = validate_pid_json(&json, &reqs);
1285 assert_eq!(errors.len(), 1);
1286 match &errors[0] {
1287 PidValidationError::MissingEntity {
1288 entity,
1289 ahb_status,
1290 severity,
1291 } => {
1292 assert_eq!(entity, "Marktlokation");
1293 assert_eq!(ahb_status, "Muss");
1294 assert_eq!(severity, &Severity::Error);
1295 }
1296 other => panic!("Expected MissingEntity, got: {other:?}"),
1297 }
1298
1299 let msg = errors[0].to_string();
1301 assert!(msg.contains("ERROR"));
1302 assert!(msg.contains("Marktlokation"));
1303 assert!(msg.contains("Muss"));
1304 }
1305
1306 #[test]
1307 fn test_validate_missing_field() {
1308 let reqs = sample_requirements();
1309 let json = json!({
1310 "prozessdaten": {
1311 "transaktionsgrund": "E01"
1312 },
1314 "marktlokation": {
1315 "marktlokationsId": "51234567890",
1316 "haushaltskunde": "Z15"
1317 },
1318 "geschaeftspartner": [
1319 { "identifikation": "9900000000003" }
1320 ]
1321 });
1322
1323 let errors = validate_pid_json(&json, &reqs);
1324 assert_eq!(errors.len(), 1);
1325 match &errors[0] {
1326 PidValidationError::MissingField {
1327 entity,
1328 field,
1329 ahb_status,
1330 severity,
1331 ..
1332 } => {
1333 assert_eq!(entity, "Prozessdaten");
1334 assert_eq!(field, "vorgangId");
1335 assert_eq!(ahb_status, "X");
1336 assert_eq!(severity, &Severity::Error);
1337 }
1338 other => panic!("Expected MissingField, got: {other:?}"),
1339 }
1340
1341 let msg = errors[0].to_string();
1342 assert!(msg.contains("ERROR"));
1343 assert!(msg.contains("Prozessdaten.vorgangId"));
1344 }
1345
1346 #[test]
1347 fn test_validate_invalid_code() {
1348 let reqs = sample_requirements();
1349 let json = json!({
1350 "prozessdaten": {
1351 "vorgangId": "ABC123",
1352 "transaktionsgrund": "E01"
1353 },
1354 "marktlokation": {
1355 "marktlokationsId": "51234567890",
1356 "haushaltskunde": "Z99" },
1358 "geschaeftspartner": [
1359 { "identifikation": "9900000000003" }
1360 ]
1361 });
1362
1363 let errors = validate_pid_json(&json, &reqs);
1364 assert_eq!(errors.len(), 1);
1365 match &errors[0] {
1366 PidValidationError::InvalidCode {
1367 entity,
1368 field,
1369 value,
1370 valid_values,
1371 } => {
1372 assert_eq!(entity, "Marktlokation");
1373 assert_eq!(field, "haushaltskunde");
1374 assert_eq!(value, "Z99");
1375 assert_eq!(valid_values.len(), 2);
1376 assert!(valid_values.iter().any(|(c, _)| c == "Z15"));
1377 assert!(valid_values.iter().any(|(c, _)| c == "Z18"));
1378 }
1379 other => panic!("Expected InvalidCode, got: {other:?}"),
1380 }
1381
1382 let msg = errors[0].to_string();
1383 assert!(msg.contains("INVALID"));
1384 assert!(msg.contains("Z99"));
1385 assert!(msg.contains("Z15"));
1386 }
1387
1388 #[test]
1389 fn test_validate_array_entity() {
1390 let reqs = sample_requirements();
1391 let json = json!({
1392 "prozessdaten": {
1393 "vorgangId": "ABC123",
1394 "transaktionsgrund": "E01"
1395 },
1396 "marktlokation": {
1397 "marktlokationsId": "51234567890",
1398 "haushaltskunde": "Z15"
1399 },
1400 "geschaeftspartner": [
1401 { "identifikation": "9900000000003" },
1402 { } ]
1404 });
1405
1406 let errors = validate_pid_json(&json, &reqs);
1407 assert_eq!(errors.len(), 1);
1408 match &errors[0] {
1409 PidValidationError::MissingField { entity, field, .. } => {
1410 assert_eq!(entity, "Geschaeftspartner");
1411 assert_eq!(field, "identifikation");
1412 }
1413 other => panic!("Expected MissingField, got: {other:?}"),
1414 }
1415 }
1416
1417 #[test]
1418 fn test_to_camel_case() {
1419 assert_eq!(to_camel_case("Prozessdaten"), "prozessdaten");
1420 assert_eq!(
1421 to_camel_case("RuhendeMarktlokation"),
1422 "ruhendeMarktlokation"
1423 );
1424 assert_eq!(to_camel_case("Marktlokation"), "marktlokation");
1425 assert_eq!(to_camel_case(""), "");
1426 }
1427
1428 #[test]
1429 fn test_snake_to_camel_case() {
1430 assert_eq!(snake_to_camel_case("code_codepflege"), "codeCodepflege");
1431 assert_eq!(snake_to_camel_case("vorgang_id"), "vorgangId");
1432 assert_eq!(snake_to_camel_case("marktlokation"), "marktlokation");
1433 assert_eq!(snake_to_camel_case(""), "");
1434 assert_eq!(snake_to_camel_case("a_b_c"), "aBC");
1435 }
1436
1437 #[test]
1440 fn test_camel_case_fallback_for_snake_case_bo4e_name() {
1441 let reqs = PidRequirements {
1442 transaktion_wiederholbar: false,
1443 pid: "55077".to_string(),
1444 beschreibung: "Test camelCase fallback".to_string(),
1445 entities: vec![EntityRequirement {
1446 entity: "Zuordnung".to_string(),
1447 ref_type: Bo4eRefType::Object {
1448 type_name: "Zuordnung".to_string(),
1449
1450 cardinality: Cardinality::REQUIRED,
1451 },
1452
1453 ahb_status: "Muss".to_string(),
1454 map_key: None,
1455 scope: EntityScope::Transaction,
1456 variants: vec![],
1457 groups: vec![],
1458 fields: vec![
1459 FieldRequirement {
1460 bo4e_name: "code_codepflege".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 valid_codes: vec![],
1468 ref_type: Bo4eRefType::Unknown,
1469 },
1470 FieldRequirement {
1471 bo4e_name: "codeliste".to_string(),
1472 ahb_status: "X".to_string(),
1473 field_type: "data".to_string(),
1474 format: None,
1475 enum_name: None,
1476 free_data_codes: Vec::new(),
1477 valid_codes: vec![],
1478 ref_type: Bo4eRefType::Unknown,
1479 },
1480 ],
1481 nested: Vec::new(),
1482 }],
1483 };
1484
1485 let json_camel = json!({
1488 "zuordnung": {
1489 "codeCodepflege": "DE_BDEW",
1490 "codeliste": "6"
1491 }
1492 });
1493
1494 let errors = validate_pid_json(&json_camel, &reqs);
1495 assert!(
1496 errors.is_empty(),
1497 "Expected no errors when field is present under camelCase key, got: {errors:?}"
1498 );
1499
1500 let json_snake = json!({
1502 "zuordnung": {
1503 "code_codepflege": "DE_BDEW",
1504 "codeliste": "6"
1505 }
1506 });
1507
1508 let errors = validate_pid_json(&json_snake, &reqs);
1509 assert!(
1510 errors.is_empty(),
1511 "Expected no errors when field is present under snake_case key, got: {errors:?}"
1512 );
1513
1514 let json_missing = json!({
1516 "zuordnung": {
1517 "codeliste": "6"
1518 }
1519 });
1520
1521 let errors = validate_pid_json(&json_missing, &reqs);
1522 assert_eq!(errors.len(), 1);
1523 match &errors[0] {
1524 PidValidationError::MissingField { field, .. } => {
1525 assert_eq!(field, "code_codepflege");
1526 }
1527 other => panic!("Expected MissingField, got: {other:?}"),
1528 }
1529 }
1530
1531 #[test]
1532 fn test_is_unconditionally_required() {
1533 assert!(is_unconditionally_required("X"));
1534 assert!(is_unconditionally_required("Muss"));
1535 assert!(is_unconditionally_required("Soll"));
1536 assert!(!is_unconditionally_required("Kann"));
1537 assert!(!is_unconditionally_required("[1]"));
1538 assert!(!is_unconditionally_required(""));
1539 }
1540
1541 #[test]
1542 fn test_validation_report_display() {
1543 let errors = vec![
1544 PidValidationError::MissingEntity {
1545 entity: "Marktlokation".to_string(),
1546 ahb_status: "Muss".to_string(),
1547 severity: Severity::Error,
1548 },
1549 PidValidationError::MissingField {
1550 entity: "Prozessdaten".to_string(),
1551 field: "vorgangId".to_string(),
1552 ahb_status: "X".to_string(),
1553 rust_type: None,
1554 valid_values: vec![],
1555 severity: Severity::Error,
1556 },
1557 ];
1558 let report = ValidationReport(errors);
1559 assert!(report.has_errors());
1560 assert_eq!(report.len(), 2);
1561 assert!(!report.is_empty());
1562
1563 let display = report.to_string();
1564 assert!(display.contains("missing entity 'Marktlokation'"));
1565 assert!(display.contains("missing Prozessdaten.vorgangId"));
1566 }
1567
1568 #[test]
1569 fn test_missing_field_with_type_and_values_display() {
1570 let err = PidValidationError::MissingField {
1571 entity: "Marktlokation".to_string(),
1572 field: "haushaltskunde".to_string(),
1573 ahb_status: "Muss".to_string(),
1574 rust_type: Some("Haushaltskunde".to_string()),
1575 valid_values: vec![
1576 ("Z15".to_string(), "Ja".to_string()),
1577 ("Z18".to_string(), "Nein".to_string()),
1578 ],
1579 severity: Severity::Error,
1580 };
1581 let msg = err.to_string();
1582 assert!(msg.contains("type: Haushaltskunde"));
1583 assert!(msg.contains("valid: Z15 (Ja), Z18 (Nein)"));
1584 }
1585
1586 #[test]
1587 fn test_optional_fields_not_flagged() {
1588 let reqs = PidRequirements {
1589 transaktion_wiederholbar: false,
1590 pid: "99999".to_string(),
1591 beschreibung: "Test".to_string(),
1592 entities: vec![EntityRequirement {
1593 entity: "Test".to_string(),
1594 ref_type: Bo4eRefType::Object {
1595 type_name: "Test".to_string(),
1596
1597 cardinality: Cardinality::OPTIONAL,
1598 },
1599
1600 ahb_status: "Kann".to_string(),
1601 map_key: None,
1602 scope: EntityScope::Transaction,
1603 variants: vec![],
1604 groups: vec![],
1605 fields: vec![FieldRequirement {
1606 bo4e_name: "optionalField".to_string(),
1607 ahb_status: "Kann".to_string(),
1608 field_type: "data".to_string(),
1609 format: None,
1610 enum_name: None,
1611 free_data_codes: Vec::new(),
1612 valid_codes: vec![],
1613 ref_type: Bo4eRefType::Unknown,
1614 }],
1615 nested: Vec::new(),
1616 }],
1617 };
1618
1619 let errors = validate_pid_json(&json!({}), &reqs);
1621 assert!(errors.is_empty());
1622
1623 let errors = validate_pid_json(&json!({ "test": {} }), &reqs);
1625 assert!(errors.is_empty());
1626 }
1627
1628 #[test]
1631 fn test_nested_dot_path_fields_not_falsely_missing() {
1632 let reqs = PidRequirements {
1633 transaktion_wiederholbar: false,
1634 pid: "55001".to_string(),
1635 beschreibung: "Test nested paths".to_string(),
1636 entities: vec![EntityRequirement {
1637 entity: "ProduktpaketDaten".to_string(),
1638 ref_type: Bo4eRefType::Object {
1639 type_name: "ProduktpaketDaten".to_string(),
1640
1641 cardinality: Cardinality {
1642 min: 1,
1643 max: Some(99999),
1644 },
1645 },
1646
1647 ahb_status: "Muss".to_string(),
1648 map_key: None,
1649 scope: EntityScope::Transaction,
1650 variants: vec![],
1651 groups: vec![],
1652 fields: vec![
1653 FieldRequirement {
1654 bo4e_name: "produktIdentifikation.funktion".to_string(),
1655 ahb_status: "X".to_string(),
1656 field_type: "code".to_string(),
1657 format: None,
1658 enum_name: Some("Produktidentifikation".to_string()),
1659 free_data_codes: Vec::new(),
1660 valid_codes: vec![CodeValue {
1661 code: "5".to_string(),
1662 meaning: "Produktidentifikation".to_string(),
1663 enum_name: None,
1664 bo4e_value: None,
1665 ahb_status: None,
1666 }],
1667 ref_type: Bo4eRefType::Unknown,
1668 },
1669 FieldRequirement {
1670 bo4e_name: "produktMerkmal.code".to_string(),
1671 ahb_status: "X".to_string(),
1672 field_type: "code".to_string(),
1673 format: None,
1674 enum_name: None,
1675 free_data_codes: Vec::new(),
1676 valid_codes: vec![],
1677 ref_type: Bo4eRefType::Unknown,
1678 },
1679 ],
1680 nested: Vec::new(),
1681 }],
1682 };
1683
1684 let json = json!({
1686 "produktpaketDaten": [{
1687 "produktIdentifikation": { "funktion": "5", "id": "9991000002082", "typ": "Z11" },
1688 "produktMerkmal": { "code": "ZH9" }
1689 }]
1690 });
1691
1692 let errors = validate_pid_json(&json, &reqs);
1693 assert!(
1694 errors.is_empty(),
1695 "Nested dot-path fields should be found (issue #48), got: {errors:?}"
1696 );
1697 }
1698
1699 #[test]
1700 fn test_nested_dot_path_truly_missing() {
1701 let reqs = PidRequirements {
1702 transaktion_wiederholbar: false,
1703 pid: "55001".to_string(),
1704 beschreibung: "Test nested paths missing".to_string(),
1705 entities: vec![EntityRequirement {
1706 entity: "ProduktpaketDaten".to_string(),
1707 ref_type: Bo4eRefType::Object {
1708 type_name: "ProduktpaketDaten".to_string(),
1709
1710 cardinality: Cardinality {
1711 min: 1,
1712 max: Some(99999),
1713 },
1714 },
1715
1716 ahb_status: "Muss".to_string(),
1717 map_key: None,
1718 scope: EntityScope::Transaction,
1719 variants: vec![],
1720 groups: vec![],
1721 fields: vec![FieldRequirement {
1722 bo4e_name: "produktIdentifikation.funktion".to_string(),
1723 ahb_status: "X".to_string(),
1724 field_type: "data".to_string(),
1725 format: None,
1726 enum_name: None,
1727 free_data_codes: Vec::new(),
1728 valid_codes: vec![],
1729 ref_type: Bo4eRefType::Unknown,
1730 }],
1731 nested: Vec::new(),
1732 }],
1733 };
1734
1735 let json = json!({
1737 "produktpaketDaten": [{
1738 "produktIdentifikation": { "id": "123" }
1739 }]
1740 });
1741
1742 let errors = validate_pid_json(&json, &reqs);
1743 assert_eq!(errors.len(), 1, "Should report missing nested field");
1744 match &errors[0] {
1745 PidValidationError::MissingField { field, .. } => {
1746 assert_eq!(field, "produktIdentifikation.funktion");
1747 }
1748 other => panic!("Expected MissingField, got: {other:?}"),
1749 }
1750 }
1751
1752 fn field(name: &str, status: &str, codes: &[(&str, &str)]) -> FieldRequirement {
1753 FieldRequirement {
1754 bo4e_name: name.to_string(),
1755 ahb_status: status.to_string(),
1756 field_type: if codes.is_empty() { "data" } else { "code" }.to_string(),
1757 format: None,
1758 enum_name: None,
1759 free_data_codes: Vec::new(),
1760 valid_codes: codes
1761 .iter()
1762 .map(|(code, mapped)| CodeValue {
1763 code: code.to_string(),
1764 meaning: String::new(),
1765 enum_name: None,
1766 bo4e_value: Some(mapped.to_string()),
1767 ahb_status: None,
1768 })
1769 .collect(),
1770 ref_type: Bo4eRefType::Unknown,
1771 }
1772 }
1773
1774 fn multi_variant_requirements() -> PidRequirements {
1777 let z03 = ("Z03", "messlokationsadresse");
1778 let z07 = ("Z07", "kundeMsb");
1779 PidRequirements {
1780 transaktion_wiederholbar: false,
1781 pid: "55042".to_string(),
1782 beschreibung: String::new(),
1783 entities: vec![EntityRequirement {
1784 entity: "Geschaeftspartner".to_string(),
1785 ref_type: Bo4eRefType::Object {
1786 type_name: "Geschaeftspartner".to_string(),
1787 cardinality: Cardinality {
1788 min: 1,
1789 max: Some(99),
1790 },
1791 },
1792 ahb_status: "Muss".to_string(),
1793 fields: vec![
1795 field("adresse.ort", "X", &[]),
1796 field("name1", "X", &[]),
1797 field("partnerrolle", "X", &[z03, z07]),
1798 ],
1799 map_key: None,
1800 scope: EntityScope::Transaction,
1801 variants: vec![
1802 EntityVariantRequirement {
1803 discriminator_field: "partnerrolle".to_string(),
1804 code: "Z03".to_string(),
1805 bo4e_value: Some("messlokationsadresse".to_string()),
1806 source_paths: vec!["sg4.sg12_z03".to_string()],
1807 fields: vec![
1808 field("adresse.ort", "X", &[]),
1809 field("partnerrolle", "X", &[z03]),
1810 ],
1811 ahb_status: String::new(),
1812 },
1813 EntityVariantRequirement {
1814 discriminator_field: "partnerrolle".to_string(),
1815 code: "Z07".to_string(),
1816 bo4e_value: Some("kundeMsb".to_string()),
1817 source_paths: vec!["sg4.sg12_z07".to_string()],
1818 fields: vec![field("name1", "X", &[]), field("partnerrolle", "X", &[z07])],
1819 ahb_status: String::new(),
1820 },
1821 ],
1822 groups: vec![],
1823 nested: Vec::new(),
1824 }],
1825 }
1826 }
1827
1828 fn multi_group_requirements(z22_status: &str) -> PidRequirements {
1830 let group = |path: &str, status: &str, fields: &[&str]| EntityGroupRequirement {
1831 source_path: path.to_string(),
1832 ahb_status: status.to_string(),
1833 fields: fields.iter().map(|f| f.to_string()).collect(),
1834 };
1835 PidRequirements {
1836 transaktion_wiederholbar: false,
1837 pid: "55043".to_string(),
1838 beschreibung: String::new(),
1839 entities: vec![EntityRequirement {
1840 entity: "Marktlokation".to_string(),
1841 ref_type: Bo4eRefType::Object {
1842 type_name: "Marktlokation".to_string(),
1843 cardinality: Cardinality::REQUIRED,
1844 },
1845 ahb_status: "Muss".to_string(),
1846 fields: vec![
1847 field("marktlokationsId", "X", &[]),
1848 field("ruhendeMarktlokationsId", "X", &[]),
1849 field("ruhendeMarktlokationZeitraumId", "Kann", &[]),
1850 ],
1851 map_key: None,
1852 scope: EntityScope::Transaction,
1853 variants: vec![],
1854 groups: vec![
1855 group("sg4.sg5_z16", "Muss", &["marktlokationsId"]),
1856 group(
1857 "sg4.sg5_z22",
1858 z22_status,
1859 &["ruhendeMarktlokationsId", "ruhendeMarktlokationZeitraumId"],
1860 ),
1861 ],
1862 nested: Vec::new(),
1863 }],
1864 }
1865 }
1866
1867 fn missing_fields(errors: &[PidValidationError]) -> Vec<&str> {
1868 errors
1869 .iter()
1870 .filter_map(|e| match e {
1871 PidValidationError::MissingField { field, .. } => Some(field.as_str()),
1872 _ => None,
1873 })
1874 .collect()
1875 }
1876
1877 #[test]
1878 fn an_absent_optional_groups_fields_are_not_demanded() {
1879 let reqs = multi_group_requirements("Soll [2003]");
1880 let errors = validate_pid_json(&json!({ "marktlokation": {} }), &reqs);
1881 assert_eq!(missing_fields(&errors), vec!["marktlokationsId"]);
1883 }
1884
1885 #[test]
1886 fn a_filled_groups_fields_are_demanded() {
1887 let reqs = multi_group_requirements("Soll [2003]");
1888 let json = json!({ "marktlokation": {
1889 "marktlokationsId": "51238696781",
1890 "ruhendeMarktlokationZeitraumId": "1"
1891 }});
1892 let errors = validate_pid_json(&json, &reqs);
1893 assert_eq!(missing_fields(&errors), vec!["ruhendeMarktlokationsId"]);
1894 }
1895
1896 #[test]
1897 fn an_absent_required_groups_fields_are_demanded() {
1898 let reqs = multi_group_requirements("Muss");
1899 let json = json!({ "marktlokation": { "marktlokationsId": "51238696781" } });
1900 let errors = validate_pid_json(&json, &reqs);
1901 assert_eq!(missing_fields(&errors), vec!["ruhendeMarktlokationsId"]);
1902 }
1903
1904 #[test]
1905 fn multi_variant_entity_uses_the_elements_own_variant() {
1906 let reqs = multi_variant_requirements();
1907 let json = json!({
1909 "geschaeftspartner": [
1910 { "partnerrolle": "Z03", "adresse": { "ort": "Berlin" } },
1911 { "partnerrolle": "kundeMsb", "name1": "Muster" },
1912 { "partnerrolle": { "code": "messlokationsadresse", "meaning": "x" },
1913 "adresse": { "ort": "Köln" } },
1914 { "partnerrolle": { "code": "Z07" }, "name1": "Beispiel" },
1915 ]
1916 });
1917 let errors = validate_pid_json(&json, &reqs);
1918 assert!(errors.is_empty(), "{}", ValidationReport(errors));
1919 }
1920
1921 #[test]
1922 fn multi_variant_entity_reports_variant_required_fields() {
1923 let reqs = multi_variant_requirements();
1924 let json = json!({ "geschaeftspartner": [{ "partnerrolle": "kundeMsb" }] });
1925 let errors = validate_pid_json(&json, &reqs);
1926 assert_eq!(errors.len(), 1, "{}", ValidationReport(errors));
1927 assert!(matches!(
1928 &errors[0],
1929 PidValidationError::MissingField { field, .. } if field == "name1"
1930 ));
1931 }
1932
1933 #[test]
1934 fn multi_variant_entity_unknown_qualifier_is_invalid_code_only() {
1935 let reqs = multi_variant_requirements();
1936 for bad in [json!("Z99"), json!("bogus"), json!({ "code": "Z99" })] {
1937 let json = json!({ "geschaeftspartner": [{ "partnerrolle": bad }] });
1938 let errors = validate_pid_json(&json, &reqs);
1939 assert_eq!(errors.len(), 1, "{bad}: {}", ValidationReport(errors));
1941 match &errors[0] {
1942 PidValidationError::InvalidCode {
1943 field,
1944 valid_values,
1945 ..
1946 } => {
1947 assert_eq!(field, "partnerrolle");
1948 let codes: Vec<&str> = valid_values.iter().map(|(c, _)| c.as_str()).collect();
1949 assert_eq!(codes, ["Z03", "Z07"]);
1950 }
1951 other => panic!("expected InvalidCode, got {other:?}"),
1952 }
1953 }
1954 }
1955
1956 #[test]
1957 fn code_objects_and_enum_mapped_names_are_code_checked() {
1958 let f = field("partnerrolle", "X", &[("Z07", "kundeMsb")]);
1959 assert_eq!(invalid_code_value(&json!("Z07"), &f), None);
1960 assert_eq!(invalid_code_value(&json!("kundeMsb"), &f), None);
1961 assert_eq!(invalid_code_value(&json!({ "code": "kundeMsb" }), &f), None);
1962 assert_eq!(invalid_code_value(&json!({ "code": "Z07" }), &f), None);
1963 assert_eq!(
1964 invalid_code_value(&json!({ "code": "Z99", "meaning": null }), &f),
1965 Some("Z99".to_string())
1966 );
1967 assert_eq!(
1968 invalid_code_value(&json!("kundeLf"), &f),
1969 Some("kundeLf".to_string())
1970 );
1971 assert_eq!(invalid_code_value(&json!(7), &f), None);
1973 assert_eq!(invalid_code_value(&json!({ "meaning": "x" }), &f), None);
1974 }
1975}