1use serde_json::Value;
7use std::collections::{BTreeMap, HashMap, HashSet};
8use std::path::Path;
9
10pub type CodeLookupKey = (String, String, Option<String>, usize, usize);
20
21#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
23pub struct CodeEnrichment {
24 pub meaning: String,
25 pub enum_key: Option<String>,
26}
27
28pub type CodeMeanings = BTreeMap<String, CodeEnrichment>;
31
32#[derive(Debug, Clone, Default)]
41pub struct CodeLookup {
42 entries: BTreeMap<CodeLookupKey, CodeMeanings>,
43 variants: HashSet<(String, String, String)>,
48}
49
50impl serde::Serialize for CodeLookup {
55 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
56 use serde::ser::SerializeMap;
57 let mut entries: Vec<(String, &CodeMeanings)> = self
58 .entries
59 .iter()
60 .map(|((path, tag, qual, elem, comp), meanings)| {
61 let q = qual.as_deref().unwrap_or("");
62 (format!("{path}|{tag}|{q}|{elem}|{comp}"), meanings)
63 })
64 .collect();
65 entries.sort_by(|a, b| a.0.cmp(&b.0));
66 let mut map = serializer.serialize_map(Some(entries.len()))?;
67 for (key, meanings) in entries {
68 map.serialize_entry(&key, meanings)?;
69 }
70 map.end()
71 }
72}
73
74impl<'de> serde::Deserialize<'de> for CodeLookup {
75 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
76 let raw: HashMap<String, CodeMeanings> = HashMap::deserialize(deserializer)?;
77 let mut entries = BTreeMap::new();
78 for (key_str, meanings) in raw {
79 let parts: Vec<&str> = key_str.splitn(5, '|').collect();
80 if parts.len() == 5 {
81 let qual = if parts[2].is_empty() {
82 None
83 } else {
84 Some(parts[2].to_string())
85 };
86 let elem: usize = parts[3].parse().map_err(serde::de::Error::custom)?;
87 let comp: usize = parts[4].parse().map_err(serde::de::Error::custom)?;
88 entries.insert(
89 (parts[0].to_string(), parts[1].to_string(), qual, elem, comp),
90 meanings,
91 );
92 }
93 }
94 Ok(Self::from_entries(entries))
95 }
96}
97
98impl CodeLookup {
99 pub fn from_schema_file(path: &Path) -> Result<Self, std::io::Error> {
101 let content = std::fs::read_to_string(path)?;
102 let schema: Value = serde_json::from_str(&content)
103 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
104 Ok(Self::from_schema_value(&schema))
105 }
106
107 pub fn from_schema_value(schema: &Value) -> Self {
109 let mut entries = BTreeMap::new();
110 if let Some(fields) = schema.get("fields").and_then(|f| f.as_object()) {
111 for (group_key, group_value) in fields {
112 Self::walk_group(group_key, group_value, &mut entries);
113 }
114 }
115 if let Some(root_segments) = schema.get("root_segments").and_then(|s| s.as_array()) {
117 for segment in root_segments {
118 let seg_id = segment
119 .get("id")
120 .and_then(|v| v.as_str())
121 .unwrap_or("")
122 .to_uppercase();
123 Self::process_segment("", &seg_id, segment, &mut entries);
124 }
125 }
126 Self::from_entries(entries)
127 }
128
129 fn from_entries(entries: BTreeMap<CodeLookupKey, CodeMeanings>) -> Self {
130 let variants = entries
131 .keys()
132 .filter_map(|(path, tag, qual, _, _)| {
133 qual.as_ref()
134 .map(|q| (path.clone(), tag.clone(), q.clone()))
135 })
136 .collect();
137 Self { entries, variants }
138 }
139
140 pub fn enrichment_codes(
150 &self,
151 source_path: &str,
152 segment_tag: &str,
153 path_qualifier: Option<&str>,
154 disc_qualifier: Option<&str>,
155 element_index: usize,
156 component_index: usize,
157 ) -> Option<&CodeMeanings> {
158 let at = |q| self.resolve_q(source_path, segment_tag, q, element_index, component_index);
159 let path_variant =
160 path_qualifier.filter(|q| self.is_known_variant(source_path, segment_tag, q));
161 match (path_variant, disc_qualifier) {
162 (Some(p), Some(d)) if p != d => at(Some(p)),
163 (Some(p), None) => at(None).and(at(Some(p))),
164 _ => at(disc_qualifier),
165 }
166 }
167
168 pub fn field_codes(
173 &self,
174 source_path: &str,
175 segment_tag: &str,
176 path_qualifier: Option<&str>,
177 disc_qualifier: Option<&str>,
178 element_index: usize,
179 component_index: usize,
180 ) -> Option<CodeMeanings> {
181 match path_qualifier.or(disc_qualifier) {
182 Some(q) => self
183 .resolve_q(
184 source_path,
185 segment_tag,
186 Some(q),
187 element_index,
188 component_index,
189 )
190 .cloned(),
191 None => Some(self.codes_all_qualifiers(
192 source_path,
193 segment_tag,
194 element_index,
195 component_index,
196 ))
197 .filter(|c| !c.is_empty()),
198 }
199 }
200
201 pub fn is_known_variant(&self, source_path: &str, segment_tag: &str, qualifier: &str) -> bool {
204 self.variants.contains(&(
205 source_path.to_string(),
206 segment_tag.to_string(),
207 qualifier.to_string(),
208 ))
209 }
210
211 fn resolve_q(
217 &self,
218 source_path: &str,
219 segment_tag: &str,
220 qualifier: Option<&str>,
221 element_index: usize,
222 component_index: usize,
223 ) -> Option<&CodeMeanings> {
224 let key = |q: Option<&str>| {
225 (
226 source_path.to_string(),
227 segment_tag.to_string(),
228 q.map(String::from),
229 element_index,
230 component_index,
231 )
232 };
233 match qualifier {
234 Some(q) if self.is_known_variant(source_path, segment_tag, q) => {
235 self.entries.get(&key(Some(q)))
236 }
237 Some(q) => self
238 .entries
239 .get(&key(Some(q)))
240 .or_else(|| self.entries.get(&key(None))),
241 None => self.entries.get(&key(None)),
242 }
243 }
244
245 #[deprecated(
254 note = "use is_code_field_q with the discriminator qualifier; this shim scans across all qualifiers"
255 )]
256 pub fn is_code_field(
257 &self,
258 source_path: &str,
259 segment_tag: &str,
260 element_index: usize,
261 component_index: usize,
262 ) -> bool {
263 self.entries.iter().any(|((p, t, _q, e, c), _)| {
266 p == source_path && t == segment_tag && *e == element_index && *c == component_index
267 })
268 }
269
270 pub fn is_code_field_q(
279 &self,
280 source_path: &str,
281 segment_tag: &str,
282 qualifier: Option<&str>,
283 element_index: usize,
284 component_index: usize,
285 ) -> bool {
286 self.resolve_q(
287 source_path,
288 segment_tag,
289 qualifier,
290 element_index,
291 component_index,
292 )
293 .is_some()
294 }
295
296 pub fn codes_q(
302 &self,
303 source_path: &str,
304 segment_tag: &str,
305 qualifier: Option<&str>,
306 element_index: usize,
307 component_index: usize,
308 ) -> Option<&CodeMeanings> {
309 self.resolve_q(
310 source_path,
311 segment_tag,
312 qualifier,
313 element_index,
314 component_index,
315 )
316 }
317
318 pub fn codes_all_qualifiers(
322 &self,
323 source_path: &str,
324 segment_tag: &str,
325 element_index: usize,
326 component_index: usize,
327 ) -> CodeMeanings {
328 let mut slots: Vec<(&Option<String>, &CodeMeanings)> = self
330 .entries
331 .iter()
332 .filter(|((p, t, _, e, c), _)| {
333 p == source_path && t == segment_tag && *e == element_index && *c == component_index
334 })
335 .map(|((_, _, q, _, _), meanings)| (q, meanings))
336 .collect();
337 slots.sort_by(|a, b| a.0.cmp(b.0));
338 let mut merged = CodeMeanings::new();
339 for (_, meanings) in slots {
340 for (code, enrichment) in meanings {
341 merged
342 .entry(code.clone())
343 .or_insert_with(|| enrichment.clone());
344 }
345 }
346 merged
347 }
348
349 #[deprecated(
354 note = "use enrichment_for_q with the discriminator qualifier; this shim scans across all qualifiers"
355 )]
356 pub fn enrichment_for(
357 &self,
358 source_path: &str,
359 segment_tag: &str,
360 element_index: usize,
361 component_index: usize,
362 value: &str,
363 ) -> Option<&CodeEnrichment> {
364 let unqualified_key = (
366 source_path.to_string(),
367 segment_tag.to_string(),
368 None,
369 element_index,
370 component_index,
371 );
372 if let Some(e) = self
373 .entries
374 .get(&unqualified_key)
375 .and_then(|meanings| meanings.get(value))
376 {
377 return Some(e);
378 }
379 self.entries
380 .iter()
381 .filter(|((p, t, q, e, c), _)| {
382 p == source_path
383 && t == segment_tag
384 && q.is_some()
385 && *e == element_index
386 && *c == component_index
387 })
388 .find_map(|(_, meanings)| meanings.get(value))
389 }
390
391 pub fn enrichment_for_q(
394 &self,
395 source_path: &str,
396 segment_tag: &str,
397 qualifier: Option<&str>,
398 element_index: usize,
399 component_index: usize,
400 value: &str,
401 ) -> Option<&CodeEnrichment> {
402 self.resolve_q(
403 source_path,
404 segment_tag,
405 qualifier,
406 element_index,
407 component_index,
408 )
409 .and_then(|meanings| meanings.get(value))
410 }
411
412 #[deprecated(
419 note = "use enrichment_for_q with the discriminator qualifier; this shim scans across all qualifiers"
420 )]
421 pub fn meaning_for(
422 &self,
423 source_path: &str,
424 segment_tag: &str,
425 element_index: usize,
426 component_index: usize,
427 value: &str,
428 ) -> Option<&str> {
429 #[allow(deprecated)]
430 self.enrichment_for(
431 source_path,
432 segment_tag,
433 element_index,
434 component_index,
435 value,
436 )
437 .map(|e| e.meaning.as_str())
438 }
439
440 pub fn is_pid_self_reference(
446 &self,
447 source_path: &str,
448 segment_tag: &str,
449 qualifier: Option<&str>,
450 element_index: usize,
451 component_index: usize,
452 pid: &str,
453 ) -> bool {
454 let key = (
455 source_path.to_string(),
456 segment_tag.to_string(),
457 qualifier.map(String::from),
458 element_index,
459 component_index,
460 );
461 if let Some(meanings) = self.entries.get(&key) {
462 meanings.len() == 1 && meanings.contains_key(pid)
463 } else {
464 false
465 }
466 }
467
468 fn walk_group(
470 path_prefix: &str,
471 group: &Value,
472 entries: &mut BTreeMap<CodeLookupKey, CodeMeanings>,
473 ) {
474 if let Some(segments) = group.get("segments").and_then(|s| s.as_array()) {
475 for segment in segments {
476 let seg_id = segment
477 .get("id")
478 .and_then(|v| v.as_str())
479 .unwrap_or("")
480 .to_uppercase();
481 Self::process_segment(path_prefix, &seg_id, segment, entries);
482 }
483 }
484 if let Some(children) = group.get("children").and_then(|c| c.as_object()) {
485 for (child_key, child_value) in children {
486 let child_path = format!("{}.{}", path_prefix, child_key);
487 Self::walk_group(&child_path, child_value, entries);
488 }
489 Self::merge_variant_entries(path_prefix, children, entries);
493 }
494 }
495
496 fn process_segment(
503 source_path: &str,
504 segment_tag: &str,
505 segment: &Value,
506 entries: &mut BTreeMap<CodeLookupKey, CodeMeanings>,
507 ) {
508 let Some(elements) = segment.get("elements").and_then(|e| e.as_array()) else {
509 return;
510 };
511
512 let qualifier = Self::extract_qualifier(segment_tag, elements);
513 let own_variant = if qualifier.is_none() && !Self::has_qualifier_convention(segment_tag) {
516 Self::single_leading_code(elements)
517 } else {
518 None
519 };
520 let slots: Vec<Option<String>> = std::iter::once(qualifier)
521 .chain(own_variant.map(Some))
522 .collect();
523
524 for element in elements {
525 let element_index = element.get("index").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
526
527 if let Some("code") = element.get("type").and_then(|v| v.as_str()) {
529 if let Some(codes) = element.get("codes").and_then(|c| c.as_array()) {
530 let meanings = Self::extract_codes(codes);
531 if !meanings.is_empty() {
532 for slot in &slots {
533 let key = (
534 source_path.to_string(),
535 segment_tag.to_string(),
536 slot.clone(),
537 element_index,
538 0,
539 );
540 entries.entry(key).or_default().extend(meanings.clone());
541 }
542 }
543 }
544 }
545
546 if let Some(components) = element.get("components").and_then(|c| c.as_array()) {
548 for component in components {
549 if let Some("code") = component.get("type").and_then(|v| v.as_str()) {
550 let sub_index = component
551 .get("sub_index")
552 .and_then(|v| v.as_u64())
553 .unwrap_or(0) as usize;
554 if let Some(codes) = component.get("codes").and_then(|c| c.as_array()) {
555 let meanings = Self::extract_codes(codes);
556 if !meanings.is_empty() {
557 for slot in &slots {
558 let key = (
559 source_path.to_string(),
560 segment_tag.to_string(),
561 slot.clone(),
562 element_index,
563 sub_index,
564 );
565 entries.entry(key).or_default().extend(meanings.clone());
566 }
567 }
568 }
569 }
570 }
571 }
572 }
573 }
574
575 fn extract_qualifier(segment_tag: &str, elements: &[Value]) -> Option<String> {
590 if !Self::has_qualifier_convention(segment_tag) {
591 return None;
592 }
593 Self::single_leading_code(elements)
594 }
595
596 fn has_qualifier_convention(segment_tag: &str) -> bool {
598 matches!(segment_tag, "RFF" | "STS" | "CCI" | "DTM")
599 }
600
601 fn single_leading_code(elements: &[Value]) -> Option<String> {
604 let element0 = elements
606 .iter()
607 .find(|el| el.get("index").and_then(|v| v.as_u64()) == Some(0))
608 .or_else(|| elements.first())?;
609
610 let component0 = element0
612 .get("components")
613 .and_then(|c| c.as_array())
614 .and_then(|comps| {
615 comps
616 .iter()
617 .find(|c| c.get("sub_index").and_then(|v| v.as_u64()) == Some(0))
618 .or_else(|| comps.first())
619 });
620
621 let codes_node = if let Some(comp) = component0 {
622 if comp.get("type").and_then(|v| v.as_str()) == Some("code") {
624 comp.get("codes").and_then(|c| c.as_array())
625 } else {
626 None
627 }
628 } else if element0.get("type").and_then(|v| v.as_str()) == Some("code") {
629 element0.get("codes").and_then(|c| c.as_array())
631 } else {
632 None
633 };
634
635 let codes = codes_node?;
636 if codes.len() != 1 {
637 return None; }
639 codes[0]
640 .get("value")
641 .and_then(|v| v.as_str())
642 .map(|s| s.to_string())
643 }
644
645 fn merge_variant_entries(
652 path_prefix: &str,
653 children: &serde_json::Map<String, Value>,
654 entries: &mut BTreeMap<CodeLookupKey, CodeMeanings>,
655 ) {
656 let mut bases: HashMap<&str, Vec<&str>> = HashMap::new();
658 for child_key in children.keys() {
659 if let Some(underscore_pos) = child_key.find('_') {
660 let base = &child_key[..underscore_pos];
661 bases.entry(base).or_default().push(child_key);
662 }
663 }
664
665 for (base, variant_keys) in &bases {
666 if variant_keys.len() < 2 {
667 continue; }
669 let base_path = format!("{}.{}", path_prefix, base);
670 let mut merged: HashMap<(String, Option<String>, usize, usize), CodeMeanings> =
674 HashMap::new();
675 for variant_key in variant_keys {
676 let variant_path = format!("{}.{}", path_prefix, variant_key);
677 for (key, meanings) in entries.iter() {
678 if key.0 == variant_path {
679 let agg_key = (key.1.clone(), key.2.clone(), key.3, key.4);
680 let target = merged.entry(agg_key).or_default();
681 for (k, v) in meanings {
682 target.insert(k.clone(), v.clone());
683 }
684 }
685 }
686 }
687 for ((seg_tag, qual, elem_idx, comp_idx), meanings) in merged {
688 let key = (base_path.clone(), seg_tag, qual, elem_idx, comp_idx);
689 entries.entry(key).or_default().extend(meanings);
690 }
691 }
692 }
693
694 fn extract_codes(codes: &[Value]) -> CodeMeanings {
696 let mut meanings = BTreeMap::new();
697 for code in codes {
698 if let (Some(value), Some(name)) = (
699 code.get("value").and_then(|v| v.as_str()),
700 code.get("name").and_then(|v| v.as_str()),
701 ) {
702 let enum_key = code
703 .get("enum")
704 .and_then(|v| v.as_str())
705 .map(|s| s.to_string());
706 meanings.insert(
707 value.to_string(),
708 CodeEnrichment {
709 meaning: name.to_string(),
710 enum_key,
711 },
712 );
713 }
714 }
715 meanings
716 }
717}
718
719#[cfg(test)]
720#[allow(deprecated)]
721mod tests {
722 use super::*;
723
724 #[test]
725 fn test_parse_pid_55001_schema() {
726 let schema_path = Path::new(concat!(
727 env!("CARGO_MANIFEST_DIR"),
728 "/../../crates/mig-types/src/generated/fv2504/utilmd/pids/pid_55001_schema.json"
729 ));
730 if !schema_path.exists() {
731 eprintln!("Skipping: PID schema not found");
732 return;
733 }
734
735 let lookup = CodeLookup::from_schema_file(schema_path).unwrap();
736
737 assert!(lookup.is_code_field("sg4.sg8_z01.sg10", "CCI", 2, 0));
739 assert_eq!(
740 lookup.meaning_for("sg4.sg8_z01.sg10", "CCI", 2, 0, "Z15"),
741 Some("Haushaltskunde gem. EnWG")
742 );
743 assert_eq!(
744 lookup.meaning_for("sg4.sg8_z01.sg10", "CCI", 2, 0, "Z18"),
745 Some("Kein Haushaltskunde gem. EnWG")
746 );
747
748 assert!(lookup.is_code_field("sg4.sg8_z79.sg10", "CCI", 0, 0));
750 assert_eq!(
751 lookup.meaning_for("sg4.sg8_z79.sg10", "CCI", 0, 0, "Z66"),
752 Some("Produkteigenschaft")
753 );
754
755 assert!(lookup.is_code_field("sg4.sg8_z79.sg10", "CAV", 0, 0));
757
758 assert!(!lookup.is_code_field("sg4.sg8_z79.sg10", "CAV", 0, 3));
760
761 assert!(!lookup.is_code_field("sg4.sg5_z16", "LOC", 1, 0));
763 }
764
765 #[test]
766 fn test_from_inline_schema() {
767 let schema = serde_json::json!({
768 "fields": {
769 "sg4": {
770 "children": {
771 "sg8_test": {
772 "children": {
773 "sg10": {
774 "segments": [{
775 "id": "CCI",
776 "elements": [{
777 "index": 2,
778 "components": [{
779 "sub_index": 0,
780 "type": "code",
781 "codes": [
782 {"value": "A1", "name": "Alpha"},
783 {"value": "B2", "name": "Beta"}
784 ]
785 }]
786 }]
787 }],
788 "source_group": "SG10"
789 }
790 },
791 "segments": [],
792 "source_group": "SG8"
793 }
794 },
795 "segments": [],
796 "source_group": "SG4"
797 }
798 }
799 });
800
801 let lookup = CodeLookup::from_schema_value(&schema);
802
803 assert!(lookup.is_code_field("sg4.sg8_test.sg10", "CCI", 2, 0));
804 assert_eq!(
805 lookup.meaning_for("sg4.sg8_test.sg10", "CCI", 2, 0, "A1"),
806 Some("Alpha")
807 );
808 assert_eq!(
809 lookup.meaning_for("sg4.sg8_test.sg10", "CCI", 2, 0, "B2"),
810 Some("Beta")
811 );
812 assert_eq!(
813 lookup.meaning_for("sg4.sg8_test.sg10", "CCI", 2, 0, "XX"),
814 None
815 );
816 assert!(!lookup.is_code_field("sg4.sg8_test.sg10", "CCI", 0, 0));
817 }
818
819 #[test]
820 fn test_discriminated_variant_merge() {
821 let schema = serde_json::json!({
823 "fields": {
824 "sg4": {
825 "children": {
826 "sg12_z63": {
827 "segments": [{
828 "id": "NAD",
829 "elements": [{
830 "index": 0,
831 "type": "code",
832 "codes": [{"value": "Z63", "name": "Standortadresse"}]
833 }]
834 }],
835 "source_group": "SG12"
836 },
837 "sg12_z65": {
838 "segments": [{
839 "id": "NAD",
840 "elements": [
841 {
842 "index": 0,
843 "type": "code",
844 "codes": [{"value": "Z65", "name": "Kunde des LF"}]
845 },
846 {
847 "index": 3,
848 "components": [{
849 "sub_index": 5,
850 "type": "code",
851 "codes": [
852 {"value": "Z01", "name": "Herr"},
853 {"value": "Z02", "name": "Frau"}
854 ]
855 }]
856 }
857 ]
858 }],
859 "source_group": "SG12"
860 }
861 },
862 "segments": [],
863 "source_group": "SG4"
864 }
865 }
866 });
867
868 let lookup = CodeLookup::from_schema_value(&schema);
869
870 assert!(lookup.is_code_field("sg4.sg12_z63", "NAD", 0, 0));
872 assert!(lookup.is_code_field("sg4.sg12_z65", "NAD", 0, 0));
873
874 assert!(lookup.is_code_field("sg4.sg12", "NAD", 0, 0));
876 assert_eq!(
877 lookup.meaning_for("sg4.sg12", "NAD", 0, 0, "Z63"),
878 Some("Standortadresse")
879 );
880 assert_eq!(
881 lookup.meaning_for("sg4.sg12", "NAD", 0, 0, "Z65"),
882 Some("Kunde des LF")
883 );
884
885 assert!(lookup.is_code_field("sg4.sg12", "NAD", 3, 5));
887 assert_eq!(
888 lookup.meaning_for("sg4.sg12", "NAD", 3, 5, "Z01"),
889 Some("Herr")
890 );
891 }
892
893 #[test]
894 fn test_pid_55013_sg12_base_path() {
895 let schema_path = Path::new(concat!(
896 env!("CARGO_MANIFEST_DIR"),
897 "/../../crates/mig-types/src/generated/fv2504/utilmd/pids/pid_55013_schema.json"
898 ));
899 if !schema_path.exists() {
900 eprintln!("Skipping: PID schema not found");
901 return;
902 }
903
904 let lookup = CodeLookup::from_schema_file(schema_path).unwrap();
905
906 assert!(lookup.is_code_field("sg4.sg12", "NAD", 0, 0));
908 assert!(lookup.meaning_for("sg4.sg12", "NAD", 0, 0, "Z67").is_some());
910 for code in &["Z63", "Z65", "Z66", "Z67", "Z68", "Z69", "Z70"] {
912 assert!(
913 lookup.meaning_for("sg4.sg12", "NAD", 0, 0, code).is_some(),
914 "Missing meaning for NAD qualifier {code} at base path sg4.sg12"
915 );
916 }
917 }
918
919 #[test]
920 fn test_multi_segment_code_merge() {
921 let schema = serde_json::json!({
924 "fields": {
925 "sg4": {
926 "children": {
927 "sg8_z98": {
928 "children": {
929 "sg10": {
930 "segments": [
931 {
932 "id": "CCI",
933 "elements": [{"index": 2, "components": [{
934 "sub_index": 0, "type": "code",
935 "codes": [{"value": "ZB3", "name": "Zugeordneter Marktpartner"}]
936 }]}]
937 },
938 {
939 "id": "CAV",
940 "elements": [{"index": 0, "components": [{
941 "sub_index": 0, "type": "code",
942 "codes": [{"value": "Z91", "name": "MSB"}]
943 }]}]
944 },
945 {
946 "id": "CCI",
947 "elements": [{"index": 2, "components": [{
948 "sub_index": 0, "type": "code",
949 "codes": [{"value": "E03", "name": "Spannungsebene"}]
950 }]}]
951 },
952 {
953 "id": "CAV",
954 "elements": [{"index": 0, "components": [{
955 "sub_index": 0, "type": "code",
956 "codes": [
957 {"value": "E05", "name": "Mittelspannung"},
958 {"value": "E06", "name": "Niederspannung"}
959 ]
960 }]}]
961 },
962 {
963 "id": "CCI",
964 "elements": [{"index": 2, "components": [{
965 "sub_index": 0, "type": "code",
966 "codes": [
967 {"value": "Z15", "name": "Haushaltskunde"},
968 {"value": "Z18", "name": "Kein Haushaltskunde"}
969 ]
970 }]}]
971 }
972 ],
973 "source_group": "SG10"
974 }
975 },
976 "segments": [],
977 "source_group": "SG8"
978 }
979 },
980 "segments": [],
981 "source_group": "SG4"
982 }
983 }
984 });
985
986 let lookup = CodeLookup::from_schema_value(&schema);
987
988 assert_eq!(
990 lookup.meaning_for("sg4.sg8_z98.sg10", "CCI", 2, 0, "ZB3"),
991 Some("Zugeordneter Marktpartner")
992 );
993 assert_eq!(
994 lookup.meaning_for("sg4.sg8_z98.sg10", "CCI", 2, 0, "E03"),
995 Some("Spannungsebene")
996 );
997 assert_eq!(
998 lookup.meaning_for("sg4.sg8_z98.sg10", "CCI", 2, 0, "Z15"),
999 Some("Haushaltskunde")
1000 );
1001
1002 assert_eq!(
1004 lookup.meaning_for("sg4.sg8_z98.sg10", "CAV", 0, 0, "Z91"),
1005 Some("MSB")
1006 );
1007 assert_eq!(
1008 lookup.meaning_for("sg4.sg8_z98.sg10", "CAV", 0, 0, "E06"),
1009 Some("Niederspannung")
1010 );
1011 }
1012
1013 #[test]
1014 fn test_enrichment_for_with_enum() {
1015 let schema = serde_json::json!({
1016 "fields": {
1017 "sg4": {
1018 "children": {
1019 "sg10": {
1020 "segments": [{
1021 "id": "CCI",
1022 "elements": [{
1023 "index": 2,
1024 "components": [{
1025 "sub_index": 0,
1026 "type": "code",
1027 "codes": [
1028 {"value": "Z15", "name": "Haushaltskunde", "enum": "HAUSHALTSKUNDE"},
1029 {"value": "Z18", "name": "Kein Haushaltskunde", "enum": "KEIN_HAUSHALTSKUNDE"}
1030 ]
1031 }]
1032 }]
1033 }],
1034 "source_group": "SG10"
1035 }
1036 },
1037 "segments": [],
1038 "source_group": "SG4"
1039 }
1040 }
1041 });
1042
1043 let lookup = CodeLookup::from_schema_value(&schema);
1044
1045 let enrichment = lookup.enrichment_for("sg4.sg10", "CCI", 2, 0, "Z15");
1046 assert!(enrichment.is_some());
1047 let e = enrichment.unwrap();
1048 assert_eq!(e.meaning, "Haushaltskunde");
1049 assert_eq!(e.enum_key.as_deref(), Some("HAUSHALTSKUNDE"));
1050
1051 let e2 = lookup
1052 .enrichment_for("sg4.sg10", "CCI", 2, 0, "Z18")
1053 .unwrap();
1054 assert_eq!(e2.enum_key.as_deref(), Some("KEIN_HAUSHALTSKUNDE"));
1055
1056 assert_eq!(
1058 lookup.meaning_for("sg4.sg10", "CCI", 2, 0, "Z15"),
1059 Some("Haushaltskunde")
1060 );
1061 }
1062
1063 #[test]
1064 fn test_backward_compat_no_enum() {
1065 let schema = serde_json::json!({
1067 "fields": {
1068 "sg4": {
1069 "children": {
1070 "sg10": {
1071 "segments": [{
1072 "id": "CCI",
1073 "elements": [{
1074 "index": 2,
1075 "components": [{
1076 "sub_index": 0,
1077 "type": "code",
1078 "codes": [
1079 {"value": "Z15", "name": "Haushaltskunde"}
1080 ]
1081 }]
1082 }]
1083 }],
1084 "source_group": "SG10"
1085 }
1086 },
1087 "segments": [],
1088 "source_group": "SG4"
1089 }
1090 }
1091 });
1092
1093 let lookup = CodeLookup::from_schema_value(&schema);
1094 let enrichment = lookup.enrichment_for("sg4.sg10", "CCI", 2, 0, "Z15");
1095 assert!(enrichment.is_some());
1096 let e = enrichment.unwrap();
1097 assert_eq!(e.meaning, "Haushaltskunde");
1098 assert_eq!(e.enum_key, None); }
1100
1101 fn qualified_variants_schema() -> Value {
1105 let rff = |qual: &str, name: &str, id_codes: Option<Value>| {
1106 let id = match id_codes {
1107 Some(codes) => {
1108 serde_json::json!({"sub_index": 1, "id": "1154", "type": "code", "codes": codes})
1109 }
1110 None => serde_json::json!({"sub_index": 1, "id": "1154", "type": "data"}),
1111 };
1112 serde_json::json!({"id": "RFF", "elements": [{"index": 0, "composite": "C506", "components": [
1113 {"sub_index": 0, "id": "1153", "type": "code", "codes": [{"value": qual, "name": name}]},
1114 id,
1115 ]}]})
1116 };
1117 let cav = |qual: &str, codes: Value| {
1118 serde_json::json!({"id": "CAV", "elements": [{"index": 0, "composite": "C889", "components": [
1119 {"sub_index": 0, "id": "7111", "type": "code", "codes": [{"value": qual, "name": qual}]},
1120 {"sub_index": 1, "id": "7110", "type": "code", "codes": codes},
1121 ]}]})
1122 };
1123 serde_json::json!({"fields": {"sg14": {"segments": [], "children": {"sg15": {"segments": [
1124 rff("Z13", "Prüfidentifikator", Some(serde_json::json!([{"value": "21037", "name": "RD / NB-Bewertung"}]))),
1125 rff("ACW", "Referenznummer einer vorangegangenen Nachricht", None),
1126 rff("ACE", "Nummer des zugehörigen Dokuments", None),
1127 cav("Z91", serde_json::json!([{"value": "A", "name": "Alpha"}, {"value": "B", "name": "Beta"}])),
1128 cav("ZF0", serde_json::json!([{"value": "C", "name": "Gamma"}])),
1129 ]}}}}})
1130 }
1131
1132 #[test]
1133 fn qualified_lookup_uses_only_codes_of_that_segment_variant() {
1134 let lookup = CodeLookup::from_schema_value(&qualified_variants_schema());
1135 let sp = "sg14.sg15";
1136
1137 for qual in ["ACW", "ACE"] {
1139 assert!(
1140 lookup.codes_q(sp, "RFF", Some(qual), 0, 1).is_none(),
1141 "{qual}"
1142 );
1143 assert!(
1144 !lookup.is_code_field_q(sp, "RFF", Some(qual), 0, 1),
1145 "{qual}"
1146 );
1147 assert!(lookup
1148 .enrichment_for_q(sp, "RFF", Some(qual), 0, 1, "21037")
1149 .is_none());
1150 }
1151 let z13: Vec<&String> = lookup
1152 .codes_q(sp, "RFF", Some("Z13"), 0, 1)
1153 .unwrap()
1154 .keys()
1155 .collect();
1156 assert_eq!(z13, ["21037"]);
1157 let acw: Vec<&String> = lookup
1158 .codes_q(sp, "RFF", Some("ACW"), 0, 0)
1159 .unwrap()
1160 .keys()
1161 .collect();
1162 assert_eq!(acw, ["ACW"]);
1163
1164 let z91: Vec<&String> = lookup
1167 .codes_q(sp, "CAV", Some("Z91"), 0, 1)
1168 .unwrap()
1169 .keys()
1170 .collect();
1171 assert_eq!(z91, ["A", "B"]);
1172 assert!(lookup
1173 .enrichment_for_q(sp, "CAV", Some("Z91"), 0, 1, "C")
1174 .is_none());
1175 let zf0: Vec<&String> = lookup
1176 .codes_q(sp, "CAV", Some("ZF0"), 0, 1)
1177 .unwrap()
1178 .keys()
1179 .collect();
1180 assert_eq!(zf0, ["C"]);
1181 let all: Vec<&String> = lookup
1182 .codes_q(sp, "CAV", None, 0, 1)
1183 .unwrap()
1184 .keys()
1185 .collect();
1186 assert_eq!(all, ["A", "B", "C"]);
1187 assert!(lookup.is_code_field_q(sp, "CAV", Some("Z98"), 0, 1));
1190 }
1191
1192 #[test]
1193 fn qualified_lookup_survives_cache_serialization() {
1194 let lookup = CodeLookup::from_schema_value(&qualified_variants_schema());
1195 let back: CodeLookup =
1196 serde_json::from_str(&serde_json::to_string(&lookup).unwrap()).unwrap();
1197 assert!(back
1198 .codes_q("sg14.sg15", "RFF", Some("ACW"), 0, 1)
1199 .is_none());
1200 let z91: Vec<&String> = back
1201 .codes_q("sg14.sg15", "CAV", Some("Z91"), 0, 1)
1202 .unwrap()
1203 .keys()
1204 .collect();
1205 assert_eq!(z91, ["A", "B"]);
1206 }
1207
1208 #[test]
1209 fn rff_tn_in_55002_is_not_a_code_field() {
1210 let schema_path = Path::new(concat!(
1211 env!("CARGO_MANIFEST_DIR"),
1212 "/../../crates/mig-types/src/generated/fv2504/utilmd/pids/pid_55002_schema.json"
1213 ));
1214 if !schema_path.exists() {
1215 return;
1216 }
1217 let lookup = CodeLookup::from_schema_file(schema_path).unwrap();
1218
1219 assert!(
1221 !lookup.is_code_field_q("sg4.sg6", "RFF", Some("TN"), 0, 1),
1222 "RFF+TN d1154 is free-text Vorgangsnummer, must not be classified as code"
1223 );
1224
1225 assert!(
1228 lookup.is_code_field_q("sg4.sg6", "RFF", Some("Z13"), 0, 1),
1229 "RFF+Z13 d1154 is type=code with PID-identifier value"
1230 );
1231 }
1232
1233 #[test]
1234 fn pid_self_reference_detection() {
1235 let schema_path = Path::new(concat!(
1236 env!("CARGO_MANIFEST_DIR"),
1237 "/../../crates/mig-types/src/generated/fv2504/utilmd/pids/pid_55002_schema.json"
1238 ));
1239 if !schema_path.exists() {
1240 return;
1241 }
1242 let lookup = CodeLookup::from_schema_file(schema_path).unwrap();
1243
1244 assert!(
1246 lookup.is_pid_self_reference("sg4.sg6", "RFF", Some("Z13"), 0, 1, "55002"),
1247 "Z13 d1154 with single value '55002' must be detected as PID self-ref"
1248 );
1249 assert!(
1251 !lookup.is_pid_self_reference("sg4.sg6", "RFF", Some("Z13"), 0, 1, "55001"),
1252 "Z13 d1154's '55002' should not count as self-ref for PID 55001"
1253 );
1254 }
1255}