1use std::borrow::Cow;
4use std::collections::{BTreeMap, BTreeSet};
5use std::fmt;
6
7use serde_json::Value;
8
9use crate::pid_requirements::{
10 CodeValue, EntityGroupRequirement, EntityRequirement, EntityScope, EntityVariantRequirement,
11 FieldRequirement, KindSelector, NestedElementKind, NestedListRequirement, PidRequirements,
12 VariantQualifier,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum Severity {
18 Error,
20 Warning,
22}
23
24#[derive(Debug, Clone)]
26pub enum PidValidationError {
27 MissingEntity {
29 entity: String,
30 ahb_status: String,
31 severity: Severity,
32 },
33 MissingField {
35 entity: String,
36 field: String,
37 ahb_status: String,
38 rust_type: Option<String>,
39 valid_values: Vec<(String, String)>,
40 severity: Severity,
41 },
42 InvalidCode {
44 entity: String,
45 field: String,
46 value: String,
47 valid_values: Vec<(String, String)>,
48 },
49 NotAllowed {
55 entity: String,
56 field: String,
57 value: Option<String>,
58 ahb_status: String,
59 severity: Severity,
60 },
61}
62
63impl PidValidationError {
64 pub fn severity(&self) -> &Severity {
65 match self {
66 Self::MissingEntity { severity, .. } => severity,
67 Self::MissingField { severity, .. } => severity,
68 Self::NotAllowed { severity, .. } => severity,
69 Self::InvalidCode { .. } => &Severity::Error,
70 }
71 }
72
73 pub fn is_error(&self) -> bool {
74 matches!(self.severity(), Severity::Error)
75 }
76}
77
78impl fmt::Display for PidValidationError {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 PidValidationError::MissingEntity {
82 entity,
83 ahb_status,
84 severity,
85 } => {
86 let label = severity_label(severity);
87 write!(
88 f,
89 "{label}: missing entity '{entity}' (required: {ahb_status})"
90 )
91 }
92 PidValidationError::MissingField {
93 entity,
94 field,
95 ahb_status,
96 rust_type,
97 valid_values,
98 severity,
99 } => {
100 let label = severity_label(severity);
101 write!(
102 f,
103 "{label}: missing {entity}.{field} (required: {ahb_status})"
104 )?;
105 if let Some(rt) = rust_type {
106 write!(f, "\n → type: {rt}")?;
107 }
108 if !valid_values.is_empty() {
109 let codes: Vec<String> = valid_values
110 .iter()
111 .map(|(code, meaning)| {
112 if meaning.is_empty() {
113 code.clone()
114 } else {
115 format!("{code} ({meaning})")
116 }
117 })
118 .collect();
119 write!(f, "\n → valid: {}", codes.join(", "))?;
120 }
121 Ok(())
122 }
123 PidValidationError::InvalidCode {
124 entity,
125 field,
126 value,
127 valid_values,
128 } => {
129 write!(f, "INVALID: {entity}.{field} = \"{value}\"")?;
130 if !valid_values.is_empty() {
131 let codes: Vec<String> = valid_values.iter().map(|(c, _)| c.clone()).collect();
132 write!(f, "\n → valid: {}", codes.join(", "))?;
133 }
134 Ok(())
135 }
136 PidValidationError::NotAllowed {
137 entity,
138 field,
139 value,
140 ahb_status,
141 severity,
142 } => {
143 write!(f, "{}: {entity}", severity_label(severity))?;
144 if !field.is_empty() {
145 write!(f, ".{field}")?;
146 }
147 if let Some(value) = value {
148 write!(f, " = \"{value}\"")?;
149 }
150 write!(
151 f,
152 " present although its condition does not hold ({ahb_status})"
153 )
154 }
155 }
156 }
157}
158
159fn severity_label(severity: &Severity) -> &'static str {
160 match severity {
161 Severity::Error => "ERROR",
162 Severity::Warning => "WARNING",
163 }
164}
165
166pub struct ValidationReport(pub Vec<PidValidationError>);
168
169impl ValidationReport {
170 pub fn has_errors(&self) -> bool {
172 self.0.iter().any(|e| e.is_error())
173 }
174
175 pub fn errors(&self) -> Vec<&PidValidationError> {
177 self.0.iter().filter(|e| e.is_error()).collect()
178 }
179
180 pub fn is_empty(&self) -> bool {
182 self.0.is_empty()
183 }
184
185 pub fn len(&self) -> usize {
187 self.0.len()
188 }
189}
190
191impl fmt::Display for ValidationReport {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 for (i, err) in self.0.iter().enumerate() {
194 if i > 0 {
195 writeln!(f)?;
196 }
197 write!(f, "{err}")?;
198 }
199 Ok(())
200 }
201}
202
203pub fn validate_pid_json(json: &Value, requirements: &PidRequirements) -> Vec<PidValidationError> {
215 validate_entities(json, &requirements.entities, None)
216}
217
218pub fn validate_pid_json_transaction(
224 json: &Value,
225 requirements: &PidRequirements,
226) -> Vec<PidValidationError> {
227 validate_entities(json, &requirements.entities, Some(EntityScope::Transaction))
228}
229
230fn validate_entities(
232 json: &Value,
233 entities: &[EntityRequirement],
234 scope_filter: Option<EntityScope>,
235) -> Vec<PidValidationError> {
236 let mut errors = Vec::new();
237
238 for entity_req in entities {
239 if let Some(ref scope) = scope_filter {
241 if &entity_req.scope != scope {
242 continue;
243 }
244 }
245
246 let key = to_camel_case(&entity_req.entity);
247
248 match json.get(&key) {
249 None | Some(serde_json::Value::Null) => {
250 if is_unconditionally_required(&entity_req.ahb_status) {
251 errors.push(PidValidationError::MissingEntity {
252 entity: entity_req.entity.clone(),
253 ahb_status: entity_req.ahb_status.clone(),
254 severity: Severity::Error,
255 });
256 }
257 }
258 Some(val) => {
259 if let Some(arr) = val.as_array() {
262 for element in arr {
263 validate_entity_fields(element, entity_req, &mut errors);
264 }
265 } else {
266 validate_entity_fields(val, entity_req, &mut errors);
267 }
268 }
269 }
270 }
271
272 errors
273}
274
275pub fn get_nested<'a>(json: &'a Value, path: &str) -> Option<&'a Value> {
278 if let Some((list, rest)) = path.split_once("[].") {
281 return get_nested(json, list)?
282 .as_array()?
283 .iter()
284 .find_map(|item| get_nested(item, rest).filter(|v| !v.is_null()));
285 }
286 let mut current = json;
287 for part in path.split('.') {
288 current = current.get(part).or_else(|| {
289 if part.contains('_') {
290 current.get(snake_to_camel_case(part))
291 } else {
292 None
293 }
294 })?;
295 }
296 Some(current)
297}
298
299fn validate_entity_fields(
301 entity_json: &Value,
302 entity_req: &EntityRequirement,
303 errors: &mut Vec<PidValidationError>,
304) {
305 let fields = effective_field_requirements(entity_req, entity_json);
306 let exempt =
307 fields_of_unrequired_absent_groups(entity_req, entity_json, is_unconditionally_required);
308 for field_req in fields.iter() {
309 let val = get_nested(entity_json, &field_req.bo4e_name);
312
313 let val = val.filter(|v| !v.is_null());
315
316 match val {
317 None if exempt.contains(field_req.bo4e_name.as_str()) => {}
318 None => {
319 if is_unconditionally_required(&field_req.ahb_status) {
320 errors.push(PidValidationError::MissingField {
321 entity: entity_req.entity.clone(),
322 field: field_req.bo4e_name.clone(),
323 ahb_status: field_req.ahb_status.clone(),
324 rust_type: field_req.enum_name.clone(),
325 valid_values: code_values_to_tuples(&field_req.valid_codes),
326 severity: Severity::Error,
327 });
328 }
329 }
330 Some(val) => {
331 validate_code_value(val, entity_req, field_req, errors);
332 }
333 }
334 }
335 validate_nested_lists(
336 &entity_req.entity,
337 entity_json,
338 &entity_req.nested,
339 "",
340 &|status, _| is_unconditionally_required(status).then_some(Severity::Error),
341 &|_| None,
342 &|_, _| false,
344 errors,
345 );
346}
347
348pub fn validate_nested_lists(
358 entity: &str,
359 element: &Value,
360 lists: &[NestedListRequirement],
361 prefix: &str,
362 required: &dyn Fn(&str, &[(String, usize)]) -> Option<Severity>,
363 variant_holds: &dyn Fn(&VariantQualifier) -> Option<bool>,
364 forbidden: &dyn Fn(&str, &[(String, usize)]) -> bool,
365 errors: &mut Vec<PidValidationError>,
366) {
367 nested_lists_at(
368 entity,
369 element,
370 lists,
371 prefix,
372 &mut Vec::new(),
373 required,
374 variant_holds,
375 forbidden,
376 errors,
377 );
378}
379
380#[allow(clippy::too_many_arguments)]
382fn nested_lists_at(
383 entity: &str,
384 element: &Value,
385 lists: &[NestedListRequirement],
386 prefix: &str,
387 position: &mut Vec<(String, usize)>,
388 required: &dyn Fn(&str, &[(String, usize)]) -> Option<Severity>,
389 variant_holds: &dyn Fn(&VariantQualifier) -> Option<bool>,
390 forbidden: &dyn Fn(&str, &[(String, usize)]) -> bool,
391 errors: &mut Vec<PidValidationError>,
392) {
393 let applies = |kind: &NestedElementKind| {
396 kind.transaction_variant
397 .as_ref()
398 .map_or(Some(true), variant_holds)
399 };
400 let mut fields: Vec<&str> = lists.iter().map(|l| l.field.as_str()).collect();
403 fields.sort_unstable();
404 fields.dedup();
405 for field in fields {
406 let candidates: Vec<&NestedListRequirement> = lists
409 .iter()
410 .filter(|l| l.field == field)
411 .filter(|l| {
412 l.parent_selector
413 .iter()
414 .all(|selector| selector_matches(selector, element))
415 })
416 .collect();
417 if candidates.is_empty() {
418 continue;
419 }
420 let items: &[Value] = get_nested(element, field)
421 .and_then(|v| v.as_array())
422 .map_or(&[], |a| a.as_slice());
423 let label = format!("{prefix}{field}");
424 let alone = candidates.len() == 1;
425
426 let chosen = match &candidates[..] {
430 [only] => Some(*only),
431 _ if items.is_empty() => None,
432 _ => {
433 let fitting: Vec<&&NestedListRequirement> = candidates
434 .iter()
435 .filter(|l| items.iter().all(|item| kind_of(l, item, false).is_some()))
436 .collect();
437 match fitting[..] {
438 [one] => Some(*one),
439 _ => None,
440 }
441 }
442 };
443
444 if let Some(list) = chosen {
445 for kind in &list.kinds {
446 if applies(kind) != Some(true) {
447 continue;
448 }
449 let present = if alone && list.kinds.len() == 1 {
450 !items.is_empty()
451 } else {
452 items.iter().any(|item| kind_matches(kind, item))
453 };
454 if present {
455 continue;
456 }
457 if let Some(severity) = required(&kind.ahb_status, position) {
458 let codes = kind.selector.first().map_or(&[][..], |s| &s.codes[..]);
459 let name = if alone && list.kinds.len() == 1 {
462 String::new()
463 } else {
464 codes
465 .first()
466 .map(|c| c.bo4e_value.clone().unwrap_or_else(|| c.code.clone()))
467 .unwrap_or_default()
468 };
469 errors.push(PidValidationError::MissingField {
470 entity: entity.to_string(),
471 field: format!("{label}[{name}]"),
472 ahb_status: kind.ahb_status.clone(),
473 rust_type: None,
474 valid_values: code_values_to_tuples(codes),
475 severity,
476 });
477 }
478 }
479 }
480
481 for (index, item) in items.iter().enumerate() {
482 position.push((field.to_string(), index));
483 let here = position.clone();
484 let required_here = |status: &str| required(status, &here);
485 let matching: Vec<(&NestedListRequirement, &NestedElementKind)> = match chosen {
486 Some(list) => kind_of(list, item, alone)
487 .filter(|k| applies(k) != Some(false))
488 .map(|k| (list, k))
489 .into_iter()
490 .collect(),
491 None => candidates
492 .iter()
493 .flat_map(|l| {
494 l.kinds
495 .iter()
496 .filter(|k| applies(k) != Some(false))
497 .filter(|k| kind_matches(k, item))
498 .map(move |k| (*l, k))
499 })
500 .collect(),
501 };
502 let best = matching
506 .iter()
507 .map(|(l, k)| {
508 (
509 *l,
510 *k,
511 element_findings(entity, &label, k, item, &required_here),
512 )
513 })
514 .min_by_key(|(_, k, f)| (f.len(), k.selector.is_empty()));
515 if let Some((list, kind, findings)) = best {
516 errors.extend(findings);
517 let forbidden_here = |status: &str| forbidden(status, &here);
520 if forbidden_here(&kind.ahb_status) {
521 let name = kind
522 .selector
523 .first()
524 .and_then(|s| s.codes.first())
525 .map(|c| c.bo4e_value.clone().unwrap_or_else(|| c.code.clone()))
526 .unwrap_or_default();
527 errors.push(PidValidationError::NotAllowed {
528 entity: entity.to_string(),
529 field: format!("{label}[{name}]"),
530 value: None,
531 ahb_status: kind.ahb_status.clone(),
532 severity: Severity::Warning,
533 });
534 } else {
535 errors.extend(not_allowed_codes(
536 entity,
537 &format!("{label}[]."),
538 &kind.fields,
539 item,
540 &forbidden_here,
541 ));
542 }
543 nested_lists_at(
544 entity,
545 item,
546 &list.nested,
547 &format!("{label}[]."),
548 position,
549 required,
550 variant_holds,
551 forbidden,
552 errors,
553 );
554 }
555 position.pop();
556 }
557 }
558}
559
560fn kind_of<'l>(
564 list: &'l NestedListRequirement,
565 item: &Value,
566 alone: bool,
567) -> Option<&'l NestedElementKind> {
568 match &list.kinds[..] {
569 [only] if alone => Some(only),
570 kinds => kinds.iter().find(|k| kind_matches(k, item)),
571 }
572}
573
574fn kind_matches(kind: &NestedElementKind, item: &Value) -> bool {
577 kind.selector
578 .iter()
579 .all(|selector| selector_matches(selector, item))
580}
581
582fn selector_matches(selector: &KindSelector, item: &Value) -> bool {
584 get_nested(item, &selector.field)
585 .and_then(code_field_value)
586 .is_some_and(|value| {
587 selector
588 .codes
589 .iter()
590 .any(|c| c.code == value || c.bo4e_value.as_deref() == Some(value))
591 })
592}
593
594pub fn not_allowed_codes(
602 entity: &str,
603 label: &str,
604 fields: &[FieldRequirement],
605 element: &Value,
606 forbidden: &dyn Fn(&str) -> bool,
607) -> Vec<PidValidationError> {
608 let mut out = Vec::new();
609 for field_req in fields {
610 let values = all_nested(element, &field_req.bo4e_name);
611 if values.is_empty() {
612 continue;
613 }
614 let name = format!("{label}{}", field_req.bo4e_name);
615 for value in values {
616 let Some(code) = code_field_value(value) else {
617 continue;
618 };
619 let status = field_req
620 .valid_codes
621 .iter()
622 .find(|cv| cv.code == code || cv.bo4e_value.as_deref() == Some(code))
623 .and_then(|cv| cv.ahb_status.as_deref());
624 if let Some(status) = status.filter(|s| forbidden(s)) {
625 out.push(PidValidationError::NotAllowed {
626 entity: entity.to_string(),
627 field: name.clone(),
628 value: Some(code.to_string()),
629 ahb_status: status.to_string(),
630 severity: Severity::Warning,
631 });
632 }
633 }
634 }
635 out
636}
637
638fn element_findings(
641 entity: &str,
642 label: &str,
643 kind: &NestedElementKind,
644 item: &Value,
645 required: &dyn Fn(&str) -> Option<Severity>,
646) -> Vec<PidValidationError> {
647 let mut out = Vec::new();
648 for field_req in &kind.fields {
649 let name = format!("{label}[].{}", field_req.bo4e_name);
650 let values = all_nested(item, &field_req.bo4e_name);
651 if values.is_empty() {
652 if let Some(severity) = required(&field_req.ahb_status) {
653 out.push(PidValidationError::MissingField {
654 entity: entity.to_string(),
655 field: name,
656 ahb_status: field_req.ahb_status.clone(),
657 rust_type: field_req.enum_name.clone(),
658 valid_values: code_values_to_tuples(&field_req.valid_codes),
659 severity,
660 });
661 }
662 continue;
663 }
664 for value in values {
665 if let Some(value) = invalid_code_value(value, field_req) {
666 out.push(PidValidationError::InvalidCode {
667 entity: entity.to_string(),
668 field: name.clone(),
669 value,
670 valid_values: code_values_to_tuples(&field_req.valid_codes),
671 });
672 }
673 }
674 }
675 out
676}
677
678fn all_nested<'a>(json: &'a Value, path: &str) -> Vec<&'a Value> {
681 if let Some((list, rest)) = path.split_once("[].") {
682 return get_nested(json, list)
683 .and_then(|v| v.as_array())
684 .map(|items| {
685 items
686 .iter()
687 .flat_map(|item| all_nested(item, rest))
688 .collect()
689 })
690 .unwrap_or_default();
691 }
692 get_nested(json, path)
693 .filter(|v| !v.is_null())
694 .into_iter()
695 .collect()
696}
697
698fn validate_code_value(
700 val: &Value,
701 entity_req: &EntityRequirement,
702 field_req: &FieldRequirement,
703 errors: &mut Vec<PidValidationError>,
704) {
705 if let Some(value) = invalid_code_value(val, field_req) {
706 errors.push(PidValidationError::InvalidCode {
707 entity: entity_req.entity.clone(),
708 field: field_req.bo4e_name.clone(),
709 value,
710 valid_values: code_values_to_tuples(&field_req.valid_codes),
711 });
712 }
713}
714
715pub fn code_field_value(val: &Value) -> Option<&str> {
718 val.as_str()
719 .or_else(|| val.get("code").and_then(|c| c.as_str()))
720}
721
722pub fn invalid_code_value(val: &Value, field_req: &FieldRequirement) -> Option<String> {
729 if field_req.valid_codes.is_empty() {
730 return None;
731 }
732 let value = code_field_value(val)?;
733 let is_valid = field_req
734 .valid_codes
735 .iter()
736 .any(|cv| cv.code == value || cv.bo4e_value.as_deref() == Some(value));
737 (!is_valid).then(|| value.to_string())
738}
739
740pub fn variant_statuses<'a>(entity_req: &'a EntityRequirement, element: &Value) -> Vec<&'a str> {
744 let mut out: Vec<&'a str> = Vec::new();
745 for v in &entity_req.variants {
746 if v.ahb_status.is_empty() {
747 continue;
748 }
749 let value = get_nested(element, &v.discriminator_field).and_then(code_field_value);
750 if value.is_some_and(|s| v.code == s || v.bo4e_value.as_deref() == Some(s))
751 && !out.contains(&v.ahb_status.as_str())
752 {
753 out.push(v.ahb_status.as_str());
754 }
755 }
756 out
757}
758
759pub fn effective_field_requirements<'a>(
774 entity_req: &'a EntityRequirement,
775 element: &Value,
776) -> Cow<'a, [FieldRequirement]> {
777 if entity_req.variants.is_empty() {
778 return Cow::Borrowed(&entity_req.fields);
779 }
780
781 let mut by_field: BTreeMap<&str, Vec<&EntityVariantRequirement>> = BTreeMap::new();
783 for v in &entity_req.variants {
784 by_field
785 .entry(v.discriminator_field.as_str())
786 .or_default()
787 .push(v);
788 }
789 let mut candidates: Vec<&EntityVariantRequirement> = Vec::new();
790 for (field, group) in by_field {
791 let value = get_nested(element, field).and_then(code_field_value);
792 let matched: Vec<&EntityVariantRequirement> = group
793 .iter()
794 .copied()
795 .filter(|v| value.is_some_and(|s| v.code == s || v.bo4e_value.as_deref() == Some(s)))
796 .collect();
797 candidates.extend(if matched.is_empty() { group } else { matched });
798 }
799
800 let mut owned: Vec<&str> = Vec::new();
802 let mut owned_set: BTreeSet<&str> = BTreeSet::new();
803 for v in &entity_req.variants {
804 for f in &v.fields {
805 if owned_set.insert(f.bo4e_name.as_str()) {
806 owned.push(f.bo4e_name.as_str());
807 }
808 }
809 }
810
811 let mut combined: BTreeMap<&str, FieldRequirement> = BTreeMap::new();
812 for name in owned {
813 let reqs: Vec<&FieldRequirement> = candidates
814 .iter()
815 .filter_map(|v| v.fields.iter().find(|f| f.bo4e_name == name))
816 .collect();
817 let Some((first, rest)) = reqs.split_first() else {
818 continue; };
820 let mut field = (*first).clone();
821 let mut statuses_agree = reqs.len() == candidates.len();
822 for r in rest {
823 if r.ahb_status != field.ahb_status {
824 statuses_agree = false;
825 }
826 for cv in &r.valid_codes {
827 if !field.valid_codes.iter().any(|c| c.code == cv.code) {
828 field.valid_codes.push(cv.clone());
829 }
830 }
831 }
832 if !statuses_agree {
833 field.ahb_status = String::new();
834 }
835 combined.insert(name, field);
836 }
837
838 let mut result: Vec<FieldRequirement> = Vec::with_capacity(entity_req.fields.len());
839 for f in &entity_req.fields {
840 if owned_set.contains(f.bo4e_name.as_str()) {
841 if let Some(c) = combined.remove(f.bo4e_name.as_str()) {
842 result.push(c);
843 }
844 } else {
845 result.push(f.clone());
846 }
847 }
848 result.extend(combined.into_values());
849 Cow::Owned(result)
850}
851
852pub fn absent_groups<'a>(
855 entity_req: &'a EntityRequirement,
856 element: &'a Value,
857) -> impl Iterator<Item = &'a EntityGroupRequirement> + 'a {
858 entity_req.groups.iter().filter(move |g| {
859 !g.fields
860 .iter()
861 .any(|f| get_nested(element, f).is_some_and(|v| !v.is_null()))
862 })
863}
864
865pub fn fields_of_unrequired_absent_groups<'a>(
874 entity_req: &'a EntityRequirement,
875 element: &'a Value,
876 mut group_required: impl FnMut(&str) -> bool,
877) -> BTreeSet<&'a str> {
878 let exempt: Vec<&EntityGroupRequirement> = absent_groups(entity_req, element)
879 .filter(|g| !group_required(&g.ahb_status))
880 .collect();
881 let mut fields = BTreeSet::new();
882 for g in &exempt {
883 for f in &g.fields {
884 let kept_elsewhere = entity_req.groups.iter().any(|other| {
887 other.fields.contains(f) && !exempt.iter().any(|e| std::ptr::eq(*e, other))
888 });
889 if !kept_elsewhere {
890 fields.insert(f.as_str());
891 }
892 }
893 }
894 fields
895}
896
897fn code_values_to_tuples(codes: &[CodeValue]) -> Vec<(String, String)> {
899 codes
900 .iter()
901 .map(|cv| (cv.code.clone(), cv.meaning.clone()))
902 .collect()
903}
904
905fn to_camel_case(s: &str) -> String {
911 if s.is_empty() {
912 return String::new();
913 }
914 let mut chars = s.chars();
915 let first = chars.next().unwrap();
916 let mut result = first.to_lowercase().to_string();
917 result.extend(chars);
918 result
919}
920
921fn snake_to_camel_case(s: &str) -> String {
932 let mut result = String::with_capacity(s.len());
933 let mut capitalize_next = false;
934 for ch in s.chars() {
935 if ch == '_' {
936 capitalize_next = true;
937 } else if capitalize_next {
938 result.extend(ch.to_uppercase());
939 capitalize_next = false;
940 } else {
941 result.push(ch);
942 }
943 }
944 result
945}
946
947fn is_unconditionally_required(ahb_status: &str) -> bool {
949 matches!(ahb_status, "X" | "Muss" | "Soll")
950}
951
952#[cfg(test)]
953mod tests {
954 use super::*;
955
956 #[test]
960 fn a_variant_bound_kind_is_demanded_only_in_its_variant() {
961 use crate::pid_requirements::{NestedElementKind, NestedListRequirement, VariantQualifier};
962 let lists = vec![NestedListRequirement {
963 field: "zuordnungen".to_string(),
964 bo4e_type: "Zuordnung".to_string(),
965 source_path: "sg4.sg8_z22.sg10".to_string(),
966 kinds: vec![NestedElementKind {
967 ahb_status: "Muss".to_string(),
968 selector: vec![],
969 fields: vec![],
970 transaction_variant: Some(VariantQualifier {
971 group: "SG4".to_string(),
972 segment: "IDE".to_string(),
973 codes: vec!["24".to_string()],
974 }),
975 }],
976 parent_selector: vec![],
977 nested: vec![],
978 }];
979 let demanded = |holds: Option<bool>| {
980 let mut errors = Vec::new();
981 validate_nested_lists(
982 "SummenzeitreihenDaten",
983 &serde_json::json!({}),
984 &lists,
985 "",
986 &|_, _| Some(Severity::Error),
987 &|_| holds,
988 &|_, _| false,
989 &mut errors,
990 );
991 errors.len()
992 };
993 assert_eq!(demanded(Some(true)), 1);
994 assert_eq!(demanded(Some(false)), 0);
995 assert_eq!(demanded(None), 0);
996 }
997 use crate::pid_requirements::{
998 Bo4eRefType, Cardinality, CodeValue, EntityRequirement, FieldRequirement, PidRequirements,
999 };
1000 use serde_json::json;
1001
1002 fn sample_requirements() -> PidRequirements {
1003 PidRequirements {
1004 transaktion_wiederholbar: false,
1005 pid: "55001".to_string(),
1006 beschreibung: "Anmeldung verb. MaLo".to_string(),
1007 entities: vec![
1008 EntityRequirement {
1009 entity: "Prozessdaten".to_string(),
1010 ref_type: Bo4eRefType::Object {
1011 type_name: "Prozessdaten".to_string(),
1012
1013 cardinality: Cardinality::REQUIRED,
1014 },
1015
1016 ahb_status: "Muss".to_string(),
1017 map_key: None,
1018 scope: EntityScope::Transaction,
1019 variants: vec![],
1020 groups: vec![],
1021 fields: vec![
1022 FieldRequirement {
1023 bo4e_name: "vorgangId".to_string(),
1024 ahb_status: "X".to_string(),
1025 field_type: "data".to_string(),
1026 format: None,
1027 enum_name: None,
1028 valid_codes: vec![],
1029 ref_type: Bo4eRefType::Unknown,
1030 },
1031 FieldRequirement {
1032 bo4e_name: "transaktionsgrund".to_string(),
1033 ahb_status: "X".to_string(),
1034 field_type: "code".to_string(),
1035 format: None,
1036 enum_name: Some("Transaktionsgrund".to_string()),
1037 valid_codes: vec![
1038 CodeValue {
1039 code: "E01".to_string(),
1040 meaning: "Ein-/Auszug (Einzug)".to_string(),
1041 enum_name: None,
1042 bo4e_value: None,
1043 ahb_status: None,
1044 },
1045 CodeValue {
1046 code: "E03".to_string(),
1047 meaning: "Wechsel".to_string(),
1048 enum_name: None,
1049 bo4e_value: None,
1050 ahb_status: None,
1051 },
1052 ],
1053 ref_type: Bo4eRefType::Unknown,
1054 },
1055 ],
1056 nested: Vec::new(),
1057 },
1058 EntityRequirement {
1059 entity: "Marktlokation".to_string(),
1060 ref_type: Bo4eRefType::Object {
1061 type_name: "Marktlokation".to_string(),
1062
1063 cardinality: Cardinality::REQUIRED,
1064 },
1065
1066 ahb_status: "Muss".to_string(),
1067 map_key: None,
1068 scope: EntityScope::Transaction,
1069 variants: vec![],
1070 groups: vec![],
1071 fields: vec![
1072 FieldRequirement {
1073 bo4e_name: "marktlokationsId".to_string(),
1074 ahb_status: "X".to_string(),
1075 field_type: "data".to_string(),
1076 format: None,
1077 enum_name: None,
1078 valid_codes: vec![],
1079 ref_type: Bo4eRefType::Unknown,
1080 },
1081 FieldRequirement {
1082 bo4e_name: "haushaltskunde".to_string(),
1083 ahb_status: "X".to_string(),
1084 field_type: "code".to_string(),
1085 format: None,
1086 enum_name: Some("Haushaltskunde".to_string()),
1087 valid_codes: vec![
1088 CodeValue {
1089 code: "Z15".to_string(),
1090 meaning: "Ja".to_string(),
1091 enum_name: None,
1092 bo4e_value: None,
1093 ahb_status: None,
1094 },
1095 CodeValue {
1096 code: "Z18".to_string(),
1097 meaning: "Nein".to_string(),
1098 enum_name: None,
1099 bo4e_value: None,
1100 ahb_status: None,
1101 },
1102 ],
1103 ref_type: Bo4eRefType::Unknown,
1104 },
1105 ],
1106 nested: Vec::new(),
1107 },
1108 EntityRequirement {
1109 entity: "Geschaeftspartner".to_string(),
1110 ref_type: Bo4eRefType::Object {
1111 type_name: "Geschaeftspartner".to_string(),
1112
1113 cardinality: Cardinality {
1114 min: 1,
1115 max: Some(7),
1116 },
1117 },
1118
1119 ahb_status: "Muss".to_string(),
1120 map_key: None,
1121 scope: EntityScope::Transaction,
1122 variants: vec![],
1123 groups: vec![],
1124 fields: vec![FieldRequirement {
1125 bo4e_name: "identifikation".to_string(),
1126 ahb_status: "X".to_string(),
1127 field_type: "data".to_string(),
1128 format: None,
1129 enum_name: None,
1130 valid_codes: vec![],
1131 ref_type: Bo4eRefType::Unknown,
1132 }],
1133 nested: Vec::new(),
1134 },
1135 ],
1136 }
1137 }
1138
1139 #[test]
1140 fn test_validate_complete_json() {
1141 let reqs = sample_requirements();
1142 let json = json!({
1143 "prozessdaten": {
1144 "vorgangId": "ABC123",
1145 "transaktionsgrund": "E01"
1146 },
1147 "marktlokation": {
1148 "marktlokationsId": "51234567890",
1149 "haushaltskunde": "Z15"
1150 },
1151 "geschaeftspartner": [
1152 { "identifikation": "9900000000003" }
1153 ]
1154 });
1155
1156 let errors = validate_pid_json(&json, &reqs);
1157 assert!(errors.is_empty(), "Expected no errors, got: {errors:?}");
1158 }
1159
1160 #[test]
1161 fn test_validate_missing_entity() {
1162 let reqs = sample_requirements();
1163 let json = json!({
1164 "prozessdaten": {
1165 "vorgangId": "ABC123",
1166 "transaktionsgrund": "E01"
1167 },
1168 "geschaeftspartner": [
1169 { "identifikation": "9900000000003" }
1170 ]
1171 });
1172 let errors = validate_pid_json(&json, &reqs);
1175 assert_eq!(errors.len(), 1);
1176 match &errors[0] {
1177 PidValidationError::MissingEntity {
1178 entity,
1179 ahb_status,
1180 severity,
1181 } => {
1182 assert_eq!(entity, "Marktlokation");
1183 assert_eq!(ahb_status, "Muss");
1184 assert_eq!(severity, &Severity::Error);
1185 }
1186 other => panic!("Expected MissingEntity, got: {other:?}"),
1187 }
1188
1189 let msg = errors[0].to_string();
1191 assert!(msg.contains("ERROR"));
1192 assert!(msg.contains("Marktlokation"));
1193 assert!(msg.contains("Muss"));
1194 }
1195
1196 #[test]
1197 fn test_validate_missing_field() {
1198 let reqs = sample_requirements();
1199 let json = json!({
1200 "prozessdaten": {
1201 "transaktionsgrund": "E01"
1202 },
1204 "marktlokation": {
1205 "marktlokationsId": "51234567890",
1206 "haushaltskunde": "Z15"
1207 },
1208 "geschaeftspartner": [
1209 { "identifikation": "9900000000003" }
1210 ]
1211 });
1212
1213 let errors = validate_pid_json(&json, &reqs);
1214 assert_eq!(errors.len(), 1);
1215 match &errors[0] {
1216 PidValidationError::MissingField {
1217 entity,
1218 field,
1219 ahb_status,
1220 severity,
1221 ..
1222 } => {
1223 assert_eq!(entity, "Prozessdaten");
1224 assert_eq!(field, "vorgangId");
1225 assert_eq!(ahb_status, "X");
1226 assert_eq!(severity, &Severity::Error);
1227 }
1228 other => panic!("Expected MissingField, got: {other:?}"),
1229 }
1230
1231 let msg = errors[0].to_string();
1232 assert!(msg.contains("ERROR"));
1233 assert!(msg.contains("Prozessdaten.vorgangId"));
1234 }
1235
1236 #[test]
1237 fn test_validate_invalid_code() {
1238 let reqs = sample_requirements();
1239 let json = json!({
1240 "prozessdaten": {
1241 "vorgangId": "ABC123",
1242 "transaktionsgrund": "E01"
1243 },
1244 "marktlokation": {
1245 "marktlokationsId": "51234567890",
1246 "haushaltskunde": "Z99" },
1248 "geschaeftspartner": [
1249 { "identifikation": "9900000000003" }
1250 ]
1251 });
1252
1253 let errors = validate_pid_json(&json, &reqs);
1254 assert_eq!(errors.len(), 1);
1255 match &errors[0] {
1256 PidValidationError::InvalidCode {
1257 entity,
1258 field,
1259 value,
1260 valid_values,
1261 } => {
1262 assert_eq!(entity, "Marktlokation");
1263 assert_eq!(field, "haushaltskunde");
1264 assert_eq!(value, "Z99");
1265 assert_eq!(valid_values.len(), 2);
1266 assert!(valid_values.iter().any(|(c, _)| c == "Z15"));
1267 assert!(valid_values.iter().any(|(c, _)| c == "Z18"));
1268 }
1269 other => panic!("Expected InvalidCode, got: {other:?}"),
1270 }
1271
1272 let msg = errors[0].to_string();
1273 assert!(msg.contains("INVALID"));
1274 assert!(msg.contains("Z99"));
1275 assert!(msg.contains("Z15"));
1276 }
1277
1278 #[test]
1279 fn test_validate_array_entity() {
1280 let reqs = sample_requirements();
1281 let json = json!({
1282 "prozessdaten": {
1283 "vorgangId": "ABC123",
1284 "transaktionsgrund": "E01"
1285 },
1286 "marktlokation": {
1287 "marktlokationsId": "51234567890",
1288 "haushaltskunde": "Z15"
1289 },
1290 "geschaeftspartner": [
1291 { "identifikation": "9900000000003" },
1292 { } ]
1294 });
1295
1296 let errors = validate_pid_json(&json, &reqs);
1297 assert_eq!(errors.len(), 1);
1298 match &errors[0] {
1299 PidValidationError::MissingField { entity, field, .. } => {
1300 assert_eq!(entity, "Geschaeftspartner");
1301 assert_eq!(field, "identifikation");
1302 }
1303 other => panic!("Expected MissingField, got: {other:?}"),
1304 }
1305 }
1306
1307 #[test]
1308 fn test_to_camel_case() {
1309 assert_eq!(to_camel_case("Prozessdaten"), "prozessdaten");
1310 assert_eq!(
1311 to_camel_case("RuhendeMarktlokation"),
1312 "ruhendeMarktlokation"
1313 );
1314 assert_eq!(to_camel_case("Marktlokation"), "marktlokation");
1315 assert_eq!(to_camel_case(""), "");
1316 }
1317
1318 #[test]
1319 fn test_snake_to_camel_case() {
1320 assert_eq!(snake_to_camel_case("code_codepflege"), "codeCodepflege");
1321 assert_eq!(snake_to_camel_case("vorgang_id"), "vorgangId");
1322 assert_eq!(snake_to_camel_case("marktlokation"), "marktlokation");
1323 assert_eq!(snake_to_camel_case(""), "");
1324 assert_eq!(snake_to_camel_case("a_b_c"), "aBC");
1325 }
1326
1327 #[test]
1330 fn test_camel_case_fallback_for_snake_case_bo4e_name() {
1331 let reqs = PidRequirements {
1332 transaktion_wiederholbar: false,
1333 pid: "55077".to_string(),
1334 beschreibung: "Test camelCase fallback".to_string(),
1335 entities: vec![EntityRequirement {
1336 entity: "Zuordnung".to_string(),
1337 ref_type: Bo4eRefType::Object {
1338 type_name: "Zuordnung".to_string(),
1339
1340 cardinality: Cardinality::REQUIRED,
1341 },
1342
1343 ahb_status: "Muss".to_string(),
1344 map_key: None,
1345 scope: EntityScope::Transaction,
1346 variants: vec![],
1347 groups: vec![],
1348 fields: vec![
1349 FieldRequirement {
1350 bo4e_name: "code_codepflege".to_string(),
1352 ahb_status: "X".to_string(),
1353 field_type: "data".to_string(),
1354 format: None,
1355 enum_name: None,
1356 valid_codes: vec![],
1357 ref_type: Bo4eRefType::Unknown,
1358 },
1359 FieldRequirement {
1360 bo4e_name: "codeliste".to_string(),
1361 ahb_status: "X".to_string(),
1362 field_type: "data".to_string(),
1363 format: None,
1364 enum_name: None,
1365 valid_codes: vec![],
1366 ref_type: Bo4eRefType::Unknown,
1367 },
1368 ],
1369 nested: Vec::new(),
1370 }],
1371 };
1372
1373 let json_camel = json!({
1376 "zuordnung": {
1377 "codeCodepflege": "DE_BDEW",
1378 "codeliste": "6"
1379 }
1380 });
1381
1382 let errors = validate_pid_json(&json_camel, &reqs);
1383 assert!(
1384 errors.is_empty(),
1385 "Expected no errors when field is present under camelCase key, got: {errors:?}"
1386 );
1387
1388 let json_snake = json!({
1390 "zuordnung": {
1391 "code_codepflege": "DE_BDEW",
1392 "codeliste": "6"
1393 }
1394 });
1395
1396 let errors = validate_pid_json(&json_snake, &reqs);
1397 assert!(
1398 errors.is_empty(),
1399 "Expected no errors when field is present under snake_case key, got: {errors:?}"
1400 );
1401
1402 let json_missing = json!({
1404 "zuordnung": {
1405 "codeliste": "6"
1406 }
1407 });
1408
1409 let errors = validate_pid_json(&json_missing, &reqs);
1410 assert_eq!(errors.len(), 1);
1411 match &errors[0] {
1412 PidValidationError::MissingField { field, .. } => {
1413 assert_eq!(field, "code_codepflege");
1414 }
1415 other => panic!("Expected MissingField, got: {other:?}"),
1416 }
1417 }
1418
1419 #[test]
1420 fn test_is_unconditionally_required() {
1421 assert!(is_unconditionally_required("X"));
1422 assert!(is_unconditionally_required("Muss"));
1423 assert!(is_unconditionally_required("Soll"));
1424 assert!(!is_unconditionally_required("Kann"));
1425 assert!(!is_unconditionally_required("[1]"));
1426 assert!(!is_unconditionally_required(""));
1427 }
1428
1429 #[test]
1430 fn test_validation_report_display() {
1431 let errors = vec![
1432 PidValidationError::MissingEntity {
1433 entity: "Marktlokation".to_string(),
1434 ahb_status: "Muss".to_string(),
1435 severity: Severity::Error,
1436 },
1437 PidValidationError::MissingField {
1438 entity: "Prozessdaten".to_string(),
1439 field: "vorgangId".to_string(),
1440 ahb_status: "X".to_string(),
1441 rust_type: None,
1442 valid_values: vec![],
1443 severity: Severity::Error,
1444 },
1445 ];
1446 let report = ValidationReport(errors);
1447 assert!(report.has_errors());
1448 assert_eq!(report.len(), 2);
1449 assert!(!report.is_empty());
1450
1451 let display = report.to_string();
1452 assert!(display.contains("missing entity 'Marktlokation'"));
1453 assert!(display.contains("missing Prozessdaten.vorgangId"));
1454 }
1455
1456 #[test]
1457 fn test_missing_field_with_type_and_values_display() {
1458 let err = PidValidationError::MissingField {
1459 entity: "Marktlokation".to_string(),
1460 field: "haushaltskunde".to_string(),
1461 ahb_status: "Muss".to_string(),
1462 rust_type: Some("Haushaltskunde".to_string()),
1463 valid_values: vec![
1464 ("Z15".to_string(), "Ja".to_string()),
1465 ("Z18".to_string(), "Nein".to_string()),
1466 ],
1467 severity: Severity::Error,
1468 };
1469 let msg = err.to_string();
1470 assert!(msg.contains("type: Haushaltskunde"));
1471 assert!(msg.contains("valid: Z15 (Ja), Z18 (Nein)"));
1472 }
1473
1474 #[test]
1475 fn test_optional_fields_not_flagged() {
1476 let reqs = PidRequirements {
1477 transaktion_wiederholbar: false,
1478 pid: "99999".to_string(),
1479 beschreibung: "Test".to_string(),
1480 entities: vec![EntityRequirement {
1481 entity: "Test".to_string(),
1482 ref_type: Bo4eRefType::Object {
1483 type_name: "Test".to_string(),
1484
1485 cardinality: Cardinality::OPTIONAL,
1486 },
1487
1488 ahb_status: "Kann".to_string(),
1489 map_key: None,
1490 scope: EntityScope::Transaction,
1491 variants: vec![],
1492 groups: vec![],
1493 fields: vec![FieldRequirement {
1494 bo4e_name: "optionalField".to_string(),
1495 ahb_status: "Kann".to_string(),
1496 field_type: "data".to_string(),
1497 format: None,
1498 enum_name: None,
1499 valid_codes: vec![],
1500 ref_type: Bo4eRefType::Unknown,
1501 }],
1502 nested: Vec::new(),
1503 }],
1504 };
1505
1506 let errors = validate_pid_json(&json!({}), &reqs);
1508 assert!(errors.is_empty());
1509
1510 let errors = validate_pid_json(&json!({ "test": {} }), &reqs);
1512 assert!(errors.is_empty());
1513 }
1514
1515 #[test]
1518 fn test_nested_dot_path_fields_not_falsely_missing() {
1519 let reqs = PidRequirements {
1520 transaktion_wiederholbar: false,
1521 pid: "55001".to_string(),
1522 beschreibung: "Test nested paths".to_string(),
1523 entities: vec![EntityRequirement {
1524 entity: "ProduktpaketDaten".to_string(),
1525 ref_type: Bo4eRefType::Object {
1526 type_name: "ProduktpaketDaten".to_string(),
1527
1528 cardinality: Cardinality {
1529 min: 1,
1530 max: Some(99999),
1531 },
1532 },
1533
1534 ahb_status: "Muss".to_string(),
1535 map_key: None,
1536 scope: EntityScope::Transaction,
1537 variants: vec![],
1538 groups: vec![],
1539 fields: vec![
1540 FieldRequirement {
1541 bo4e_name: "produktIdentifikation.funktion".to_string(),
1542 ahb_status: "X".to_string(),
1543 field_type: "code".to_string(),
1544 format: None,
1545 enum_name: Some("Produktidentifikation".to_string()),
1546 valid_codes: vec![CodeValue {
1547 code: "5".to_string(),
1548 meaning: "Produktidentifikation".to_string(),
1549 enum_name: None,
1550 bo4e_value: None,
1551 ahb_status: None,
1552 }],
1553 ref_type: Bo4eRefType::Unknown,
1554 },
1555 FieldRequirement {
1556 bo4e_name: "produktMerkmal.code".to_string(),
1557 ahb_status: "X".to_string(),
1558 field_type: "code".to_string(),
1559 format: None,
1560 enum_name: None,
1561 valid_codes: vec![],
1562 ref_type: Bo4eRefType::Unknown,
1563 },
1564 ],
1565 nested: Vec::new(),
1566 }],
1567 };
1568
1569 let json = json!({
1571 "produktpaketDaten": [{
1572 "produktIdentifikation": { "funktion": "5", "id": "9991000002082", "typ": "Z11" },
1573 "produktMerkmal": { "code": "ZH9" }
1574 }]
1575 });
1576
1577 let errors = validate_pid_json(&json, &reqs);
1578 assert!(
1579 errors.is_empty(),
1580 "Nested dot-path fields should be found (issue #48), got: {errors:?}"
1581 );
1582 }
1583
1584 #[test]
1585 fn test_nested_dot_path_truly_missing() {
1586 let reqs = PidRequirements {
1587 transaktion_wiederholbar: false,
1588 pid: "55001".to_string(),
1589 beschreibung: "Test nested paths missing".to_string(),
1590 entities: vec![EntityRequirement {
1591 entity: "ProduktpaketDaten".to_string(),
1592 ref_type: Bo4eRefType::Object {
1593 type_name: "ProduktpaketDaten".to_string(),
1594
1595 cardinality: Cardinality {
1596 min: 1,
1597 max: Some(99999),
1598 },
1599 },
1600
1601 ahb_status: "Muss".to_string(),
1602 map_key: None,
1603 scope: EntityScope::Transaction,
1604 variants: vec![],
1605 groups: vec![],
1606 fields: vec![FieldRequirement {
1607 bo4e_name: "produktIdentifikation.funktion".to_string(),
1608 ahb_status: "X".to_string(),
1609 field_type: "data".to_string(),
1610 format: None,
1611 enum_name: None,
1612 valid_codes: vec![],
1613 ref_type: Bo4eRefType::Unknown,
1614 }],
1615 nested: Vec::new(),
1616 }],
1617 };
1618
1619 let json = json!({
1621 "produktpaketDaten": [{
1622 "produktIdentifikation": { "id": "123" }
1623 }]
1624 });
1625
1626 let errors = validate_pid_json(&json, &reqs);
1627 assert_eq!(errors.len(), 1, "Should report missing nested field");
1628 match &errors[0] {
1629 PidValidationError::MissingField { field, .. } => {
1630 assert_eq!(field, "produktIdentifikation.funktion");
1631 }
1632 other => panic!("Expected MissingField, got: {other:?}"),
1633 }
1634 }
1635
1636 fn field(name: &str, status: &str, codes: &[(&str, &str)]) -> FieldRequirement {
1637 FieldRequirement {
1638 bo4e_name: name.to_string(),
1639 ahb_status: status.to_string(),
1640 field_type: if codes.is_empty() { "data" } else { "code" }.to_string(),
1641 format: None,
1642 enum_name: None,
1643 valid_codes: codes
1644 .iter()
1645 .map(|(code, mapped)| CodeValue {
1646 code: code.to_string(),
1647 meaning: String::new(),
1648 enum_name: None,
1649 bo4e_value: Some(mapped.to_string()),
1650 ahb_status: None,
1651 })
1652 .collect(),
1653 ref_type: Bo4eRefType::Unknown,
1654 }
1655 }
1656
1657 fn multi_variant_requirements() -> PidRequirements {
1660 let z03 = ("Z03", "messlokationsadresse");
1661 let z07 = ("Z07", "kundeMsb");
1662 PidRequirements {
1663 transaktion_wiederholbar: false,
1664 pid: "55042".to_string(),
1665 beschreibung: String::new(),
1666 entities: vec![EntityRequirement {
1667 entity: "Geschaeftspartner".to_string(),
1668 ref_type: Bo4eRefType::Object {
1669 type_name: "Geschaeftspartner".to_string(),
1670 cardinality: Cardinality {
1671 min: 1,
1672 max: Some(99),
1673 },
1674 },
1675 ahb_status: "Muss".to_string(),
1676 fields: vec![
1678 field("adresse.ort", "X", &[]),
1679 field("name1", "X", &[]),
1680 field("partnerrolle", "X", &[z03, z07]),
1681 ],
1682 map_key: None,
1683 scope: EntityScope::Transaction,
1684 variants: vec![
1685 EntityVariantRequirement {
1686 discriminator_field: "partnerrolle".to_string(),
1687 code: "Z03".to_string(),
1688 bo4e_value: Some("messlokationsadresse".to_string()),
1689 source_paths: vec!["sg4.sg12_z03".to_string()],
1690 fields: vec![
1691 field("adresse.ort", "X", &[]),
1692 field("partnerrolle", "X", &[z03]),
1693 ],
1694 ahb_status: String::new(),
1695 },
1696 EntityVariantRequirement {
1697 discriminator_field: "partnerrolle".to_string(),
1698 code: "Z07".to_string(),
1699 bo4e_value: Some("kundeMsb".to_string()),
1700 source_paths: vec!["sg4.sg12_z07".to_string()],
1701 fields: vec![field("name1", "X", &[]), field("partnerrolle", "X", &[z07])],
1702 ahb_status: String::new(),
1703 },
1704 ],
1705 groups: vec![],
1706 nested: Vec::new(),
1707 }],
1708 }
1709 }
1710
1711 fn multi_group_requirements(z22_status: &str) -> PidRequirements {
1713 let group = |path: &str, status: &str, fields: &[&str]| EntityGroupRequirement {
1714 source_path: path.to_string(),
1715 ahb_status: status.to_string(),
1716 fields: fields.iter().map(|f| f.to_string()).collect(),
1717 };
1718 PidRequirements {
1719 transaktion_wiederholbar: false,
1720 pid: "55043".to_string(),
1721 beschreibung: String::new(),
1722 entities: vec![EntityRequirement {
1723 entity: "Marktlokation".to_string(),
1724 ref_type: Bo4eRefType::Object {
1725 type_name: "Marktlokation".to_string(),
1726 cardinality: Cardinality::REQUIRED,
1727 },
1728 ahb_status: "Muss".to_string(),
1729 fields: vec![
1730 field("marktlokationsId", "X", &[]),
1731 field("ruhendeMarktlokationsId", "X", &[]),
1732 field("ruhendeMarktlokationZeitraumId", "Kann", &[]),
1733 ],
1734 map_key: None,
1735 scope: EntityScope::Transaction,
1736 variants: vec![],
1737 groups: vec![
1738 group("sg4.sg5_z16", "Muss", &["marktlokationsId"]),
1739 group(
1740 "sg4.sg5_z22",
1741 z22_status,
1742 &["ruhendeMarktlokationsId", "ruhendeMarktlokationZeitraumId"],
1743 ),
1744 ],
1745 nested: Vec::new(),
1746 }],
1747 }
1748 }
1749
1750 fn missing_fields(errors: &[PidValidationError]) -> Vec<&str> {
1751 errors
1752 .iter()
1753 .filter_map(|e| match e {
1754 PidValidationError::MissingField { field, .. } => Some(field.as_str()),
1755 _ => None,
1756 })
1757 .collect()
1758 }
1759
1760 #[test]
1761 fn an_absent_optional_groups_fields_are_not_demanded() {
1762 let reqs = multi_group_requirements("Soll [2003]");
1763 let errors = validate_pid_json(&json!({ "marktlokation": {} }), &reqs);
1764 assert_eq!(missing_fields(&errors), vec!["marktlokationsId"]);
1766 }
1767
1768 #[test]
1769 fn a_filled_groups_fields_are_demanded() {
1770 let reqs = multi_group_requirements("Soll [2003]");
1771 let json = json!({ "marktlokation": {
1772 "marktlokationsId": "51238696781",
1773 "ruhendeMarktlokationZeitraumId": "1"
1774 }});
1775 let errors = validate_pid_json(&json, &reqs);
1776 assert_eq!(missing_fields(&errors), vec!["ruhendeMarktlokationsId"]);
1777 }
1778
1779 #[test]
1780 fn an_absent_required_groups_fields_are_demanded() {
1781 let reqs = multi_group_requirements("Muss");
1782 let json = json!({ "marktlokation": { "marktlokationsId": "51238696781" } });
1783 let errors = validate_pid_json(&json, &reqs);
1784 assert_eq!(missing_fields(&errors), vec!["ruhendeMarktlokationsId"]);
1785 }
1786
1787 #[test]
1788 fn multi_variant_entity_uses_the_elements_own_variant() {
1789 let reqs = multi_variant_requirements();
1790 let json = json!({
1792 "geschaeftspartner": [
1793 { "partnerrolle": "Z03", "adresse": { "ort": "Berlin" } },
1794 { "partnerrolle": "kundeMsb", "name1": "Muster" },
1795 { "partnerrolle": { "code": "messlokationsadresse", "meaning": "x" },
1796 "adresse": { "ort": "Köln" } },
1797 { "partnerrolle": { "code": "Z07" }, "name1": "Beispiel" },
1798 ]
1799 });
1800 let errors = validate_pid_json(&json, &reqs);
1801 assert!(errors.is_empty(), "{}", ValidationReport(errors));
1802 }
1803
1804 #[test]
1805 fn multi_variant_entity_reports_variant_required_fields() {
1806 let reqs = multi_variant_requirements();
1807 let json = json!({ "geschaeftspartner": [{ "partnerrolle": "kundeMsb" }] });
1808 let errors = validate_pid_json(&json, &reqs);
1809 assert_eq!(errors.len(), 1, "{}", ValidationReport(errors));
1810 assert!(matches!(
1811 &errors[0],
1812 PidValidationError::MissingField { field, .. } if field == "name1"
1813 ));
1814 }
1815
1816 #[test]
1817 fn multi_variant_entity_unknown_qualifier_is_invalid_code_only() {
1818 let reqs = multi_variant_requirements();
1819 for bad in [json!("Z99"), json!("bogus"), json!({ "code": "Z99" })] {
1820 let json = json!({ "geschaeftspartner": [{ "partnerrolle": bad }] });
1821 let errors = validate_pid_json(&json, &reqs);
1822 assert_eq!(errors.len(), 1, "{bad}: {}", ValidationReport(errors));
1824 match &errors[0] {
1825 PidValidationError::InvalidCode {
1826 field,
1827 valid_values,
1828 ..
1829 } => {
1830 assert_eq!(field, "partnerrolle");
1831 let codes: Vec<&str> = valid_values.iter().map(|(c, _)| c.as_str()).collect();
1832 assert_eq!(codes, ["Z03", "Z07"]);
1833 }
1834 other => panic!("expected InvalidCode, got {other:?}"),
1835 }
1836 }
1837 }
1838
1839 #[test]
1840 fn code_objects_and_enum_mapped_names_are_code_checked() {
1841 let f = field("partnerrolle", "X", &[("Z07", "kundeMsb")]);
1842 assert_eq!(invalid_code_value(&json!("Z07"), &f), None);
1843 assert_eq!(invalid_code_value(&json!("kundeMsb"), &f), None);
1844 assert_eq!(invalid_code_value(&json!({ "code": "kundeMsb" }), &f), None);
1845 assert_eq!(invalid_code_value(&json!({ "code": "Z07" }), &f), None);
1846 assert_eq!(
1847 invalid_code_value(&json!({ "code": "Z99", "meaning": null }), &f),
1848 Some("Z99".to_string())
1849 );
1850 assert_eq!(
1851 invalid_code_value(&json!("kundeLf"), &f),
1852 Some("kundeLf".to_string())
1853 );
1854 assert_eq!(invalid_code_value(&json!(7), &f), None);
1856 assert_eq!(invalid_code_value(&json!({ "meaning": "x" }), &f), None);
1857 }
1858}