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}
50
51impl PidValidationError {
52 pub fn severity(&self) -> &Severity {
53 match self {
54 Self::MissingEntity { severity, .. } => severity,
55 Self::MissingField { severity, .. } => severity,
56 Self::InvalidCode { .. } => &Severity::Error,
57 }
58 }
59
60 pub fn is_error(&self) -> bool {
61 matches!(self.severity(), Severity::Error)
62 }
63}
64
65impl fmt::Display for PidValidationError {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 match self {
68 PidValidationError::MissingEntity {
69 entity,
70 ahb_status,
71 severity,
72 } => {
73 let label = severity_label(severity);
74 write!(
75 f,
76 "{label}: missing entity '{entity}' (required: {ahb_status})"
77 )
78 }
79 PidValidationError::MissingField {
80 entity,
81 field,
82 ahb_status,
83 rust_type,
84 valid_values,
85 severity,
86 } => {
87 let label = severity_label(severity);
88 write!(
89 f,
90 "{label}: missing {entity}.{field} (required: {ahb_status})"
91 )?;
92 if let Some(rt) = rust_type {
93 write!(f, "\n → type: {rt}")?;
94 }
95 if !valid_values.is_empty() {
96 let codes: Vec<String> = valid_values
97 .iter()
98 .map(|(code, meaning)| {
99 if meaning.is_empty() {
100 code.clone()
101 } else {
102 format!("{code} ({meaning})")
103 }
104 })
105 .collect();
106 write!(f, "\n → valid: {}", codes.join(", "))?;
107 }
108 Ok(())
109 }
110 PidValidationError::InvalidCode {
111 entity,
112 field,
113 value,
114 valid_values,
115 } => {
116 write!(f, "INVALID: {entity}.{field} = \"{value}\"")?;
117 if !valid_values.is_empty() {
118 let codes: Vec<String> = valid_values.iter().map(|(c, _)| c.clone()).collect();
119 write!(f, "\n → valid: {}", codes.join(", "))?;
120 }
121 Ok(())
122 }
123 }
124 }
125}
126
127fn severity_label(severity: &Severity) -> &'static str {
128 match severity {
129 Severity::Error => "ERROR",
130 Severity::Warning => "WARNING",
131 }
132}
133
134pub struct ValidationReport(pub Vec<PidValidationError>);
136
137impl ValidationReport {
138 pub fn has_errors(&self) -> bool {
140 self.0.iter().any(|e| e.is_error())
141 }
142
143 pub fn errors(&self) -> Vec<&PidValidationError> {
145 self.0.iter().filter(|e| e.is_error()).collect()
146 }
147
148 pub fn is_empty(&self) -> bool {
150 self.0.is_empty()
151 }
152
153 pub fn len(&self) -> usize {
155 self.0.len()
156 }
157}
158
159impl fmt::Display for ValidationReport {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 for (i, err) in self.0.iter().enumerate() {
162 if i > 0 {
163 writeln!(f)?;
164 }
165 write!(f, "{err}")?;
166 }
167 Ok(())
168 }
169}
170
171pub fn validate_pid_json(json: &Value, requirements: &PidRequirements) -> Vec<PidValidationError> {
183 validate_entities(json, &requirements.entities, None)
184}
185
186pub fn validate_pid_json_transaction(
192 json: &Value,
193 requirements: &PidRequirements,
194) -> Vec<PidValidationError> {
195 validate_entities(json, &requirements.entities, Some(EntityScope::Transaction))
196}
197
198fn validate_entities(
200 json: &Value,
201 entities: &[EntityRequirement],
202 scope_filter: Option<EntityScope>,
203) -> Vec<PidValidationError> {
204 let mut errors = Vec::new();
205
206 for entity_req in entities {
207 if let Some(ref scope) = scope_filter {
209 if &entity_req.scope != scope {
210 continue;
211 }
212 }
213
214 let key = to_camel_case(&entity_req.entity);
215
216 match json.get(&key) {
217 None | Some(serde_json::Value::Null) => {
218 if is_unconditionally_required(&entity_req.ahb_status) {
219 errors.push(PidValidationError::MissingEntity {
220 entity: entity_req.entity.clone(),
221 ahb_status: entity_req.ahb_status.clone(),
222 severity: Severity::Error,
223 });
224 }
225 }
226 Some(val) => {
227 if let Some(arr) = val.as_array() {
230 for element in arr {
231 validate_entity_fields(element, entity_req, &mut errors);
232 }
233 } else {
234 validate_entity_fields(val, entity_req, &mut errors);
235 }
236 }
237 }
238 }
239
240 errors
241}
242
243pub fn get_nested<'a>(json: &'a Value, path: &str) -> Option<&'a Value> {
246 if let Some((list, rest)) = path.split_once("[].") {
249 return get_nested(json, list)?
250 .as_array()?
251 .iter()
252 .find_map(|item| get_nested(item, rest).filter(|v| !v.is_null()));
253 }
254 let mut current = json;
255 for part in path.split('.') {
256 current = current.get(part).or_else(|| {
257 if part.contains('_') {
258 current.get(snake_to_camel_case(part))
259 } else {
260 None
261 }
262 })?;
263 }
264 Some(current)
265}
266
267fn validate_entity_fields(
269 entity_json: &Value,
270 entity_req: &EntityRequirement,
271 errors: &mut Vec<PidValidationError>,
272) {
273 let fields = effective_field_requirements(entity_req, entity_json);
274 let exempt =
275 fields_of_unrequired_absent_groups(entity_req, entity_json, is_unconditionally_required);
276 for field_req in fields.iter() {
277 let val = get_nested(entity_json, &field_req.bo4e_name);
280
281 let val = val.filter(|v| !v.is_null());
283
284 match val {
285 None if exempt.contains(field_req.bo4e_name.as_str()) => {}
286 None => {
287 if is_unconditionally_required(&field_req.ahb_status) {
288 errors.push(PidValidationError::MissingField {
289 entity: entity_req.entity.clone(),
290 field: field_req.bo4e_name.clone(),
291 ahb_status: field_req.ahb_status.clone(),
292 rust_type: field_req.enum_name.clone(),
293 valid_values: code_values_to_tuples(&field_req.valid_codes),
294 severity: Severity::Error,
295 });
296 }
297 }
298 Some(val) => {
299 validate_code_value(val, entity_req, field_req, errors);
300 }
301 }
302 }
303 validate_nested_lists(
304 &entity_req.entity,
305 entity_json,
306 &entity_req.nested,
307 "",
308 &|status, _| is_unconditionally_required(status).then_some(Severity::Error),
309 &|_| None,
310 errors,
311 );
312}
313
314pub fn validate_nested_lists(
324 entity: &str,
325 element: &Value,
326 lists: &[NestedListRequirement],
327 prefix: &str,
328 required: &dyn Fn(&str, &[(String, usize)]) -> Option<Severity>,
329 variant_holds: &dyn Fn(&VariantQualifier) -> Option<bool>,
330 errors: &mut Vec<PidValidationError>,
331) {
332 nested_lists_at(
333 entity,
334 element,
335 lists,
336 prefix,
337 &mut Vec::new(),
338 required,
339 variant_holds,
340 errors,
341 );
342}
343
344#[allow(clippy::too_many_arguments)]
346fn nested_lists_at(
347 entity: &str,
348 element: &Value,
349 lists: &[NestedListRequirement],
350 prefix: &str,
351 position: &mut Vec<(String, usize)>,
352 required: &dyn Fn(&str, &[(String, usize)]) -> Option<Severity>,
353 variant_holds: &dyn Fn(&VariantQualifier) -> Option<bool>,
354 errors: &mut Vec<PidValidationError>,
355) {
356 let applies = |kind: &NestedElementKind| {
359 kind.transaction_variant
360 .as_ref()
361 .map_or(Some(true), variant_holds)
362 };
363 let mut fields: Vec<&str> = lists.iter().map(|l| l.field.as_str()).collect();
366 fields.sort_unstable();
367 fields.dedup();
368 for field in fields {
369 let candidates: Vec<&NestedListRequirement> = lists
372 .iter()
373 .filter(|l| l.field == field)
374 .filter(|l| {
375 l.parent_selector
376 .iter()
377 .all(|selector| selector_matches(selector, element))
378 })
379 .collect();
380 if candidates.is_empty() {
381 continue;
382 }
383 let items: &[Value] = get_nested(element, field)
384 .and_then(|v| v.as_array())
385 .map_or(&[], |a| a.as_slice());
386 let label = format!("{prefix}{field}");
387 let alone = candidates.len() == 1;
388
389 let chosen = match &candidates[..] {
393 [only] => Some(*only),
394 _ if items.is_empty() => None,
395 _ => {
396 let fitting: Vec<&&NestedListRequirement> = candidates
397 .iter()
398 .filter(|l| items.iter().all(|item| kind_of(l, item, false).is_some()))
399 .collect();
400 match fitting[..] {
401 [one] => Some(*one),
402 _ => None,
403 }
404 }
405 };
406
407 if let Some(list) = chosen {
408 for kind in &list.kinds {
409 if applies(kind) != Some(true) {
410 continue;
411 }
412 let present = if alone && list.kinds.len() == 1 {
413 !items.is_empty()
414 } else {
415 items.iter().any(|item| kind_matches(kind, item))
416 };
417 if present {
418 continue;
419 }
420 if let Some(severity) = required(&kind.ahb_status, position) {
421 let codes = kind.selector.first().map_or(&[][..], |s| &s.codes[..]);
422 let name = if alone && list.kinds.len() == 1 {
425 String::new()
426 } else {
427 codes
428 .first()
429 .map(|c| c.bo4e_value.clone().unwrap_or_else(|| c.code.clone()))
430 .unwrap_or_default()
431 };
432 errors.push(PidValidationError::MissingField {
433 entity: entity.to_string(),
434 field: format!("{label}[{name}]"),
435 ahb_status: kind.ahb_status.clone(),
436 rust_type: None,
437 valid_values: code_values_to_tuples(codes),
438 severity,
439 });
440 }
441 }
442 }
443
444 for (index, item) in items.iter().enumerate() {
445 position.push((field.to_string(), index));
446 let here = position.clone();
447 let required_here = |status: &str| required(status, &here);
448 let matching: Vec<(&NestedListRequirement, &NestedElementKind)> = match chosen {
449 Some(list) => kind_of(list, item, alone)
450 .filter(|k| applies(k) != Some(false))
451 .map(|k| (list, k))
452 .into_iter()
453 .collect(),
454 None => candidates
455 .iter()
456 .flat_map(|l| {
457 l.kinds
458 .iter()
459 .filter(|k| applies(k) != Some(false))
460 .filter(|k| kind_matches(k, item))
461 .map(move |k| (*l, k))
462 })
463 .collect(),
464 };
465 let best = matching
469 .iter()
470 .map(|(l, k)| {
471 (
472 *l,
473 k,
474 element_findings(entity, &label, k, item, &required_here),
475 )
476 })
477 .min_by_key(|(_, k, f)| (f.len(), k.selector.is_empty()))
478 .map(|(l, _, f)| (l, f));
479 if let Some((list, findings)) = best {
480 errors.extend(findings);
481 nested_lists_at(
482 entity,
483 item,
484 &list.nested,
485 &format!("{label}[]."),
486 position,
487 required,
488 variant_holds,
489 errors,
490 );
491 }
492 position.pop();
493 }
494 }
495}
496
497fn kind_of<'l>(
501 list: &'l NestedListRequirement,
502 item: &Value,
503 alone: bool,
504) -> Option<&'l NestedElementKind> {
505 match &list.kinds[..] {
506 [only] if alone => Some(only),
507 kinds => kinds.iter().find(|k| kind_matches(k, item)),
508 }
509}
510
511fn kind_matches(kind: &NestedElementKind, item: &Value) -> bool {
514 kind.selector
515 .iter()
516 .all(|selector| selector_matches(selector, item))
517}
518
519fn selector_matches(selector: &KindSelector, item: &Value) -> bool {
521 get_nested(item, &selector.field)
522 .and_then(code_field_value)
523 .is_some_and(|value| {
524 selector
525 .codes
526 .iter()
527 .any(|c| c.code == value || c.bo4e_value.as_deref() == Some(value))
528 })
529}
530
531fn element_findings(
534 entity: &str,
535 label: &str,
536 kind: &NestedElementKind,
537 item: &Value,
538 required: &dyn Fn(&str) -> Option<Severity>,
539) -> Vec<PidValidationError> {
540 let mut out = Vec::new();
541 for field_req in &kind.fields {
542 let name = format!("{label}[].{}", field_req.bo4e_name);
543 let values = all_nested(item, &field_req.bo4e_name);
544 if values.is_empty() {
545 if let Some(severity) = required(&field_req.ahb_status) {
546 out.push(PidValidationError::MissingField {
547 entity: entity.to_string(),
548 field: name,
549 ahb_status: field_req.ahb_status.clone(),
550 rust_type: field_req.enum_name.clone(),
551 valid_values: code_values_to_tuples(&field_req.valid_codes),
552 severity,
553 });
554 }
555 continue;
556 }
557 for value in values {
558 if let Some(value) = invalid_code_value(value, field_req) {
559 out.push(PidValidationError::InvalidCode {
560 entity: entity.to_string(),
561 field: name.clone(),
562 value,
563 valid_values: code_values_to_tuples(&field_req.valid_codes),
564 });
565 }
566 }
567 }
568 out
569}
570
571fn all_nested<'a>(json: &'a Value, path: &str) -> Vec<&'a Value> {
574 if let Some((list, rest)) = path.split_once("[].") {
575 return get_nested(json, list)
576 .and_then(|v| v.as_array())
577 .map(|items| {
578 items
579 .iter()
580 .flat_map(|item| all_nested(item, rest))
581 .collect()
582 })
583 .unwrap_or_default();
584 }
585 get_nested(json, path)
586 .filter(|v| !v.is_null())
587 .into_iter()
588 .collect()
589}
590
591fn validate_code_value(
593 val: &Value,
594 entity_req: &EntityRequirement,
595 field_req: &FieldRequirement,
596 errors: &mut Vec<PidValidationError>,
597) {
598 if let Some(value) = invalid_code_value(val, field_req) {
599 errors.push(PidValidationError::InvalidCode {
600 entity: entity_req.entity.clone(),
601 field: field_req.bo4e_name.clone(),
602 value,
603 valid_values: code_values_to_tuples(&field_req.valid_codes),
604 });
605 }
606}
607
608pub fn code_field_value(val: &Value) -> Option<&str> {
611 val.as_str()
612 .or_else(|| val.get("code").and_then(|c| c.as_str()))
613}
614
615pub fn invalid_code_value(val: &Value, field_req: &FieldRequirement) -> Option<String> {
622 if field_req.valid_codes.is_empty() {
623 return None;
624 }
625 let value = code_field_value(val)?;
626 let is_valid = field_req
627 .valid_codes
628 .iter()
629 .any(|cv| cv.code == value || cv.bo4e_value.as_deref() == Some(value));
630 (!is_valid).then(|| value.to_string())
631}
632
633pub fn effective_field_requirements<'a>(
648 entity_req: &'a EntityRequirement,
649 element: &Value,
650) -> Cow<'a, [FieldRequirement]> {
651 if entity_req.variants.is_empty() {
652 return Cow::Borrowed(&entity_req.fields);
653 }
654
655 let mut by_field: BTreeMap<&str, Vec<&EntityVariantRequirement>> = BTreeMap::new();
657 for v in &entity_req.variants {
658 by_field
659 .entry(v.discriminator_field.as_str())
660 .or_default()
661 .push(v);
662 }
663 let mut candidates: Vec<&EntityVariantRequirement> = Vec::new();
664 for (field, group) in by_field {
665 let value = get_nested(element, field).and_then(code_field_value);
666 let matched: Vec<&EntityVariantRequirement> = group
667 .iter()
668 .copied()
669 .filter(|v| value.is_some_and(|s| v.code == s || v.bo4e_value.as_deref() == Some(s)))
670 .collect();
671 candidates.extend(if matched.is_empty() { group } else { matched });
672 }
673
674 let mut owned: Vec<&str> = Vec::new();
676 let mut owned_set: BTreeSet<&str> = BTreeSet::new();
677 for v in &entity_req.variants {
678 for f in &v.fields {
679 if owned_set.insert(f.bo4e_name.as_str()) {
680 owned.push(f.bo4e_name.as_str());
681 }
682 }
683 }
684
685 let mut combined: BTreeMap<&str, FieldRequirement> = BTreeMap::new();
686 for name in owned {
687 let reqs: Vec<&FieldRequirement> = candidates
688 .iter()
689 .filter_map(|v| v.fields.iter().find(|f| f.bo4e_name == name))
690 .collect();
691 let Some((first, rest)) = reqs.split_first() else {
692 continue; };
694 let mut field = (*first).clone();
695 let mut statuses_agree = reqs.len() == candidates.len();
696 for r in rest {
697 if r.ahb_status != field.ahb_status {
698 statuses_agree = false;
699 }
700 for cv in &r.valid_codes {
701 if !field.valid_codes.iter().any(|c| c.code == cv.code) {
702 field.valid_codes.push(cv.clone());
703 }
704 }
705 }
706 if !statuses_agree {
707 field.ahb_status = String::new();
708 }
709 combined.insert(name, field);
710 }
711
712 let mut result: Vec<FieldRequirement> = Vec::with_capacity(entity_req.fields.len());
713 for f in &entity_req.fields {
714 if owned_set.contains(f.bo4e_name.as_str()) {
715 if let Some(c) = combined.remove(f.bo4e_name.as_str()) {
716 result.push(c);
717 }
718 } else {
719 result.push(f.clone());
720 }
721 }
722 result.extend(combined.into_values());
723 Cow::Owned(result)
724}
725
726pub fn absent_groups<'a>(
729 entity_req: &'a EntityRequirement,
730 element: &'a Value,
731) -> impl Iterator<Item = &'a EntityGroupRequirement> + 'a {
732 entity_req.groups.iter().filter(move |g| {
733 !g.fields
734 .iter()
735 .any(|f| get_nested(element, f).is_some_and(|v| !v.is_null()))
736 })
737}
738
739pub fn fields_of_unrequired_absent_groups<'a>(
748 entity_req: &'a EntityRequirement,
749 element: &'a Value,
750 mut group_required: impl FnMut(&str) -> bool,
751) -> BTreeSet<&'a str> {
752 let exempt: Vec<&EntityGroupRequirement> = absent_groups(entity_req, element)
753 .filter(|g| !group_required(&g.ahb_status))
754 .collect();
755 let mut fields = BTreeSet::new();
756 for g in &exempt {
757 for f in &g.fields {
758 let kept_elsewhere = entity_req.groups.iter().any(|other| {
761 other.fields.contains(f) && !exempt.iter().any(|e| std::ptr::eq(*e, other))
762 });
763 if !kept_elsewhere {
764 fields.insert(f.as_str());
765 }
766 }
767 }
768 fields
769}
770
771fn code_values_to_tuples(codes: &[CodeValue]) -> Vec<(String, String)> {
773 codes
774 .iter()
775 .map(|cv| (cv.code.clone(), cv.meaning.clone()))
776 .collect()
777}
778
779fn to_camel_case(s: &str) -> String {
785 if s.is_empty() {
786 return String::new();
787 }
788 let mut chars = s.chars();
789 let first = chars.next().unwrap();
790 let mut result = first.to_lowercase().to_string();
791 result.extend(chars);
792 result
793}
794
795fn snake_to_camel_case(s: &str) -> String {
806 let mut result = String::with_capacity(s.len());
807 let mut capitalize_next = false;
808 for ch in s.chars() {
809 if ch == '_' {
810 capitalize_next = true;
811 } else if capitalize_next {
812 result.extend(ch.to_uppercase());
813 capitalize_next = false;
814 } else {
815 result.push(ch);
816 }
817 }
818 result
819}
820
821fn is_unconditionally_required(ahb_status: &str) -> bool {
823 matches!(ahb_status, "X" | "Muss" | "Soll")
824}
825
826#[cfg(test)]
827mod tests {
828 use super::*;
829
830 #[test]
834 fn a_variant_bound_kind_is_demanded_only_in_its_variant() {
835 use crate::pid_requirements::{NestedElementKind, NestedListRequirement, VariantQualifier};
836 let lists = vec![NestedListRequirement {
837 field: "zuordnungen".to_string(),
838 bo4e_type: "Zuordnung".to_string(),
839 source_path: "sg4.sg8_z22.sg10".to_string(),
840 kinds: vec![NestedElementKind {
841 ahb_status: "Muss".to_string(),
842 selector: vec![],
843 fields: vec![],
844 transaction_variant: Some(VariantQualifier {
845 group: "SG4".to_string(),
846 segment: "IDE".to_string(),
847 codes: vec!["24".to_string()],
848 }),
849 }],
850 parent_selector: vec![],
851 nested: vec![],
852 }];
853 let demanded = |holds: Option<bool>| {
854 let mut errors = Vec::new();
855 validate_nested_lists(
856 "SummenzeitreihenDaten",
857 &serde_json::json!({}),
858 &lists,
859 "",
860 &|_, _| Some(Severity::Error),
861 &|_| holds,
862 &mut errors,
863 );
864 errors.len()
865 };
866 assert_eq!(demanded(Some(true)), 1);
867 assert_eq!(demanded(Some(false)), 0);
868 assert_eq!(demanded(None), 0);
869 }
870 use crate::pid_requirements::{
871 Bo4eRefType, Cardinality, CodeValue, EntityRequirement, FieldRequirement, PidRequirements,
872 };
873 use serde_json::json;
874
875 fn sample_requirements() -> PidRequirements {
876 PidRequirements {
877 pid: "55001".to_string(),
878 beschreibung: "Anmeldung verb. MaLo".to_string(),
879 entities: vec![
880 EntityRequirement {
881 entity: "Prozessdaten".to_string(),
882 ref_type: Bo4eRefType::Object {
883 type_name: "Prozessdaten".to_string(),
884
885 cardinality: Cardinality::REQUIRED,
886 },
887
888 ahb_status: "Muss".to_string(),
889 map_key: None,
890 scope: EntityScope::Transaction,
891 variants: vec![],
892 groups: vec![],
893 fields: vec![
894 FieldRequirement {
895 bo4e_name: "vorgangId".to_string(),
896 ahb_status: "X".to_string(),
897 field_type: "data".to_string(),
898 format: None,
899 enum_name: None,
900 valid_codes: vec![],
901 child_group: None,
902 ref_type: Bo4eRefType::Unknown,
903 },
904 FieldRequirement {
905 bo4e_name: "transaktionsgrund".to_string(),
906 ahb_status: "X".to_string(),
907 field_type: "code".to_string(),
908 format: None,
909 enum_name: Some("Transaktionsgrund".to_string()),
910 valid_codes: vec![
911 CodeValue {
912 code: "E01".to_string(),
913 meaning: "Ein-/Auszug (Einzug)".to_string(),
914 enum_name: None,
915 bo4e_value: None,
916 },
917 CodeValue {
918 code: "E03".to_string(),
919 meaning: "Wechsel".to_string(),
920 enum_name: None,
921 bo4e_value: None,
922 },
923 ],
924 child_group: None,
925 ref_type: Bo4eRefType::Unknown,
926 },
927 ],
928 nested: Vec::new(),
929 },
930 EntityRequirement {
931 entity: "Marktlokation".to_string(),
932 ref_type: Bo4eRefType::Object {
933 type_name: "Marktlokation".to_string(),
934
935 cardinality: Cardinality::REQUIRED,
936 },
937
938 ahb_status: "Muss".to_string(),
939 map_key: None,
940 scope: EntityScope::Transaction,
941 variants: vec![],
942 groups: vec![],
943 fields: vec![
944 FieldRequirement {
945 bo4e_name: "marktlokationsId".to_string(),
946 ahb_status: "X".to_string(),
947 field_type: "data".to_string(),
948 format: None,
949 enum_name: None,
950 valid_codes: vec![],
951 child_group: None,
952 ref_type: Bo4eRefType::Unknown,
953 },
954 FieldRequirement {
955 bo4e_name: "haushaltskunde".to_string(),
956 ahb_status: "X".to_string(),
957 field_type: "code".to_string(),
958 format: None,
959 enum_name: Some("Haushaltskunde".to_string()),
960 valid_codes: vec![
961 CodeValue {
962 code: "Z15".to_string(),
963 meaning: "Ja".to_string(),
964 enum_name: None,
965 bo4e_value: None,
966 },
967 CodeValue {
968 code: "Z18".to_string(),
969 meaning: "Nein".to_string(),
970 enum_name: None,
971 bo4e_value: None,
972 },
973 ],
974 child_group: None,
975 ref_type: Bo4eRefType::Unknown,
976 },
977 ],
978 nested: Vec::new(),
979 },
980 EntityRequirement {
981 entity: "Geschaeftspartner".to_string(),
982 ref_type: Bo4eRefType::Object {
983 type_name: "Geschaeftspartner".to_string(),
984
985 cardinality: Cardinality {
986 min: 1,
987 max: Some(7),
988 },
989 },
990
991 ahb_status: "Muss".to_string(),
992 map_key: None,
993 scope: EntityScope::Transaction,
994 variants: vec![],
995 groups: vec![],
996 fields: vec![FieldRequirement {
997 bo4e_name: "identifikation".to_string(),
998 ahb_status: "X".to_string(),
999 field_type: "data".to_string(),
1000 format: None,
1001 enum_name: None,
1002 valid_codes: vec![],
1003 child_group: None,
1004 ref_type: Bo4eRefType::Unknown,
1005 }],
1006 nested: Vec::new(),
1007 },
1008 ],
1009 }
1010 }
1011
1012 #[test]
1013 fn test_validate_complete_json() {
1014 let reqs = sample_requirements();
1015 let json = json!({
1016 "prozessdaten": {
1017 "vorgangId": "ABC123",
1018 "transaktionsgrund": "E01"
1019 },
1020 "marktlokation": {
1021 "marktlokationsId": "51234567890",
1022 "haushaltskunde": "Z15"
1023 },
1024 "geschaeftspartner": [
1025 { "identifikation": "9900000000003" }
1026 ]
1027 });
1028
1029 let errors = validate_pid_json(&json, &reqs);
1030 assert!(errors.is_empty(), "Expected no errors, got: {errors:?}");
1031 }
1032
1033 #[test]
1034 fn test_validate_missing_entity() {
1035 let reqs = sample_requirements();
1036 let json = json!({
1037 "prozessdaten": {
1038 "vorgangId": "ABC123",
1039 "transaktionsgrund": "E01"
1040 },
1041 "geschaeftspartner": [
1042 { "identifikation": "9900000000003" }
1043 ]
1044 });
1045 let errors = validate_pid_json(&json, &reqs);
1048 assert_eq!(errors.len(), 1);
1049 match &errors[0] {
1050 PidValidationError::MissingEntity {
1051 entity,
1052 ahb_status,
1053 severity,
1054 } => {
1055 assert_eq!(entity, "Marktlokation");
1056 assert_eq!(ahb_status, "Muss");
1057 assert_eq!(severity, &Severity::Error);
1058 }
1059 other => panic!("Expected MissingEntity, got: {other:?}"),
1060 }
1061
1062 let msg = errors[0].to_string();
1064 assert!(msg.contains("ERROR"));
1065 assert!(msg.contains("Marktlokation"));
1066 assert!(msg.contains("Muss"));
1067 }
1068
1069 #[test]
1070 fn test_validate_missing_field() {
1071 let reqs = sample_requirements();
1072 let json = json!({
1073 "prozessdaten": {
1074 "transaktionsgrund": "E01"
1075 },
1077 "marktlokation": {
1078 "marktlokationsId": "51234567890",
1079 "haushaltskunde": "Z15"
1080 },
1081 "geschaeftspartner": [
1082 { "identifikation": "9900000000003" }
1083 ]
1084 });
1085
1086 let errors = validate_pid_json(&json, &reqs);
1087 assert_eq!(errors.len(), 1);
1088 match &errors[0] {
1089 PidValidationError::MissingField {
1090 entity,
1091 field,
1092 ahb_status,
1093 severity,
1094 ..
1095 } => {
1096 assert_eq!(entity, "Prozessdaten");
1097 assert_eq!(field, "vorgangId");
1098 assert_eq!(ahb_status, "X");
1099 assert_eq!(severity, &Severity::Error);
1100 }
1101 other => panic!("Expected MissingField, got: {other:?}"),
1102 }
1103
1104 let msg = errors[0].to_string();
1105 assert!(msg.contains("ERROR"));
1106 assert!(msg.contains("Prozessdaten.vorgangId"));
1107 }
1108
1109 #[test]
1110 fn test_validate_invalid_code() {
1111 let reqs = sample_requirements();
1112 let json = json!({
1113 "prozessdaten": {
1114 "vorgangId": "ABC123",
1115 "transaktionsgrund": "E01"
1116 },
1117 "marktlokation": {
1118 "marktlokationsId": "51234567890",
1119 "haushaltskunde": "Z99" },
1121 "geschaeftspartner": [
1122 { "identifikation": "9900000000003" }
1123 ]
1124 });
1125
1126 let errors = validate_pid_json(&json, &reqs);
1127 assert_eq!(errors.len(), 1);
1128 match &errors[0] {
1129 PidValidationError::InvalidCode {
1130 entity,
1131 field,
1132 value,
1133 valid_values,
1134 } => {
1135 assert_eq!(entity, "Marktlokation");
1136 assert_eq!(field, "haushaltskunde");
1137 assert_eq!(value, "Z99");
1138 assert_eq!(valid_values.len(), 2);
1139 assert!(valid_values.iter().any(|(c, _)| c == "Z15"));
1140 assert!(valid_values.iter().any(|(c, _)| c == "Z18"));
1141 }
1142 other => panic!("Expected InvalidCode, got: {other:?}"),
1143 }
1144
1145 let msg = errors[0].to_string();
1146 assert!(msg.contains("INVALID"));
1147 assert!(msg.contains("Z99"));
1148 assert!(msg.contains("Z15"));
1149 }
1150
1151 #[test]
1152 fn test_validate_array_entity() {
1153 let reqs = sample_requirements();
1154 let json = json!({
1155 "prozessdaten": {
1156 "vorgangId": "ABC123",
1157 "transaktionsgrund": "E01"
1158 },
1159 "marktlokation": {
1160 "marktlokationsId": "51234567890",
1161 "haushaltskunde": "Z15"
1162 },
1163 "geschaeftspartner": [
1164 { "identifikation": "9900000000003" },
1165 { } ]
1167 });
1168
1169 let errors = validate_pid_json(&json, &reqs);
1170 assert_eq!(errors.len(), 1);
1171 match &errors[0] {
1172 PidValidationError::MissingField { entity, field, .. } => {
1173 assert_eq!(entity, "Geschaeftspartner");
1174 assert_eq!(field, "identifikation");
1175 }
1176 other => panic!("Expected MissingField, got: {other:?}"),
1177 }
1178 }
1179
1180 #[test]
1181 fn test_to_camel_case() {
1182 assert_eq!(to_camel_case("Prozessdaten"), "prozessdaten");
1183 assert_eq!(
1184 to_camel_case("RuhendeMarktlokation"),
1185 "ruhendeMarktlokation"
1186 );
1187 assert_eq!(to_camel_case("Marktlokation"), "marktlokation");
1188 assert_eq!(to_camel_case(""), "");
1189 }
1190
1191 #[test]
1192 fn test_snake_to_camel_case() {
1193 assert_eq!(snake_to_camel_case("code_codepflege"), "codeCodepflege");
1194 assert_eq!(snake_to_camel_case("vorgang_id"), "vorgangId");
1195 assert_eq!(snake_to_camel_case("marktlokation"), "marktlokation");
1196 assert_eq!(snake_to_camel_case(""), "");
1197 assert_eq!(snake_to_camel_case("a_b_c"), "aBC");
1198 }
1199
1200 #[test]
1203 fn test_camel_case_fallback_for_snake_case_bo4e_name() {
1204 let reqs = PidRequirements {
1205 pid: "55077".to_string(),
1206 beschreibung: "Test camelCase fallback".to_string(),
1207 entities: vec![EntityRequirement {
1208 entity: "Zuordnung".to_string(),
1209 ref_type: Bo4eRefType::Object {
1210 type_name: "Zuordnung".to_string(),
1211
1212 cardinality: Cardinality::REQUIRED,
1213 },
1214
1215 ahb_status: "Muss".to_string(),
1216 map_key: None,
1217 scope: EntityScope::Transaction,
1218 variants: vec![],
1219 groups: vec![],
1220 fields: vec![
1221 FieldRequirement {
1222 bo4e_name: "code_codepflege".to_string(),
1224 ahb_status: "X".to_string(),
1225 field_type: "data".to_string(),
1226 format: None,
1227 enum_name: None,
1228 valid_codes: vec![],
1229 child_group: None,
1230 ref_type: Bo4eRefType::Unknown,
1231 },
1232 FieldRequirement {
1233 bo4e_name: "codeliste".to_string(),
1234 ahb_status: "X".to_string(),
1235 field_type: "data".to_string(),
1236 format: None,
1237 enum_name: None,
1238 valid_codes: vec![],
1239 child_group: None,
1240 ref_type: Bo4eRefType::Unknown,
1241 },
1242 ],
1243 nested: Vec::new(),
1244 }],
1245 };
1246
1247 let json_camel = json!({
1250 "zuordnung": {
1251 "codeCodepflege": "DE_BDEW",
1252 "codeliste": "6"
1253 }
1254 });
1255
1256 let errors = validate_pid_json(&json_camel, &reqs);
1257 assert!(
1258 errors.is_empty(),
1259 "Expected no errors when field is present under camelCase key, got: {errors:?}"
1260 );
1261
1262 let json_snake = json!({
1264 "zuordnung": {
1265 "code_codepflege": "DE_BDEW",
1266 "codeliste": "6"
1267 }
1268 });
1269
1270 let errors = validate_pid_json(&json_snake, &reqs);
1271 assert!(
1272 errors.is_empty(),
1273 "Expected no errors when field is present under snake_case key, got: {errors:?}"
1274 );
1275
1276 let json_missing = json!({
1278 "zuordnung": {
1279 "codeliste": "6"
1280 }
1281 });
1282
1283 let errors = validate_pid_json(&json_missing, &reqs);
1284 assert_eq!(errors.len(), 1);
1285 match &errors[0] {
1286 PidValidationError::MissingField { field, .. } => {
1287 assert_eq!(field, "code_codepflege");
1288 }
1289 other => panic!("Expected MissingField, got: {other:?}"),
1290 }
1291 }
1292
1293 #[test]
1294 fn test_is_unconditionally_required() {
1295 assert!(is_unconditionally_required("X"));
1296 assert!(is_unconditionally_required("Muss"));
1297 assert!(is_unconditionally_required("Soll"));
1298 assert!(!is_unconditionally_required("Kann"));
1299 assert!(!is_unconditionally_required("[1]"));
1300 assert!(!is_unconditionally_required(""));
1301 }
1302
1303 #[test]
1304 fn test_validation_report_display() {
1305 let errors = vec![
1306 PidValidationError::MissingEntity {
1307 entity: "Marktlokation".to_string(),
1308 ahb_status: "Muss".to_string(),
1309 severity: Severity::Error,
1310 },
1311 PidValidationError::MissingField {
1312 entity: "Prozessdaten".to_string(),
1313 field: "vorgangId".to_string(),
1314 ahb_status: "X".to_string(),
1315 rust_type: None,
1316 valid_values: vec![],
1317 severity: Severity::Error,
1318 },
1319 ];
1320 let report = ValidationReport(errors);
1321 assert!(report.has_errors());
1322 assert_eq!(report.len(), 2);
1323 assert!(!report.is_empty());
1324
1325 let display = report.to_string();
1326 assert!(display.contains("missing entity 'Marktlokation'"));
1327 assert!(display.contains("missing Prozessdaten.vorgangId"));
1328 }
1329
1330 #[test]
1331 fn test_missing_field_with_type_and_values_display() {
1332 let err = PidValidationError::MissingField {
1333 entity: "Marktlokation".to_string(),
1334 field: "haushaltskunde".to_string(),
1335 ahb_status: "Muss".to_string(),
1336 rust_type: Some("Haushaltskunde".to_string()),
1337 valid_values: vec![
1338 ("Z15".to_string(), "Ja".to_string()),
1339 ("Z18".to_string(), "Nein".to_string()),
1340 ],
1341 severity: Severity::Error,
1342 };
1343 let msg = err.to_string();
1344 assert!(msg.contains("type: Haushaltskunde"));
1345 assert!(msg.contains("valid: Z15 (Ja), Z18 (Nein)"));
1346 }
1347
1348 #[test]
1349 fn test_optional_fields_not_flagged() {
1350 let reqs = PidRequirements {
1351 pid: "99999".to_string(),
1352 beschreibung: "Test".to_string(),
1353 entities: vec![EntityRequirement {
1354 entity: "Test".to_string(),
1355 ref_type: Bo4eRefType::Object {
1356 type_name: "Test".to_string(),
1357
1358 cardinality: Cardinality::OPTIONAL,
1359 },
1360
1361 ahb_status: "Kann".to_string(),
1362 map_key: None,
1363 scope: EntityScope::Transaction,
1364 variants: vec![],
1365 groups: vec![],
1366 fields: vec![FieldRequirement {
1367 bo4e_name: "optionalField".to_string(),
1368 ahb_status: "Kann".to_string(),
1369 field_type: "data".to_string(),
1370 format: None,
1371 enum_name: None,
1372 valid_codes: vec![],
1373 child_group: None,
1374 ref_type: Bo4eRefType::Unknown,
1375 }],
1376 nested: Vec::new(),
1377 }],
1378 };
1379
1380 let errors = validate_pid_json(&json!({}), &reqs);
1382 assert!(errors.is_empty());
1383
1384 let errors = validate_pid_json(&json!({ "test": {} }), &reqs);
1386 assert!(errors.is_empty());
1387 }
1388
1389 #[test]
1392 fn test_nested_dot_path_fields_not_falsely_missing() {
1393 let reqs = PidRequirements {
1394 pid: "55001".to_string(),
1395 beschreibung: "Test nested paths".to_string(),
1396 entities: vec![EntityRequirement {
1397 entity: "ProduktpaketDaten".to_string(),
1398 ref_type: Bo4eRefType::Object {
1399 type_name: "ProduktpaketDaten".to_string(),
1400
1401 cardinality: Cardinality {
1402 min: 1,
1403 max: Some(99999),
1404 },
1405 },
1406
1407 ahb_status: "Muss".to_string(),
1408 map_key: None,
1409 scope: EntityScope::Transaction,
1410 variants: vec![],
1411 groups: vec![],
1412 fields: vec![
1413 FieldRequirement {
1414 bo4e_name: "produktIdentifikation.funktion".to_string(),
1415 ahb_status: "X".to_string(),
1416 field_type: "code".to_string(),
1417 format: None,
1418 enum_name: Some("Produktidentifikation".to_string()),
1419 valid_codes: vec![CodeValue {
1420 code: "5".to_string(),
1421 meaning: "Produktidentifikation".to_string(),
1422 enum_name: None,
1423 bo4e_value: None,
1424 }],
1425 child_group: None,
1426 ref_type: Bo4eRefType::Unknown,
1427 },
1428 FieldRequirement {
1429 bo4e_name: "produktMerkmal.code".to_string(),
1430 ahb_status: "X".to_string(),
1431 field_type: "code".to_string(),
1432 format: None,
1433 enum_name: None,
1434 valid_codes: vec![],
1435 child_group: None,
1436 ref_type: Bo4eRefType::Unknown,
1437 },
1438 ],
1439 nested: Vec::new(),
1440 }],
1441 };
1442
1443 let json = json!({
1445 "produktpaketDaten": [{
1446 "produktIdentifikation": { "funktion": "5", "id": "9991000002082", "typ": "Z11" },
1447 "produktMerkmal": { "code": "ZH9" }
1448 }]
1449 });
1450
1451 let errors = validate_pid_json(&json, &reqs);
1452 assert!(
1453 errors.is_empty(),
1454 "Nested dot-path fields should be found (issue #48), got: {errors:?}"
1455 );
1456 }
1457
1458 #[test]
1459 fn test_nested_dot_path_truly_missing() {
1460 let reqs = PidRequirements {
1461 pid: "55001".to_string(),
1462 beschreibung: "Test nested paths missing".to_string(),
1463 entities: vec![EntityRequirement {
1464 entity: "ProduktpaketDaten".to_string(),
1465 ref_type: Bo4eRefType::Object {
1466 type_name: "ProduktpaketDaten".to_string(),
1467
1468 cardinality: Cardinality {
1469 min: 1,
1470 max: Some(99999),
1471 },
1472 },
1473
1474 ahb_status: "Muss".to_string(),
1475 map_key: None,
1476 scope: EntityScope::Transaction,
1477 variants: vec![],
1478 groups: vec![],
1479 fields: vec![FieldRequirement {
1480 bo4e_name: "produktIdentifikation.funktion".to_string(),
1481 ahb_status: "X".to_string(),
1482 field_type: "data".to_string(),
1483 format: None,
1484 enum_name: None,
1485 valid_codes: vec![],
1486 child_group: None,
1487 ref_type: Bo4eRefType::Unknown,
1488 }],
1489 nested: Vec::new(),
1490 }],
1491 };
1492
1493 let json = json!({
1495 "produktpaketDaten": [{
1496 "produktIdentifikation": { "id": "123" }
1497 }]
1498 });
1499
1500 let errors = validate_pid_json(&json, &reqs);
1501 assert_eq!(errors.len(), 1, "Should report missing nested field");
1502 match &errors[0] {
1503 PidValidationError::MissingField { field, .. } => {
1504 assert_eq!(field, "produktIdentifikation.funktion");
1505 }
1506 other => panic!("Expected MissingField, got: {other:?}"),
1507 }
1508 }
1509
1510 fn field(name: &str, status: &str, codes: &[(&str, &str)]) -> FieldRequirement {
1511 FieldRequirement {
1512 bo4e_name: name.to_string(),
1513 ahb_status: status.to_string(),
1514 field_type: if codes.is_empty() { "data" } else { "code" }.to_string(),
1515 format: None,
1516 enum_name: None,
1517 valid_codes: codes
1518 .iter()
1519 .map(|(code, mapped)| CodeValue {
1520 code: code.to_string(),
1521 meaning: String::new(),
1522 enum_name: None,
1523 bo4e_value: Some(mapped.to_string()),
1524 })
1525 .collect(),
1526 child_group: None,
1527 ref_type: Bo4eRefType::Unknown,
1528 }
1529 }
1530
1531 fn multi_variant_requirements() -> PidRequirements {
1534 let z03 = ("Z03", "messlokationsadresse");
1535 let z07 = ("Z07", "kundeMsb");
1536 PidRequirements {
1537 pid: "55042".to_string(),
1538 beschreibung: String::new(),
1539 entities: vec![EntityRequirement {
1540 entity: "Geschaeftspartner".to_string(),
1541 ref_type: Bo4eRefType::Object {
1542 type_name: "Geschaeftspartner".to_string(),
1543 cardinality: Cardinality {
1544 min: 1,
1545 max: Some(99),
1546 },
1547 },
1548 ahb_status: "Muss".to_string(),
1549 fields: vec![
1551 field("adresse.ort", "X", &[]),
1552 field("name1", "X", &[]),
1553 field("partnerrolle", "X", &[z03, z07]),
1554 ],
1555 map_key: None,
1556 scope: EntityScope::Transaction,
1557 variants: vec![
1558 EntityVariantRequirement {
1559 discriminator_field: "partnerrolle".to_string(),
1560 code: "Z03".to_string(),
1561 bo4e_value: Some("messlokationsadresse".to_string()),
1562 source_paths: vec!["sg4.sg12_z03".to_string()],
1563 fields: vec![
1564 field("adresse.ort", "X", &[]),
1565 field("partnerrolle", "X", &[z03]),
1566 ],
1567 },
1568 EntityVariantRequirement {
1569 discriminator_field: "partnerrolle".to_string(),
1570 code: "Z07".to_string(),
1571 bo4e_value: Some("kundeMsb".to_string()),
1572 source_paths: vec!["sg4.sg12_z07".to_string()],
1573 fields: vec![field("name1", "X", &[]), field("partnerrolle", "X", &[z07])],
1574 },
1575 ],
1576 groups: vec![],
1577 nested: Vec::new(),
1578 }],
1579 }
1580 }
1581
1582 fn multi_group_requirements(z22_status: &str) -> PidRequirements {
1584 let group = |path: &str, status: &str, fields: &[&str]| EntityGroupRequirement {
1585 source_path: path.to_string(),
1586 ahb_status: status.to_string(),
1587 fields: fields.iter().map(|f| f.to_string()).collect(),
1588 };
1589 PidRequirements {
1590 pid: "55043".to_string(),
1591 beschreibung: String::new(),
1592 entities: vec![EntityRequirement {
1593 entity: "Marktlokation".to_string(),
1594 ref_type: Bo4eRefType::Object {
1595 type_name: "Marktlokation".to_string(),
1596 cardinality: Cardinality::REQUIRED,
1597 },
1598 ahb_status: "Muss".to_string(),
1599 fields: vec![
1600 field("marktlokationsId", "X", &[]),
1601 field("ruhendeMarktlokationsId", "X", &[]),
1602 field("ruhendeMarktlokationZeitraumId", "Kann", &[]),
1603 ],
1604 map_key: None,
1605 scope: EntityScope::Transaction,
1606 variants: vec![],
1607 groups: vec![
1608 group("sg4.sg5_z16", "Muss", &["marktlokationsId"]),
1609 group(
1610 "sg4.sg5_z22",
1611 z22_status,
1612 &["ruhendeMarktlokationsId", "ruhendeMarktlokationZeitraumId"],
1613 ),
1614 ],
1615 nested: Vec::new(),
1616 }],
1617 }
1618 }
1619
1620 fn missing_fields(errors: &[PidValidationError]) -> Vec<&str> {
1621 errors
1622 .iter()
1623 .filter_map(|e| match e {
1624 PidValidationError::MissingField { field, .. } => Some(field.as_str()),
1625 _ => None,
1626 })
1627 .collect()
1628 }
1629
1630 #[test]
1631 fn an_absent_optional_groups_fields_are_not_demanded() {
1632 let reqs = multi_group_requirements("Soll [2003]");
1633 let errors = validate_pid_json(&json!({ "marktlokation": {} }), &reqs);
1634 assert_eq!(missing_fields(&errors), vec!["marktlokationsId"]);
1636 }
1637
1638 #[test]
1639 fn a_filled_groups_fields_are_demanded() {
1640 let reqs = multi_group_requirements("Soll [2003]");
1641 let json = json!({ "marktlokation": {
1642 "marktlokationsId": "51238696781",
1643 "ruhendeMarktlokationZeitraumId": "1"
1644 }});
1645 let errors = validate_pid_json(&json, &reqs);
1646 assert_eq!(missing_fields(&errors), vec!["ruhendeMarktlokationsId"]);
1647 }
1648
1649 #[test]
1650 fn an_absent_required_groups_fields_are_demanded() {
1651 let reqs = multi_group_requirements("Muss");
1652 let json = json!({ "marktlokation": { "marktlokationsId": "51238696781" } });
1653 let errors = validate_pid_json(&json, &reqs);
1654 assert_eq!(missing_fields(&errors), vec!["ruhendeMarktlokationsId"]);
1655 }
1656
1657 #[test]
1658 fn multi_variant_entity_uses_the_elements_own_variant() {
1659 let reqs = multi_variant_requirements();
1660 let json = json!({
1662 "geschaeftspartner": [
1663 { "partnerrolle": "Z03", "adresse": { "ort": "Berlin" } },
1664 { "partnerrolle": "kundeMsb", "name1": "Muster" },
1665 { "partnerrolle": { "code": "messlokationsadresse", "meaning": "x" },
1666 "adresse": { "ort": "Köln" } },
1667 { "partnerrolle": { "code": "Z07" }, "name1": "Beispiel" },
1668 ]
1669 });
1670 let errors = validate_pid_json(&json, &reqs);
1671 assert!(errors.is_empty(), "{}", ValidationReport(errors));
1672 }
1673
1674 #[test]
1675 fn multi_variant_entity_reports_variant_required_fields() {
1676 let reqs = multi_variant_requirements();
1677 let json = json!({ "geschaeftspartner": [{ "partnerrolle": "kundeMsb" }] });
1678 let errors = validate_pid_json(&json, &reqs);
1679 assert_eq!(errors.len(), 1, "{}", ValidationReport(errors));
1680 assert!(matches!(
1681 &errors[0],
1682 PidValidationError::MissingField { field, .. } if field == "name1"
1683 ));
1684 }
1685
1686 #[test]
1687 fn multi_variant_entity_unknown_qualifier_is_invalid_code_only() {
1688 let reqs = multi_variant_requirements();
1689 for bad in [json!("Z99"), json!("bogus"), json!({ "code": "Z99" })] {
1690 let json = json!({ "geschaeftspartner": [{ "partnerrolle": bad }] });
1691 let errors = validate_pid_json(&json, &reqs);
1692 assert_eq!(errors.len(), 1, "{bad}: {}", ValidationReport(errors));
1694 match &errors[0] {
1695 PidValidationError::InvalidCode {
1696 field,
1697 valid_values,
1698 ..
1699 } => {
1700 assert_eq!(field, "partnerrolle");
1701 let codes: Vec<&str> = valid_values.iter().map(|(c, _)| c.as_str()).collect();
1702 assert_eq!(codes, ["Z03", "Z07"]);
1703 }
1704 other => panic!("expected InvalidCode, got {other:?}"),
1705 }
1706 }
1707 }
1708
1709 #[test]
1710 fn code_objects_and_enum_mapped_names_are_code_checked() {
1711 let f = field("partnerrolle", "X", &[("Z07", "kundeMsb")]);
1712 assert_eq!(invalid_code_value(&json!("Z07"), &f), None);
1713 assert_eq!(invalid_code_value(&json!("kundeMsb"), &f), None);
1714 assert_eq!(invalid_code_value(&json!({ "code": "kundeMsb" }), &f), None);
1715 assert_eq!(invalid_code_value(&json!({ "code": "Z07" }), &f), None);
1716 assert_eq!(
1717 invalid_code_value(&json!({ "code": "Z99", "meaning": null }), &f),
1718 Some("Z99".to_string())
1719 );
1720 assert_eq!(
1721 invalid_code_value(&json!("kundeLf"), &f),
1722 Some("kundeLf".to_string())
1723 );
1724 assert_eq!(invalid_code_value(&json!(7), &f), None);
1726 assert_eq!(invalid_code_value(&json!({ "meaning": "x" }), &f), None);
1727 }
1728}