1use std::collections::HashMap;
4use std::sync::Mutex;
5
6use mig_assembly::ConversionService;
7use mig_bo4e::engine::DataBundle;
8use mig_bo4e::MappingEngine;
9
10use crate::data_dir::DataDir;
11use crate::error::MapperError;
12
13pub struct Bo4eResult {
15 pub pid: String,
17 pub message_type: String,
19 pub variant: String,
21 pub bo4e: serde_json::Value,
23}
24
25#[derive(Debug, Clone)]
62pub struct PidListEntry {
63 pub fv: String,
64 pub variant: String,
65 pub pid: String,
66 pub beschreibung: String,
67}
68
69pub struct Mapper {
70 data_dir: DataDir,
71 bundles: Mutex<HashMap<String, DataBundle>>,
72}
73
74fn split_transaktion(tx: &serde_json::Value) -> mig_bo4e::model::MappedTransaktion {
87 let (transaktionsdaten, stammdaten) = if mig_bo4e::model::is_wrapped_transaktion(tx) {
88 (
89 tx.get("transaktionsdaten")
90 .cloned()
91 .unwrap_or(serde_json::Value::Null),
92 tx.get("stammdaten")
93 .cloned()
94 .unwrap_or_else(|| serde_json::Value::Object(Default::default())),
95 )
96 } else {
97 (serde_json::Value::Null, tx.clone())
98 };
99 mig_bo4e::model::MappedTransaktion {
100 transaktionsdaten,
101 stammdaten,
102 nesting_info: Default::default(),
103 }
104}
105
106impl Mapper {
107 pub fn from_data_dir(data_dir: DataDir) -> Result<Self, MapperError> {
112 let mapper = Self {
113 data_dir,
114 bundles: Mutex::new(HashMap::new()),
115 };
116 let eager_fvs: Vec<String> = mapper.data_dir.eager_fvs().to_vec();
117 for fv in &eager_fvs {
118 mapper.ensure_bundle_loaded(fv)?;
119 }
120 Ok(mapper)
121 }
122
123 fn ensure_bundle_loaded(&self, fv: &str) -> Result<(), MapperError> {
125 let mut bundles = self.bundles.lock().unwrap();
126 if bundles.contains_key(fv) {
127 return Ok(());
128 }
129 let path = self.data_dir.bundle_path(fv);
130 if !path.exists() {
131 return Err(MapperError::BundleNotFound { fv: fv.to_string() });
132 }
133 let bundle = DataBundle::load(&path)?;
134 bundles.insert(fv.to_string(), bundle);
135 Ok(())
136 }
137
138 pub fn conversion_service(
142 &self,
143 fv: &str,
144 variant: &str,
145 ) -> Result<ConversionService, MapperError> {
146 self.ensure_bundle_loaded(fv)?;
147 let bundles = self.bundles.lock().unwrap();
148 let bundle = bundles.get(fv).unwrap();
149 let vc = bundle
150 .variant(variant)
151 .ok_or_else(|| MapperError::VariantNotFound {
152 fv: fv.to_string(),
153 variant: variant.to_string(),
154 })?;
155 let mig = vc
156 .mig_schema
157 .as_ref()
158 .ok_or_else(|| MapperError::VariantNotFound {
159 fv: fv.to_string(),
160 variant: format!("{variant} (no MIG schema in bundle)"),
161 })?;
162 Ok(ConversionService::from_mig(mig.clone()))
163 }
164
165 pub fn engine(&self, fv: &str, variant: &str, pid: &str) -> Result<MappingEngine, MapperError> {
169 self.ensure_bundle_loaded(fv)?;
170 let bundles = self.bundles.lock().unwrap();
171 let bundle = bundles.get(fv).unwrap();
172 let vc = bundle
173 .variant(variant)
174 .ok_or_else(|| MapperError::VariantNotFound {
175 fv: fv.to_string(),
176 variant: variant.to_string(),
177 })?;
178 let pid_key = format!("pid_{pid}");
179 let defs = vc
180 .combined_defs
181 .get(&pid_key)
182 .ok_or_else(|| MapperError::PidNotFound {
183 fv: fv.to_string(),
184 variant: variant.to_string(),
185 pid: pid.to_string(),
186 })?;
187 Ok(MappingEngine::from_definitions(defs.clone()))
188 }
189
190 pub fn pid_requirements(
195 &self,
196 fv: &str,
197 variant: &str,
198 pid: &str,
199 ) -> Result<mig_bo4e::pid_requirements::PidRequirements, MapperError> {
200 self.ensure_bundle_loaded(fv)?;
201 let bundles = self.bundles.lock().unwrap();
202 let bundle = bundles.get(fv).unwrap();
203 let vc = bundle
204 .variant(variant)
205 .ok_or_else(|| MapperError::VariantNotFound {
206 fv: fv.to_string(),
207 variant: variant.to_string(),
208 })?;
209 let pid_key = format!("pid_{pid}");
210 vc.pid_requirements
211 .get(&pid_key)
212 .cloned()
213 .ok_or_else(|| MapperError::PidNotFound {
214 fv: fv.to_string(),
215 variant: variant.to_string(),
216 pid: pid.to_string(),
217 })
218 }
219
220 pub fn bo4e_catalog(
226 &self,
227 fv: &str,
228 ) -> Result<mig_bo4e::bo4e_catalog::Bo4eCatalog, MapperError> {
229 self.ensure_bundle_loaded(fv)?;
230 let bundles = self.bundles.lock().unwrap();
231 let bundle = bundles.get(fv).unwrap();
232 Ok(bundle.bo4e_catalog.clone())
233 }
234
235 pub fn list_pids(&self) -> Result<Vec<PidListEntry>, MapperError> {
240 let dir = self.data_dir.data_path();
241 let read_dir = std::fs::read_dir(dir).map_err(|_| MapperError::DataDirNotFound {
242 path: dir.display().to_string(),
243 })?;
244
245 let mut result = Vec::new();
246
247 for entry in read_dir.flatten() {
248 let path = entry.path();
249 if path.extension().is_some_and(|e| e == "bin") {
250 let stem = path
251 .file_stem()
252 .and_then(|s| s.to_str())
253 .unwrap_or("")
254 .to_string();
255 let fv = match stem.strip_prefix("edifact-data-") {
256 Some(v) => v.to_string(),
257 None => continue,
258 };
259 self.ensure_bundle_loaded(&fv)?;
260 let bundles = self.bundles.lock().unwrap();
261 if let Some(bundle) = bundles.get(&fv) {
262 for (variant, vc) in &bundle.variants {
263 for (pid_key, req) in &vc.pid_requirements {
264 let pid = pid_key.strip_prefix("pid_").unwrap_or(pid_key).to_string();
265 result.push(PidListEntry {
266 fv: fv.clone(),
267 variant: variant.clone(),
268 pid,
269 beschreibung: req.beschreibung.clone(),
270 });
271 }
272 }
273 }
274 }
275 }
276
277 result.sort_by(|a, b| a.pid.cmp(&b.pid));
278 Ok(result)
279 }
280
281 pub fn validate_pid(
286 &self,
287 json: &serde_json::Value,
288 fv: &str,
289 variant: &str,
290 pid: &str,
291 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
292 self.ensure_bundle_loaded(fv)?;
293 let bundles = self.bundles.lock().unwrap();
294 let bundle = bundles.get(fv).unwrap();
295 let vc = bundle
296 .variant(variant)
297 .ok_or_else(|| MapperError::VariantNotFound {
298 fv: fv.to_string(),
299 variant: variant.to_string(),
300 })?;
301 let pid_key = format!("pid_{pid}");
302 let requirements =
303 vc.pid_requirements
304 .get(&pid_key)
305 .ok_or_else(|| MapperError::PidNotFound {
306 fv: fv.to_string(),
307 variant: variant.to_string(),
308 pid: pid.to_string(),
309 })?;
310
311 Ok(mig_bo4e::pid_validation::validate_pid_json(
312 json,
313 requirements,
314 ))
315 }
316
317 pub fn validate_pid_struct(
329 &self,
330 value: &impl serde::Serialize,
331 fv: &str,
332 variant: &str,
333 pid: &str,
334 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
335 let json = serde_json::to_value(value).map_err(|e| {
336 MapperError::Mapping(mig_bo4e::MappingError::TypeConversion(e.to_string()))
337 })?;
338 self.validate_pid(&json, fv, variant, pid)
339 }
340
341 pub fn validate_pid_with_conditions(
349 &self,
350 json: &serde_json::Value,
351 fv: &str,
352 variant: &str,
353 pid: &str,
354 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
355 self.ensure_bundle_loaded(fv)?;
356 let bundles = self.bundles.lock().unwrap();
357 let bundle = bundles.get(fv).unwrap();
358 let vc = bundle
359 .variant(variant)
360 .ok_or_else(|| MapperError::VariantNotFound {
361 fv: fv.to_string(),
362 variant: variant.to_string(),
363 })?;
364 let pid_key = format!("pid_{pid}");
365
366 let requirements =
367 vc.pid_requirements
368 .get(&pid_key)
369 .ok_or_else(|| MapperError::PidNotFound {
370 fv: fv.to_string(),
371 variant: variant.to_string(),
372 pid: pid.to_string(),
373 })?;
374
375 let evaluator = crate::evaluator_factory::create_evaluator(variant, fv);
377
378 if let Some(evaluator) = evaluator {
379 let defs = vc
381 .combined_defs
382 .get(&pid_key)
383 .ok_or_else(|| MapperError::PidNotFound {
384 fv: fv.to_string(),
385 variant: variant.to_string(),
386 pid: pid.to_string(),
387 })?;
388 let engine = MappingEngine::from_definitions(defs.clone());
389 let tree = engine.map_all_reverse(json, None);
390
391 let segments = crate::tree_to_segments::tree_to_owned_segments(&tree);
393
394 Ok(crate::evaluator_factory::validate_with_boxed_evaluator(
396 evaluator.as_ref(),
397 json,
398 requirements,
399 pid,
400 &segments,
401 ))
402 } else {
403 Ok(mig_bo4e::pid_validation::validate_pid_json_transaction(
405 json,
406 requirements,
407 ))
408 }
409 }
410
411 pub fn to_edifact(
445 &self,
446 msg_stammdaten: &serde_json::Value,
447 tx_stammdaten: &[serde_json::Value],
448 fv: &str,
449 variant: &str,
450 pid: &str,
451 ) -> Result<String, MapperError> {
452 self.render_message_body(
453 msg_stammdaten,
454 tx_stammdaten,
455 fv,
456 variant,
457 pid,
458 EntrySegmentCheck::Refuse,
459 )
460 }
461
462 fn render_message_body(
470 &self,
471 msg_stammdaten: &serde_json::Value,
472 tx_stammdaten: &[serde_json::Value],
473 fv: &str,
474 variant: &str,
475 pid: &str,
476 check: EntrySegmentCheck,
477 ) -> Result<String, MapperError> {
478 self.ensure_bundle_loaded(fv)?;
479 let bundles = self.bundles.lock().unwrap();
480 let bundle = bundles.get(fv).unwrap();
481 let vc = bundle
482 .variant(variant)
483 .ok_or_else(|| MapperError::VariantNotFound {
484 fv: fv.to_string(),
485 variant: variant.to_string(),
486 })?;
487
488 let tx_group = vc.tx_group(pid).ok_or_else(|| MapperError::PidNotFound {
489 fv: fv.to_string(),
490 variant: variant.to_string(),
491 pid: pid.to_string(),
492 })?;
493
494 let msg_engine = vc.msg_engine(pid);
495 let tx_engine = vc.tx_engine(pid).ok_or_else(|| MapperError::PidNotFound {
496 fv: fv.to_string(),
497 variant: variant.to_string(),
498 pid: pid.to_string(),
499 })?;
500
501 let filtered_mig = vc
502 .filtered_mig(pid)
503 .ok_or_else(|| MapperError::NoMigSchema {
504 fv: fv.to_string(),
505 variant: variant.to_string(),
506 })?;
507
508 let transaktionen: Vec<mig_bo4e::model::MappedTransaktion> =
510 tx_stammdaten.iter().map(split_transaktion).collect();
511 let mapped = mig_bo4e::model::MappedMessage {
512 nachricht_meta: serde_json::Value::Null,
513 stammdaten: msg_stammdaten.clone(),
514 transaktionen,
515 nesting_info: Default::default(),
516 inter_group_segments: Default::default(),
517 };
518
519 let tree = MappingEngine::map_interchange_reverse(
521 &msg_engine,
522 &tx_engine,
523 &mapped,
524 tx_group,
525 Some(&filtered_mig),
526 );
527
528 let disassembler = mig_assembly::disassembler::Disassembler::new(&filtered_mig);
533 let checked = match check {
534 EntrySegmentCheck::Refuse => disassembler.disassemble_checked(&tree),
535 EntrySegmentCheck::Render => Ok(disassembler.disassemble(&tree)),
536 };
537 let segments = checked.map_err(|e| match e {
538 mig_assembly::AssemblyError::MissingGroupEntrySegment {
539 group_path,
540 source_path,
541 entry_segment,
542 present_segments,
543 } => {
544 let (entities, entry_fields) = describe_entry_segment_mappings(
545 [msg_engine.definitions(), tx_engine.definitions()],
546 &source_path,
547 &entry_segment,
548 );
549 MapperError::MissingGroupEntrySegment(Box::new(
550 crate::error::GroupEntrySegmentError {
551 pid: pid.to_string(),
552 group_path,
553 source_path,
554 entry_segment,
555 present_segments,
556 entities,
557 entry_fields,
558 },
559 ))
560 }
561 other => MapperError::Assembly(other),
562 })?;
563
564 let delimiters = edifact_primitives::EdifactDelimiters::default();
566 Ok(mig_assembly::renderer::render_edifact(
567 &segments,
568 &delimiters,
569 ))
570 }
571
572 pub fn to_edifact_struct(
578 &self,
579 nachricht: &impl serde::Serialize,
580 fv: &str,
581 variant: &str,
582 pid: &str,
583 ) -> Result<String, MapperError> {
584 let json = serde_json::to_value(nachricht)
585 .map_err(|e| MapperError::Serialization(e.to_string()))?;
586
587 let msg_stammdaten = json
588 .get("stammdaten")
589 .cloned()
590 .unwrap_or(serde_json::Value::Object(Default::default()));
591
592 let tx_stammdaten: Vec<serde_json::Value> = json
593 .get("transaktionen")
594 .and_then(|v| v.as_array())
595 .cloned()
596 .unwrap_or_default();
597
598 self.to_edifact(&msg_stammdaten, &tx_stammdaten, fv, variant, pid)
599 }
600
601 pub fn from_edifact<M, T>(
629 &self,
630 edifact: &str,
631 fv: &str,
632 variant: &str,
633 pid: &str,
634 ) -> Result<mig_bo4e::model::Interchange<M, T>, MapperError>
635 where
636 M: serde::de::DeserializeOwned,
637 T: serde::de::DeserializeOwned,
638 {
639 let (interchange, diagnostics) =
640 self.from_edifact_with_diagnostics(edifact, fv, variant, pid)?;
641 for d in &diagnostics {
644 tracing::warn!(
645 fv,
646 variant,
647 pid,
648 kind = ?d.kind,
649 segment = %d.segment_id,
650 position = d.position,
651 "from_edifact: {}",
652 d.message
653 );
654 }
655 Ok(interchange)
656 }
657
658 pub fn from_edifact_with_diagnostics<M, T>(
671 &self,
672 edifact: &str,
673 fv: &str,
674 variant: &str,
675 pid: &str,
676 ) -> Result<
677 (
678 mig_bo4e::model::Interchange<M, T>,
679 Vec<mig_assembly::StructureDiagnostic>,
680 ),
681 MapperError,
682 >
683 where
684 M: serde::de::DeserializeOwned,
685 T: serde::de::DeserializeOwned,
686 {
687 self.ensure_bundle_loaded(fv)?;
688 let bundles = self.bundles.lock().unwrap();
689 let bundle = bundles.get(fv).unwrap();
690 let vc = bundle
691 .variant(variant)
692 .ok_or_else(|| MapperError::VariantNotFound {
693 fv: fv.to_string(),
694 variant: variant.to_string(),
695 })?;
696
697 let tx_group = vc.tx_group(pid).ok_or_else(|| MapperError::PidNotFound {
698 fv: fv.to_string(),
699 variant: variant.to_string(),
700 pid: pid.to_string(),
701 })?;
702
703 let msg_engine = vc.msg_engine(pid);
704 let tx_engine = vc.tx_engine(pid).ok_or_else(|| MapperError::PidNotFound {
705 fv: fv.to_string(),
706 variant: variant.to_string(),
707 pid: pid.to_string(),
708 })?;
709
710 let filtered_mig = vc
711 .filtered_mig(pid)
712 .ok_or_else(|| MapperError::NoMigSchema {
713 fv: fv.to_string(),
714 variant: variant.to_string(),
715 })?;
716
717 let svc = ConversionService::from_mig(filtered_mig);
723 let (chunks, trees, assembly_diagnostics) = svc
724 .convert_interchange_to_trees_with_diagnostics(
725 edifact,
726 mig_assembly::assembler::AssemblerConfig {
727 strict_code_matching: true,
728 skip_unknown_segments: true,
729 ..Default::default()
730 },
731 )?;
732
733 let tree = trees.first().ok_or_else(|| {
734 MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
735 "No messages in interchange".to_string(),
736 ))
737 })?;
738
739 let interchangedaten = mig_bo4e::model::extract_interchangedaten(&chunks.envelope);
741 let msg_chunk = chunks.messages.first().ok_or_else(|| {
742 MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
743 "No message chunks".to_string(),
744 ))
745 })?;
746 let (unh_ref, nachrichten_typ) = mig_bo4e::model::extract_unh_fields(&msg_chunk.unh);
747 let nachrichtendaten = mig_bo4e::model::Nachrichtendaten {
748 unh_referenz: unh_ref,
749 nachrichten_typ,
750 nachricht: Default::default(),
751 };
752
753 let interchange = MappingEngine::map_interchange_typed::<M, T>(
755 &msg_engine,
756 &tx_engine,
757 tree,
758 tx_group,
759 true,
760 nachrichtendaten,
761 interchangedaten,
762 )
763 .map_err(|e| MapperError::Serialization(e.to_string()))?;
764
765 Ok((interchange, assembly_diagnostics))
766 }
767
768 pub fn detect_pid(&self, edifact: &str) -> Result<String, MapperError> {
780 let segments = mig_assembly::tokenize::parse_to_segments(edifact.as_bytes())?;
781 let chunks = mig_assembly::split_messages(segments)?;
782 let msg_chunk = chunks.messages.first().ok_or_else(|| {
783 MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
784 "No messages found in EDIFACT content".to_string(),
785 ))
786 })?;
787 let msg_segments = msg_chunk.message_segments();
788 mig_assembly::pid_detect::detect_pid(&msg_segments).map_err(MapperError::Assembly)
789 }
790
791 pub fn validate_edifact(
807 &self,
808 edifact: &str,
809 fv: &str,
810 level: automapper_validation::ValidationLevel,
811 ) -> Result<automapper_validation::ValidationReport, MapperError> {
812 self.validate_edifact_inner(edifact, fv, None, level)
813 }
814
815 pub fn validate_edifact_for_pid(
825 &self,
826 edifact: &str,
827 fv: &str,
828 variant: &str,
829 pid: &str,
830 level: automapper_validation::ValidationLevel,
831 ) -> Result<automapper_validation::ValidationReport, MapperError> {
832 self.validate_edifact_inner(edifact, fv, Some((variant, pid)), level)
833 }
834
835 fn validate_edifact_inner(
836 &self,
837 edifact: &str,
838 fv: &str,
839 known: Option<(&str, &str)>,
840 level: automapper_validation::ValidationLevel,
841 ) -> Result<automapper_validation::ValidationReport, MapperError> {
842 self.ensure_bundle_loaded(fv)?;
843 let bundles = self.bundles.lock().unwrap();
844 let bundle = bundles.get(fv).unwrap();
845
846 let segments = mig_assembly::tokenize::parse_to_segments(edifact.as_bytes())?;
848 let chunks = mig_assembly::split_messages(segments)?;
849 let msg_chunk = chunks.messages.first().ok_or_else(|| {
850 MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
851 "No messages found in EDIFACT content".to_string(),
852 ))
853 })?;
854
855 let (pid, variant, vc) = match known {
861 Some((variant, pid)) => {
862 let vc = bundle
863 .variant(variant)
864 .ok_or_else(|| MapperError::VariantNotFound {
865 fv: fv.to_string(),
866 variant: variant.to_string(),
867 })?;
868 (pid.to_string(), variant.to_string(), vc)
869 }
870 None => {
871 let pid = mig_assembly::pid_detect::detect_pid(&msg_chunk.message_segments())
872 .map_err(MapperError::Assembly)?;
873 let pid_key = format!("pid_{pid}");
874 let (variant, vc) = bundle
875 .variants
876 .iter()
877 .find(|(_, vc)| vc.pid_ahb_workflows.contains_key(&pid_key))
878 .ok_or_else(|| MapperError::PidNotFound {
879 fv: fv.to_string(),
880 variant: "?".to_string(),
881 pid: pid.clone(),
882 })?;
883 (pid, variant.clone(), vc)
884 }
885 };
886 let pid_key = format!("pid_{pid}");
887
888 let workflow =
889 vc.pid_ahb_workflows
890 .get(&pid_key)
891 .ok_or_else(|| MapperError::PidNotFound {
892 fv: fv.to_string(),
893 variant: variant.clone(),
894 pid: pid.clone(),
895 })?;
896 let filtered_mig = vc
897 .filtered_mig(&pid)
898 .ok_or_else(|| MapperError::NoMigSchema {
899 fv: fv.to_string(),
900 variant: variant.clone(),
901 })?;
902
903 let mut all_segments = msg_chunk.segments_for_mig(&filtered_mig);
906 if filtered_mig.segments.iter().any(|s| s.id == "UNZ") {
907 if let Some(unz) = &chunks.unz {
908 all_segments.push(unz.clone());
909 }
910 }
911
912 let evaluator: std::sync::Arc<dyn automapper_validation::ConditionEvaluator> =
916 match crate::evaluator_factory::create_evaluator(&variant, fv) {
917 Some(boxed) => std::sync::Arc::from(boxed),
918 None => std::sync::Arc::new(
919 automapper_validation::UtilmdStromConditionEvaluatorFV2504::default(),
920 ),
921 };
922 let external = automapper_validation::eval::NoOpExternalProvider;
923
924 let mut report = automapper_validation::validate_edifact_message(
925 &all_segments,
926 &filtered_mig,
927 workflow,
928 evaluator,
929 &external,
930 level,
931 );
932
933 if let (Some(mig), Some(defs)) = (vc.mig_schema.as_ref(), vc.combined_defs.get(&pid_key)) {
939 let reverse = mig_bo4e::path_resolver::ReversePathResolver::from_mig(mig);
940 let field_index =
941 mig_bo4e::Bo4eFieldIndex::build_with_resolver(defs, &filtered_mig, &reverse);
942 report.enrich_bo4e_paths(|path, hint| field_index.resolve(path, hint));
943 }
944
945 Ok(report)
946 }
947
948 pub fn validate_bo4e(
971 &self,
972 msg_stammdaten: &serde_json::Value,
973 tx_stammdaten: &[serde_json::Value],
974 fv: &str,
975 variant: &str,
976 pid: &str,
977 envelope: Option<&InterchangeEnvelope>,
978 level: automapper_validation::ValidationLevel,
979 ) -> Result<automapper_validation::ValidationReport, MapperError> {
980 let placeholder;
981 let envelope = match envelope {
982 Some(e) => e,
983 None => {
984 placeholder = InterchangeEnvelope {
985 sender: EdifactParty::bdew("9900000000001"),
986 receiver: EdifactParty::bdew("9900000000002"),
987 interchange_ref: "1".to_string(),
988 };
989 &placeholder
990 }
991 };
992
993 let edifact = self.render_interchange(
998 envelope,
999 &[InterchangeMessage {
1000 message_ref: "1".to_string(),
1001 msg_stammdaten: msg_stammdaten.clone(),
1002 tx_stammdaten: tx_stammdaten.to_vec(),
1003 fv: fv.to_string(),
1004 variant: variant.to_string(),
1005 pid: pid.to_string(),
1006 }],
1007 EntrySegmentCheck::Render,
1008 )?;
1009
1010 self.validate_edifact_for_pid(&edifact, fv, variant, pid, level)
1013 }
1014
1015 pub fn association_code(&self, fv: &str, variant: &str) -> Result<String, MapperError> {
1026 let meta = self.message_metadata(fv, variant)?;
1027 Ok(meta.association_code)
1028 }
1029
1030 pub fn message_metadata(
1035 &self,
1036 fv: &str,
1037 variant: &str,
1038 ) -> Result<MessageMetadata, MapperError> {
1039 self.ensure_bundle_loaded(fv)?;
1040 let bundles = self.bundles.lock().unwrap();
1041 let bundle = bundles.get(fv).unwrap();
1042 let vc = bundle
1043 .variant(variant)
1044 .ok_or_else(|| MapperError::VariantNotFound {
1045 fv: fv.to_string(),
1046 variant: variant.to_string(),
1047 })?;
1048 let mig = vc
1049 .mig_schema
1050 .as_ref()
1051 .ok_or_else(|| MapperError::NoMigSchema {
1052 fv: fv.to_string(),
1053 variant: variant.to_string(),
1054 })?;
1055 Ok(MessageMetadata {
1056 message_type: mig.message_type.clone(),
1057 release: release_code_for_message_type(&mig.message_type),
1058 association_code: mig.version.clone(),
1059 })
1060 }
1061
1062 pub fn to_edifact_interchange(
1092 &self,
1093 envelope: &InterchangeEnvelope,
1094 messages: &[InterchangeMessage],
1095 ) -> Result<String, MapperError> {
1096 self.render_interchange(envelope, messages, EntrySegmentCheck::Refuse)
1097 }
1098
1099 fn render_interchange(
1100 &self,
1101 envelope: &InterchangeEnvelope,
1102 messages: &[InterchangeMessage],
1103 check: EntrySegmentCheck,
1104 ) -> Result<String, MapperError> {
1105 let delimiters = edifact_primitives::EdifactDelimiters::default();
1106 let sep = delimiters.component as char;
1107 let elem = delimiters.element as char;
1108 let seg_term = delimiters.segment as char;
1109
1110 let mut output = String::new();
1111
1112 output.push_str(&format!(
1114 "UNA{}{}{}{}{}{}",
1115 sep, elem, delimiters.decimal as char, delimiters.release as char, ' ', seg_term, ));
1122
1123 let now = chrono::Utc::now();
1125 let date_str = now.format("%y%m%d").to_string();
1126 let time_str = now.format("%H%M").to_string();
1127 let sender = &envelope.sender;
1128 let receiver = &envelope.receiver;
1129 let interchange_ref = &envelope.interchange_ref;
1130 output.push_str(&format!(
1131 "UNB{elem}UNOC{sep}3{elem}{sid}{sep}{sq}{elem}{rid}{sep}{rq}{elem}{date_str}{sep}{time_str}{elem}{interchange_ref}{seg_term}",
1132 sid = sender.id,
1133 sq = sender.qualifier,
1134 rid = receiver.id,
1135 rq = receiver.qualifier,
1136 ));
1137
1138 let mut message_count = 0u32;
1139
1140 for msg in messages {
1141 let meta = self.message_metadata(&msg.fv, &msg.variant)?;
1142
1143 let body = self.render_message_body(
1145 &msg.msg_stammdaten,
1146 &msg.tx_stammdaten,
1147 &msg.fv,
1148 &msg.variant,
1149 &msg.pid,
1150 check,
1151 )?;
1152
1153 let body_seg_count = body
1155 .split(seg_term)
1156 .filter(|s: &&str| !s.is_empty())
1157 .count();
1158 let segment_count = body_seg_count + 2;
1160
1161 output.push_str(&format!(
1163 "UNH{elem}{ref}{elem}{msg_type}{sep}D{sep}{release}{sep}UN{sep}{assoc}{seg_term}",
1164 ref = msg.message_ref,
1165 msg_type = meta.message_type,
1166 release = meta.release,
1167 assoc = meta.association_code,
1168 ));
1169
1170 output.push_str(&body);
1172
1173 output.push_str(&format!(
1175 "UNT{elem}{segment_count}{elem}{ref}{seg_term}",
1176 ref = msg.message_ref,
1177 ));
1178
1179 message_count += 1;
1180 }
1181
1182 output.push_str(&format!(
1184 "UNZ{elem}{message_count}{elem}{interchange_ref}{seg_term}",
1185 ));
1186
1187 Ok(output)
1188 }
1189
1190 pub fn loaded_format_versions(&self) -> Vec<String> {
1192 self.bundles.lock().unwrap().keys().cloned().collect()
1193 }
1194
1195 pub fn variants(&self, fv: &str) -> Result<Vec<String>, MapperError> {
1199 self.ensure_bundle_loaded(fv)?;
1200 let bundles = self.bundles.lock().unwrap();
1201 let bundle = bundles.get(fv).unwrap();
1202 Ok(bundle.variants.keys().cloned().collect())
1203 }
1204}
1205
1206#[derive(Debug, Clone)]
1208pub struct MessageMetadata {
1209 pub message_type: String,
1211 pub release: String,
1213 pub association_code: String,
1215}
1216
1217#[derive(Debug, Clone)]
1219pub struct InterchangeEnvelope {
1220 pub sender: EdifactParty,
1222 pub receiver: EdifactParty,
1224 pub interchange_ref: String,
1226}
1227
1228#[derive(Debug, Clone)]
1230pub struct EdifactParty {
1231 pub id: String,
1233 pub qualifier: String,
1235}
1236
1237impl EdifactParty {
1238 pub fn bdew(id: &str) -> Self {
1240 Self {
1241 id: id.to_string(),
1242 qualifier: "500".to_string(),
1243 }
1244 }
1245
1246 pub fn gs1(id: &str) -> Self {
1248 Self {
1249 id: id.to_string(),
1250 qualifier: "14".to_string(),
1251 }
1252 }
1253}
1254
1255#[derive(Debug, Clone)]
1258pub struct InterchangeMessage {
1259 pub message_ref: String,
1261 pub msg_stammdaten: serde_json::Value,
1263 pub tx_stammdaten: Vec<serde_json::Value>,
1265 pub fv: String,
1267 pub variant: String,
1269 pub pid: String,
1271}
1272
1273#[derive(Debug, Clone, Copy)]
1275enum EntrySegmentCheck {
1276 Refuse,
1278 Render,
1280}
1281
1282fn describe_entry_segment_mappings<'d>(
1292 definition_sets: impl IntoIterator<Item = &'d [mig_bo4e::definition::MappingDefinition]>,
1293 source_path: &str,
1294 entry_segment: &str,
1295) -> (Vec<String>, Vec<String>) {
1296 fn qualifies(unqualified: &str, qualified: &str) -> bool {
1297 !unqualified.contains('_')
1298 && qualified.len() > unqualified.len()
1299 && qualified.is_char_boundary(unqualified.len())
1300 && qualified[..unqualified.len()].eq_ignore_ascii_case(unqualified)
1301 && qualified.as_bytes()[unqualified.len()] == b'_'
1302 }
1303 fn part_matches(mig_part: &str, def_part: &str) -> bool {
1304 def_part.eq_ignore_ascii_case(mig_part)
1305 || qualifies(mig_part, def_part)
1306 || qualifies(def_part, mig_part)
1307 }
1308 let mig_parts: Vec<&str> = source_path.split('.').collect();
1309
1310 let mut entities: Vec<String> = Vec::new();
1311 let mut entry_fields: Vec<String> = Vec::new();
1312 for def in definition_sets.into_iter().flatten() {
1313 let Some(def_path) = def.meta.source_path.as_deref() else {
1314 continue;
1315 };
1316 let def_parts: Vec<&str> = def_path.split('.').collect();
1317 if def_parts.len() != mig_parts.len()
1318 || !mig_parts
1319 .iter()
1320 .zip(&def_parts)
1321 .all(|(m, d)| part_matches(m, d))
1322 {
1323 continue;
1324 }
1325 if !entities.contains(&def.meta.entity) {
1326 entities.push(def.meta.entity.clone());
1327 }
1328 for (path, mapping) in &def.fields {
1329 let tag = path
1330 .split(['.', '['])
1331 .next()
1332 .unwrap_or_default()
1333 .to_ascii_uppercase();
1334 let target = match mapping {
1335 mig_bo4e::definition::FieldMapping::Simple(t) => t.as_str(),
1336 mig_bo4e::definition::FieldMapping::Structured(f) => f.target.as_str(),
1337 mig_bo4e::definition::FieldMapping::Nested(_) => continue,
1338 };
1339 if tag == entry_segment && !target.is_empty() {
1340 let field = format!("{}.{}", def.meta.entity, target);
1341 if !entry_fields.contains(&field) {
1342 entry_fields.push(field);
1343 }
1344 }
1345 }
1346 }
1347 (entities, entry_fields)
1348}
1349
1350fn release_code_for_message_type(msg_type: &str) -> String {
1354 mig_bo4e::model::release_code_for_message_type(msg_type).to_string()
1355}
1356
1357#[cfg(test)]
1358mod tests {
1359 use super::*;
1360 use std::path::Path;
1361
1362 fn data_dir() -> Option<std::path::PathBuf> {
1363 let dist = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../dist");
1365 if dist.join("edifact-data-FV2504.bin").exists() {
1366 return Some(dist);
1367 }
1368 let cache = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../cache/mappings");
1369 if cache.join("FV2504").exists() {
1370 return Some(cache);
1371 }
1372 eprintln!("Skipping test: no DataBundle files found");
1373 None
1374 }
1375
1376 #[test]
1377 fn test_to_edifact_produces_edifact_output() {
1378 let Some(data_dir) = data_dir() else {
1379 return;
1380 };
1381 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1382
1383 let msg_stammdaten = serde_json::json!({
1384 "marktteilnehmer": [{
1385 "marktrolle": "MS",
1386 "rollencodenummer": "9900123456789",
1387 "codepflegeCode": "293"
1388 }]
1389 });
1390 let tx_stammdaten = serde_json::json!({
1391 "prozessdaten": {
1392 "pruefidentifikator": "55001",
1393 "vorgangId": "ABC123",
1394 "transaktionsgrund": "E01"
1395 }
1396 });
1397
1398 let result = mapper.to_edifact(
1399 &msg_stammdaten,
1400 &[tx_stammdaten],
1401 "FV2504",
1402 "UTILMD_Strom",
1403 "55001",
1404 );
1405 assert!(result.is_ok(), "to_edifact failed: {:?}", result.err());
1406 let edifact = result.unwrap();
1407 assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
1408 assert!(edifact.contains("NAD"), "Should contain NAD segment");
1410 assert!(edifact.contains("IDE"), "Should contain IDE segment");
1412 }
1413
1414 #[test]
1415 fn test_to_edifact_struct_produces_edifact_output() {
1416 let Some(data_dir) = data_dir() else {
1417 return;
1418 };
1419 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1420
1421 let nachricht = serde_json::json!({
1422 "stammdaten": {
1423 "marktteilnehmer": [{
1424 "marktrolle": "MS",
1425 "rollencodenummer": "9900123456789",
1426 "codepflegeCode": "293"
1427 }]
1428 },
1429 "transaktionen": [{
1430 "prozessdaten": {
1431 "pruefidentifikator": "55001",
1432 "vorgangId": "ABC123"
1433 }
1434 }]
1435 });
1436
1437 let result = mapper.to_edifact_struct(&nachricht, "FV2504", "UTILMD_Strom", "55001");
1438 assert!(
1439 result.is_ok(),
1440 "to_edifact_struct failed: {:?}",
1441 result.err()
1442 );
1443 let edifact = result.unwrap();
1444 assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
1445 }
1446
1447 #[test]
1448 fn test_to_edifact_invalid_fv_returns_error() {
1449 let Some(data_dir) = data_dir() else {
1450 return;
1451 };
1452 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1453
1454 let result = mapper.to_edifact(
1455 &serde_json::json!({}),
1456 &[serde_json::json!({})],
1457 "FV9999",
1458 "UTILMD_Strom",
1459 "55001",
1460 );
1461 assert!(result.is_err());
1462 }
1463
1464 #[test]
1465 fn test_to_edifact_invalid_variant_returns_error() {
1466 let Some(data_dir) = data_dir() else {
1467 return;
1468 };
1469 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1470
1471 let result = mapper.to_edifact(
1472 &serde_json::json!({}),
1473 &[serde_json::json!({})],
1474 "FV2504",
1475 "NONEXISTENT",
1476 "55001",
1477 );
1478 assert!(result.is_err());
1479 }
1480
1481 #[test]
1482 fn test_to_edifact_invalid_pid_returns_error() {
1483 let Some(data_dir) = data_dir() else {
1484 return;
1485 };
1486 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1487
1488 let result = mapper.to_edifact(
1489 &serde_json::json!({}),
1490 &[serde_json::json!({})],
1491 "FV2504",
1492 "UTILMD_Strom",
1493 "99999",
1494 );
1495 assert!(result.is_err());
1496 }
1497
1498 #[test]
1499 fn test_association_code() {
1500 let Some(data_dir) = data_dir() else {
1501 return;
1502 };
1503 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1504
1505 let code = mapper.association_code("FV2504", "UTILMD_Strom").unwrap();
1506 assert_eq!(code, "S2.1");
1507
1508 let code = mapper.association_code("FV2504", "MSCONS").unwrap();
1509 assert_eq!(code, "2.4c");
1510 }
1511
1512 #[test]
1513 fn test_message_metadata() {
1514 let Some(data_dir) = data_dir() else {
1515 return;
1516 };
1517 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1518
1519 let meta = mapper.message_metadata("FV2504", "UTILMD_Strom").unwrap();
1520 assert_eq!(meta.message_type, "UTILMD");
1521 assert_eq!(meta.release, "11A");
1522 assert_eq!(meta.association_code, "S2.1");
1523 }
1524
1525 #[test]
1526 fn test_to_edifact_interchange() {
1527 let Some(data_dir) = data_dir() else {
1528 return;
1529 };
1530 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1531
1532 let result = mapper.to_edifact_interchange(
1533 &InterchangeEnvelope {
1534 sender: EdifactParty::bdew("9900000000003"),
1535 receiver: EdifactParty::bdew("9900000000001"),
1536 interchange_ref: "REF001".to_string(),
1537 },
1538 &[InterchangeMessage {
1539 message_ref: "MSG001".to_string(),
1540 msg_stammdaten: serde_json::json!({
1541 "marktteilnehmer": [{
1542 "marktrolle": "MS",
1543 "rollencodenummer": "9900123456789",
1544 "codepflegeCode": "293"
1545 }]
1546 }),
1547 tx_stammdaten: vec![serde_json::json!({
1548 "prozessdaten": {
1549 "pruefidentifikator": "55001",
1550 "vorgangId": "ABC123",
1551 "transaktionsgrund": "E01"
1552 }
1553 })],
1554 fv: "FV2504".to_string(),
1555 variant: "UTILMD_Strom".to_string(),
1556 pid: "55001".to_string(),
1557 }],
1558 );
1559 assert!(
1560 result.is_ok(),
1561 "to_edifact_interchange failed: {:?}",
1562 result.err()
1563 );
1564 let edifact = result.unwrap();
1565
1566 assert!(edifact.starts_with("UNA:+.? '"), "Should start with UNA");
1568 assert!(
1569 edifact.contains("UNB+UNOC:3+9900000000003:500+9900000000001:500+"),
1570 "Should contain UNB with sender/receiver"
1571 );
1572 assert!(
1573 edifact.contains("UNH+MSG001+UTILMD:D:11A:UN:S2.1'"),
1574 "Should contain UNH with correct S009"
1575 );
1576 assert!(edifact.contains("NAD"), "Should contain body NAD segment");
1577 assert!(edifact.contains("UNT+"), "Should contain UNT");
1578 assert!(
1579 edifact.contains("+MSG001'"),
1580 "UNT should reference message ref"
1581 );
1582 assert!(
1583 edifact.contains("UNZ+1+REF001'"),
1584 "Should contain UNZ with count and ref"
1585 );
1586 }
1587
1588 #[test]
1589 fn test_detect_pid_from_rff_z13() {
1590 let Some(data_dir) = data_dir() else {
1591 return;
1592 };
1593 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1594
1595 let edifact = "\
1596 UNB+UNOC:3+9978842000002:500+9900269000000:500+250331:1329+REF001'\
1597 UNH+MSG001+UTILMD:D:11A:UN:S2.1'\
1598 BGM+E01+DOC001'\
1599 DTM+137:202503311329?+00:303'\
1600 NAD+MS+9978842000002::293'\
1601 NAD+MR+9900269000000::293'\
1602 IDE+24+TX001'\
1603 DTM+92:202505312200?+00:303'\
1604 DTM+93:202512312300?+00:303'\
1605 STS+7++E01+ZW4+E03'\
1606 LOC+Z16+12345678900'\
1607 RFF+Z13:55001'\
1608 UNT+12+MSG001'\
1609 UNZ+1+REF001'";
1610
1611 let pid = mapper.detect_pid(edifact).unwrap();
1612 assert_eq!(pid, "55001");
1613 }
1614
1615 #[test]
1616 fn test_detect_pid_no_messages_returns_error() {
1617 let Some(data_dir) = data_dir() else {
1618 return;
1619 };
1620 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1621
1622 let edifact = "UNB+UNOC:3+SENDER:500+RECEIVER:500+250401:1200+REF'\
1623 UNZ+0+REF'";
1624 assert!(mapper.detect_pid(edifact).is_err());
1625 }
1626
1627 #[test]
1628 fn test_list_pids_returns_entries() {
1629 let Some(data_dir) = data_dir() else {
1630 return;
1631 };
1632 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir)).unwrap();
1633 let pids = mapper.list_pids().expect("list_pids should succeed");
1634 assert!(!pids.is_empty(), "should return at least one PID");
1635 assert!(
1636 pids.iter().any(|p| p.pid == "55001"),
1637 "should include PID 55001"
1638 );
1639 assert!(
1640 pids.iter().any(|p| p.fv == "FV2504"),
1641 "should include FV2504"
1642 );
1643 assert!(
1644 pids.iter().any(|p| p.variant == "UTILMD_Strom"),
1645 "should include UTILMD_Strom"
1646 );
1647 }
1648
1649 #[test]
1650 fn test_pid_requirements_returns_requirements() {
1651 let Some(data_dir) = data_dir() else {
1652 return;
1653 };
1654 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1655
1656 let req = mapper
1657 .pid_requirements("FV2504", "UTILMD_Strom", "55001")
1658 .expect("pid_requirements should succeed");
1659
1660 assert_eq!(req.pid, "55001");
1661 assert!(
1662 !req.entities.is_empty(),
1663 "55001 should have at least one entity"
1664 );
1665 assert!(
1666 req.entities.iter().any(|e| e.entity == "Prozessdaten"),
1667 "55001 should have a Prozessdaten entity"
1668 );
1669 }
1670}