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