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