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