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 {
82 let (transaktionsdaten, stammdaten) = match tx.get("stammdaten") {
83 Some(stammdaten) => (
84 tx.get("transaktionsdaten")
85 .cloned()
86 .unwrap_or(serde_json::Value::Null),
87 stammdaten.clone(),
88 ),
89 None => (serde_json::Value::Null, tx.clone()),
90 };
91 mig_bo4e::model::MappedTransaktion {
92 transaktionsdaten,
93 stammdaten,
94 nesting_info: Default::default(),
95 }
96}
97
98impl Mapper {
99 pub fn from_data_dir(data_dir: DataDir) -> Result<Self, MapperError> {
104 let mapper = Self {
105 data_dir,
106 bundles: Mutex::new(HashMap::new()),
107 };
108 let eager_fvs: Vec<String> = mapper.data_dir.eager_fvs().to_vec();
109 for fv in &eager_fvs {
110 mapper.ensure_bundle_loaded(fv)?;
111 }
112 Ok(mapper)
113 }
114
115 fn ensure_bundle_loaded(&self, fv: &str) -> Result<(), MapperError> {
117 let mut bundles = self.bundles.lock().unwrap();
118 if bundles.contains_key(fv) {
119 return Ok(());
120 }
121 let path = self.data_dir.bundle_path(fv);
122 if !path.exists() {
123 return Err(MapperError::BundleNotFound { fv: fv.to_string() });
124 }
125 let bundle = DataBundle::load(&path)?;
126 bundles.insert(fv.to_string(), bundle);
127 Ok(())
128 }
129
130 pub fn conversion_service(
134 &self,
135 fv: &str,
136 variant: &str,
137 ) -> Result<ConversionService, MapperError> {
138 self.ensure_bundle_loaded(fv)?;
139 let bundles = self.bundles.lock().unwrap();
140 let bundle = bundles.get(fv).unwrap();
141 let vc = bundle
142 .variant(variant)
143 .ok_or_else(|| MapperError::VariantNotFound {
144 fv: fv.to_string(),
145 variant: variant.to_string(),
146 })?;
147 let mig = vc
148 .mig_schema
149 .as_ref()
150 .ok_or_else(|| MapperError::VariantNotFound {
151 fv: fv.to_string(),
152 variant: format!("{variant} (no MIG schema in bundle)"),
153 })?;
154 Ok(ConversionService::from_mig(mig.clone()))
155 }
156
157 pub fn engine(&self, fv: &str, variant: &str, pid: &str) -> Result<MappingEngine, MapperError> {
161 self.ensure_bundle_loaded(fv)?;
162 let bundles = self.bundles.lock().unwrap();
163 let bundle = bundles.get(fv).unwrap();
164 let vc = bundle
165 .variant(variant)
166 .ok_or_else(|| MapperError::VariantNotFound {
167 fv: fv.to_string(),
168 variant: variant.to_string(),
169 })?;
170 let pid_key = format!("pid_{pid}");
171 let defs = vc
172 .combined_defs
173 .get(&pid_key)
174 .ok_or_else(|| MapperError::PidNotFound {
175 fv: fv.to_string(),
176 variant: variant.to_string(),
177 pid: pid.to_string(),
178 })?;
179 Ok(MappingEngine::from_definitions(defs.clone()))
180 }
181
182 pub fn pid_requirements(
187 &self,
188 fv: &str,
189 variant: &str,
190 pid: &str,
191 ) -> Result<mig_bo4e::pid_requirements::PidRequirements, MapperError> {
192 self.ensure_bundle_loaded(fv)?;
193 let bundles = self.bundles.lock().unwrap();
194 let bundle = bundles.get(fv).unwrap();
195 let vc = bundle
196 .variant(variant)
197 .ok_or_else(|| MapperError::VariantNotFound {
198 fv: fv.to_string(),
199 variant: variant.to_string(),
200 })?;
201 let pid_key = format!("pid_{pid}");
202 vc.pid_requirements
203 .get(&pid_key)
204 .cloned()
205 .ok_or_else(|| MapperError::PidNotFound {
206 fv: fv.to_string(),
207 variant: variant.to_string(),
208 pid: pid.to_string(),
209 })
210 }
211
212 pub fn bo4e_catalog(
218 &self,
219 fv: &str,
220 ) -> Result<mig_bo4e::bo4e_catalog::Bo4eCatalog, MapperError> {
221 self.ensure_bundle_loaded(fv)?;
222 let bundles = self.bundles.lock().unwrap();
223 let bundle = bundles.get(fv).unwrap();
224 Ok(bundle.bo4e_catalog.clone())
225 }
226
227 pub fn list_pids(&self) -> Result<Vec<PidListEntry>, MapperError> {
232 let dir = self.data_dir.data_path();
233 let read_dir = std::fs::read_dir(dir).map_err(|_| MapperError::DataDirNotFound {
234 path: dir.display().to_string(),
235 })?;
236
237 let mut result = Vec::new();
238
239 for entry in read_dir.flatten() {
240 let path = entry.path();
241 if path.extension().is_some_and(|e| e == "bin") {
242 let stem = path
243 .file_stem()
244 .and_then(|s| s.to_str())
245 .unwrap_or("")
246 .to_string();
247 let fv = match stem.strip_prefix("edifact-data-") {
248 Some(v) => v.to_string(),
249 None => continue,
250 };
251 self.ensure_bundle_loaded(&fv)?;
252 let bundles = self.bundles.lock().unwrap();
253 if let Some(bundle) = bundles.get(&fv) {
254 for (variant, vc) in &bundle.variants {
255 for (pid_key, req) in &vc.pid_requirements {
256 let pid = pid_key.strip_prefix("pid_").unwrap_or(pid_key).to_string();
257 result.push(PidListEntry {
258 fv: fv.clone(),
259 variant: variant.clone(),
260 pid,
261 beschreibung: req.beschreibung.clone(),
262 });
263 }
264 }
265 }
266 }
267 }
268
269 result.sort_by(|a, b| a.pid.cmp(&b.pid));
270 Ok(result)
271 }
272
273 pub fn validate_pid(
278 &self,
279 json: &serde_json::Value,
280 fv: &str,
281 variant: &str,
282 pid: &str,
283 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
284 self.ensure_bundle_loaded(fv)?;
285 let bundles = self.bundles.lock().unwrap();
286 let bundle = bundles.get(fv).unwrap();
287 let vc = bundle
288 .variant(variant)
289 .ok_or_else(|| MapperError::VariantNotFound {
290 fv: fv.to_string(),
291 variant: variant.to_string(),
292 })?;
293 let pid_key = format!("pid_{pid}");
294 let requirements =
295 vc.pid_requirements
296 .get(&pid_key)
297 .ok_or_else(|| MapperError::PidNotFound {
298 fv: fv.to_string(),
299 variant: variant.to_string(),
300 pid: pid.to_string(),
301 })?;
302
303 Ok(mig_bo4e::pid_validation::validate_pid_json(
304 json,
305 requirements,
306 ))
307 }
308
309 pub fn validate_pid_struct(
321 &self,
322 value: &impl serde::Serialize,
323 fv: &str,
324 variant: &str,
325 pid: &str,
326 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
327 let json = serde_json::to_value(value).map_err(|e| {
328 MapperError::Mapping(mig_bo4e::MappingError::TypeConversion(e.to_string()))
329 })?;
330 self.validate_pid(&json, fv, variant, pid)
331 }
332
333 pub fn validate_pid_with_conditions(
341 &self,
342 json: &serde_json::Value,
343 fv: &str,
344 variant: &str,
345 pid: &str,
346 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
347 self.ensure_bundle_loaded(fv)?;
348 let bundles = self.bundles.lock().unwrap();
349 let bundle = bundles.get(fv).unwrap();
350 let vc = bundle
351 .variant(variant)
352 .ok_or_else(|| MapperError::VariantNotFound {
353 fv: fv.to_string(),
354 variant: variant.to_string(),
355 })?;
356 let pid_key = format!("pid_{pid}");
357
358 let requirements =
359 vc.pid_requirements
360 .get(&pid_key)
361 .ok_or_else(|| MapperError::PidNotFound {
362 fv: fv.to_string(),
363 variant: variant.to_string(),
364 pid: pid.to_string(),
365 })?;
366
367 let evaluator = crate::evaluator_factory::create_evaluator(variant, fv);
369
370 if let Some(evaluator) = evaluator {
371 let defs = vc
373 .combined_defs
374 .get(&pid_key)
375 .ok_or_else(|| MapperError::PidNotFound {
376 fv: fv.to_string(),
377 variant: variant.to_string(),
378 pid: pid.to_string(),
379 })?;
380 let engine = MappingEngine::from_definitions(defs.clone());
381 let tree = engine.map_all_reverse(json, None);
382
383 let segments = crate::tree_to_segments::tree_to_owned_segments(&tree);
385
386 Ok(crate::evaluator_factory::validate_with_boxed_evaluator(
388 evaluator.as_ref(),
389 json,
390 requirements,
391 pid,
392 &segments,
393 ))
394 } else {
395 Ok(mig_bo4e::pid_validation::validate_pid_json_transaction(
397 json,
398 requirements,
399 ))
400 }
401 }
402
403 pub fn to_edifact(
437 &self,
438 msg_stammdaten: &serde_json::Value,
439 tx_stammdaten: &[serde_json::Value],
440 fv: &str,
441 variant: &str,
442 pid: &str,
443 ) -> Result<String, MapperError> {
444 self.render_message_body(
445 msg_stammdaten,
446 tx_stammdaten,
447 fv,
448 variant,
449 pid,
450 EntrySegmentCheck::Refuse,
451 )
452 }
453
454 fn render_message_body(
462 &self,
463 msg_stammdaten: &serde_json::Value,
464 tx_stammdaten: &[serde_json::Value],
465 fv: &str,
466 variant: &str,
467 pid: &str,
468 check: EntrySegmentCheck,
469 ) -> Result<String, MapperError> {
470 self.ensure_bundle_loaded(fv)?;
471 let bundles = self.bundles.lock().unwrap();
472 let bundle = bundles.get(fv).unwrap();
473 let vc = bundle
474 .variant(variant)
475 .ok_or_else(|| MapperError::VariantNotFound {
476 fv: fv.to_string(),
477 variant: variant.to_string(),
478 })?;
479
480 let tx_group = vc.tx_group(pid).ok_or_else(|| MapperError::PidNotFound {
481 fv: fv.to_string(),
482 variant: variant.to_string(),
483 pid: pid.to_string(),
484 })?;
485
486 let msg_engine = vc.msg_engine(pid);
487 let tx_engine = vc.tx_engine(pid).ok_or_else(|| MapperError::PidNotFound {
488 fv: fv.to_string(),
489 variant: variant.to_string(),
490 pid: pid.to_string(),
491 })?;
492
493 let filtered_mig = vc
494 .filtered_mig(pid)
495 .ok_or_else(|| MapperError::NoMigSchema {
496 fv: fv.to_string(),
497 variant: variant.to_string(),
498 })?;
499
500 let transaktionen: Vec<mig_bo4e::model::MappedTransaktion> =
502 tx_stammdaten.iter().map(split_transaktion).collect();
503 let mapped = mig_bo4e::model::MappedMessage {
504 nachricht_meta: serde_json::Value::Null,
505 stammdaten: msg_stammdaten.clone(),
506 transaktionen,
507 nesting_info: Default::default(),
508 inter_group_segments: Default::default(),
509 };
510
511 let tree = MappingEngine::map_interchange_reverse(
513 &msg_engine,
514 &tx_engine,
515 &mapped,
516 tx_group,
517 Some(&filtered_mig),
518 );
519
520 let disassembler = mig_assembly::disassembler::Disassembler::new(&filtered_mig);
525 let checked = match check {
526 EntrySegmentCheck::Refuse => disassembler.disassemble_checked(&tree),
527 EntrySegmentCheck::Render => Ok(disassembler.disassemble(&tree)),
528 };
529 let segments = checked.map_err(|e| match e {
530 mig_assembly::AssemblyError::MissingGroupEntrySegment {
531 group_path,
532 source_path,
533 entry_segment,
534 present_segments,
535 } => {
536 let (entities, entry_fields) = describe_entry_segment_mappings(
537 [msg_engine.definitions(), tx_engine.definitions()],
538 &source_path,
539 &entry_segment,
540 );
541 MapperError::MissingGroupEntrySegment(Box::new(
542 crate::error::GroupEntrySegmentError {
543 pid: pid.to_string(),
544 group_path,
545 source_path,
546 entry_segment,
547 present_segments,
548 entities,
549 entry_fields,
550 },
551 ))
552 }
553 other => MapperError::Assembly(other),
554 })?;
555
556 let delimiters = edifact_primitives::EdifactDelimiters::default();
558 Ok(mig_assembly::renderer::render_edifact(
559 &segments,
560 &delimiters,
561 ))
562 }
563
564 pub fn to_edifact_struct(
570 &self,
571 nachricht: &impl serde::Serialize,
572 fv: &str,
573 variant: &str,
574 pid: &str,
575 ) -> Result<String, MapperError> {
576 let json = serde_json::to_value(nachricht)
577 .map_err(|e| MapperError::Serialization(e.to_string()))?;
578
579 let msg_stammdaten = json
580 .get("stammdaten")
581 .cloned()
582 .unwrap_or(serde_json::Value::Object(Default::default()));
583
584 let tx_stammdaten: Vec<serde_json::Value> = json
585 .get("transaktionen")
586 .and_then(|v| v.as_array())
587 .cloned()
588 .unwrap_or_default();
589
590 self.to_edifact(&msg_stammdaten, &tx_stammdaten, fv, variant, pid)
591 }
592
593 pub fn from_edifact<M, T>(
621 &self,
622 edifact: &str,
623 fv: &str,
624 variant: &str,
625 pid: &str,
626 ) -> Result<mig_bo4e::model::Interchange<M, T>, MapperError>
627 where
628 M: serde::de::DeserializeOwned,
629 T: serde::de::DeserializeOwned,
630 {
631 let (interchange, diagnostics) =
632 self.from_edifact_with_diagnostics(edifact, fv, variant, pid)?;
633 for d in &diagnostics {
636 tracing::warn!(
637 fv,
638 variant,
639 pid,
640 kind = ?d.kind,
641 segment = %d.segment_id,
642 position = d.position,
643 "from_edifact: {}",
644 d.message
645 );
646 }
647 Ok(interchange)
648 }
649
650 pub fn from_edifact_with_diagnostics<M, T>(
663 &self,
664 edifact: &str,
665 fv: &str,
666 variant: &str,
667 pid: &str,
668 ) -> Result<
669 (
670 mig_bo4e::model::Interchange<M, T>,
671 Vec<mig_assembly::StructureDiagnostic>,
672 ),
673 MapperError,
674 >
675 where
676 M: serde::de::DeserializeOwned,
677 T: serde::de::DeserializeOwned,
678 {
679 self.ensure_bundle_loaded(fv)?;
680 let bundles = self.bundles.lock().unwrap();
681 let bundle = bundles.get(fv).unwrap();
682 let vc = bundle
683 .variant(variant)
684 .ok_or_else(|| MapperError::VariantNotFound {
685 fv: fv.to_string(),
686 variant: variant.to_string(),
687 })?;
688
689 let tx_group = vc.tx_group(pid).ok_or_else(|| MapperError::PidNotFound {
690 fv: fv.to_string(),
691 variant: variant.to_string(),
692 pid: pid.to_string(),
693 })?;
694
695 let msg_engine = vc.msg_engine(pid);
696 let tx_engine = vc.tx_engine(pid).ok_or_else(|| MapperError::PidNotFound {
697 fv: fv.to_string(),
698 variant: variant.to_string(),
699 pid: pid.to_string(),
700 })?;
701
702 let filtered_mig = vc
703 .filtered_mig(pid)
704 .ok_or_else(|| MapperError::NoMigSchema {
705 fv: fv.to_string(),
706 variant: variant.to_string(),
707 })?;
708
709 let svc = ConversionService::from_mig(filtered_mig);
715 let (chunks, trees, assembly_diagnostics) = svc
716 .convert_interchange_to_trees_with_diagnostics(
717 edifact,
718 mig_assembly::assembler::AssemblerConfig {
719 strict_code_matching: true,
720 skip_unknown_segments: true,
721 ..Default::default()
722 },
723 )?;
724
725 let tree = trees.first().ok_or_else(|| {
726 MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
727 "No messages in interchange".to_string(),
728 ))
729 })?;
730
731 let interchangedaten = mig_bo4e::model::extract_interchangedaten(&chunks.envelope);
733 let msg_chunk = chunks.messages.first().ok_or_else(|| {
734 MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
735 "No message chunks".to_string(),
736 ))
737 })?;
738 let (unh_ref, nachrichten_typ) = mig_bo4e::model::extract_unh_fields(&msg_chunk.unh);
739 let nachrichtendaten = mig_bo4e::model::Nachrichtendaten {
740 unh_referenz: unh_ref,
741 nachrichten_typ,
742 nachricht: Default::default(),
743 };
744
745 let interchange = MappingEngine::map_interchange_typed::<M, T>(
747 &msg_engine,
748 &tx_engine,
749 tree,
750 tx_group,
751 true,
752 nachrichtendaten,
753 interchangedaten,
754 )
755 .map_err(|e| MapperError::Serialization(e.to_string()))?;
756
757 Ok((interchange, assembly_diagnostics))
758 }
759
760 pub fn detect_pid(&self, edifact: &str) -> Result<String, MapperError> {
772 let segments = mig_assembly::tokenize::parse_to_segments(edifact.as_bytes())?;
773 let chunks = mig_assembly::split_messages(segments)?;
774 let msg_chunk = chunks.messages.first().ok_or_else(|| {
775 MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
776 "No messages found in EDIFACT content".to_string(),
777 ))
778 })?;
779 let msg_segments = msg_chunk.message_segments();
780 mig_assembly::pid_detect::detect_pid(&msg_segments).map_err(MapperError::Assembly)
781 }
782
783 pub fn validate_edifact(
799 &self,
800 edifact: &str,
801 fv: &str,
802 level: automapper_validation::ValidationLevel,
803 ) -> Result<automapper_validation::ValidationReport, MapperError> {
804 self.validate_edifact_inner(edifact, fv, None, level)
805 }
806
807 pub fn validate_edifact_for_pid(
817 &self,
818 edifact: &str,
819 fv: &str,
820 variant: &str,
821 pid: &str,
822 level: automapper_validation::ValidationLevel,
823 ) -> Result<automapper_validation::ValidationReport, MapperError> {
824 self.validate_edifact_inner(edifact, fv, Some((variant, pid)), level)
825 }
826
827 fn validate_edifact_inner(
828 &self,
829 edifact: &str,
830 fv: &str,
831 known: Option<(&str, &str)>,
832 level: automapper_validation::ValidationLevel,
833 ) -> Result<automapper_validation::ValidationReport, MapperError> {
834 self.ensure_bundle_loaded(fv)?;
835 let bundles = self.bundles.lock().unwrap();
836 let bundle = bundles.get(fv).unwrap();
837
838 let segments = mig_assembly::tokenize::parse_to_segments(edifact.as_bytes())?;
840 let chunks = mig_assembly::split_messages(segments)?;
841 let msg_chunk = chunks.messages.first().ok_or_else(|| {
842 MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
843 "No messages found in EDIFACT content".to_string(),
844 ))
845 })?;
846
847 let (pid, variant, vc) = match known {
853 Some((variant, pid)) => {
854 let vc = bundle
855 .variant(variant)
856 .ok_or_else(|| MapperError::VariantNotFound {
857 fv: fv.to_string(),
858 variant: variant.to_string(),
859 })?;
860 (pid.to_string(), variant.to_string(), vc)
861 }
862 None => {
863 let pid = mig_assembly::pid_detect::detect_pid(&msg_chunk.message_segments())
864 .map_err(MapperError::Assembly)?;
865 let pid_key = format!("pid_{pid}");
866 let (variant, vc) = bundle
867 .variants
868 .iter()
869 .find(|(_, vc)| vc.pid_ahb_workflows.contains_key(&pid_key))
870 .ok_or_else(|| MapperError::PidNotFound {
871 fv: fv.to_string(),
872 variant: "?".to_string(),
873 pid: pid.clone(),
874 })?;
875 (pid, variant.clone(), vc)
876 }
877 };
878 let pid_key = format!("pid_{pid}");
879
880 let workflow =
881 vc.pid_ahb_workflows
882 .get(&pid_key)
883 .ok_or_else(|| MapperError::PidNotFound {
884 fv: fv.to_string(),
885 variant: variant.clone(),
886 pid: pid.clone(),
887 })?;
888 let filtered_mig = vc
889 .filtered_mig(&pid)
890 .ok_or_else(|| MapperError::NoMigSchema {
891 fv: fv.to_string(),
892 variant: variant.clone(),
893 })?;
894
895 let mut all_segments = msg_chunk.segments_for_mig(&filtered_mig);
898 if filtered_mig.segments.iter().any(|s| s.id == "UNZ") {
899 if let Some(unz) = &chunks.unz {
900 all_segments.push(unz.clone());
901 }
902 }
903
904 let evaluator: std::sync::Arc<dyn automapper_validation::ConditionEvaluator> =
908 match crate::evaluator_factory::create_evaluator(&variant, fv) {
909 Some(boxed) => std::sync::Arc::from(boxed),
910 None => std::sync::Arc::new(
911 automapper_validation::UtilmdStromConditionEvaluatorFV2504::default(),
912 ),
913 };
914 let external = automapper_validation::eval::NoOpExternalProvider;
915
916 let mut report = automapper_validation::validate_edifact_message(
917 &all_segments,
918 &filtered_mig,
919 workflow,
920 evaluator,
921 &external,
922 level,
923 );
924
925 if let (Some(mig), Some(defs)) = (vc.mig_schema.as_ref(), vc.combined_defs.get(&pid_key)) {
931 let reverse = mig_bo4e::path_resolver::ReversePathResolver::from_mig(mig);
932 let field_index =
933 mig_bo4e::Bo4eFieldIndex::build_with_resolver(defs, &filtered_mig, &reverse);
934 report.enrich_bo4e_paths(|path, hint| field_index.resolve(path, hint));
935 }
936
937 Ok(report)
938 }
939
940 pub fn validate_bo4e(
963 &self,
964 msg_stammdaten: &serde_json::Value,
965 tx_stammdaten: &[serde_json::Value],
966 fv: &str,
967 variant: &str,
968 pid: &str,
969 envelope: Option<&InterchangeEnvelope>,
970 level: automapper_validation::ValidationLevel,
971 ) -> Result<automapper_validation::ValidationReport, MapperError> {
972 let placeholder;
973 let envelope = match envelope {
974 Some(e) => e,
975 None => {
976 placeholder = InterchangeEnvelope {
977 sender: EdifactParty::bdew("9900000000001"),
978 receiver: EdifactParty::bdew("9900000000002"),
979 interchange_ref: "1".to_string(),
980 };
981 &placeholder
982 }
983 };
984
985 let edifact = self.render_interchange(
990 envelope,
991 &[InterchangeMessage {
992 message_ref: "1".to_string(),
993 msg_stammdaten: msg_stammdaten.clone(),
994 tx_stammdaten: tx_stammdaten.to_vec(),
995 fv: fv.to_string(),
996 variant: variant.to_string(),
997 pid: pid.to_string(),
998 }],
999 EntrySegmentCheck::Render,
1000 )?;
1001
1002 self.validate_edifact_for_pid(&edifact, fv, variant, pid, level)
1005 }
1006
1007 pub fn association_code(&self, fv: &str, variant: &str) -> Result<String, MapperError> {
1018 let meta = self.message_metadata(fv, variant)?;
1019 Ok(meta.association_code)
1020 }
1021
1022 pub fn message_metadata(
1027 &self,
1028 fv: &str,
1029 variant: &str,
1030 ) -> Result<MessageMetadata, MapperError> {
1031 self.ensure_bundle_loaded(fv)?;
1032 let bundles = self.bundles.lock().unwrap();
1033 let bundle = bundles.get(fv).unwrap();
1034 let vc = bundle
1035 .variant(variant)
1036 .ok_or_else(|| MapperError::VariantNotFound {
1037 fv: fv.to_string(),
1038 variant: variant.to_string(),
1039 })?;
1040 let mig = vc
1041 .mig_schema
1042 .as_ref()
1043 .ok_or_else(|| MapperError::NoMigSchema {
1044 fv: fv.to_string(),
1045 variant: variant.to_string(),
1046 })?;
1047 Ok(MessageMetadata {
1048 message_type: mig.message_type.clone(),
1049 release: release_code_for_message_type(&mig.message_type),
1050 association_code: mig.version.clone(),
1051 })
1052 }
1053
1054 pub fn to_edifact_interchange(
1084 &self,
1085 envelope: &InterchangeEnvelope,
1086 messages: &[InterchangeMessage],
1087 ) -> Result<String, MapperError> {
1088 self.render_interchange(envelope, messages, EntrySegmentCheck::Refuse)
1089 }
1090
1091 fn render_interchange(
1092 &self,
1093 envelope: &InterchangeEnvelope,
1094 messages: &[InterchangeMessage],
1095 check: EntrySegmentCheck,
1096 ) -> Result<String, MapperError> {
1097 let delimiters = edifact_primitives::EdifactDelimiters::default();
1098 let sep = delimiters.component as char;
1099 let elem = delimiters.element as char;
1100 let seg_term = delimiters.segment as char;
1101
1102 let mut output = String::new();
1103
1104 output.push_str(&format!(
1106 "UNA{}{}{}{}{}{}",
1107 sep, elem, delimiters.decimal as char, delimiters.release as char, ' ', seg_term, ));
1114
1115 let now = chrono::Utc::now();
1117 let date_str = now.format("%y%m%d").to_string();
1118 let time_str = now.format("%H%M").to_string();
1119 let sender = &envelope.sender;
1120 let receiver = &envelope.receiver;
1121 let interchange_ref = &envelope.interchange_ref;
1122 output.push_str(&format!(
1123 "UNB{elem}UNOC{sep}3{elem}{sid}{sep}{sq}{elem}{rid}{sep}{rq}{elem}{date_str}{sep}{time_str}{elem}{interchange_ref}{seg_term}",
1124 sid = sender.id,
1125 sq = sender.qualifier,
1126 rid = receiver.id,
1127 rq = receiver.qualifier,
1128 ));
1129
1130 let mut message_count = 0u32;
1131
1132 for msg in messages {
1133 let meta = self.message_metadata(&msg.fv, &msg.variant)?;
1134
1135 let body = self.render_message_body(
1137 &msg.msg_stammdaten,
1138 &msg.tx_stammdaten,
1139 &msg.fv,
1140 &msg.variant,
1141 &msg.pid,
1142 check,
1143 )?;
1144
1145 let body_seg_count = body
1147 .split(seg_term)
1148 .filter(|s: &&str| !s.is_empty())
1149 .count();
1150 let segment_count = body_seg_count + 2;
1152
1153 output.push_str(&format!(
1155 "UNH{elem}{ref}{elem}{msg_type}{sep}D{sep}{release}{sep}UN{sep}{assoc}{seg_term}",
1156 ref = msg.message_ref,
1157 msg_type = meta.message_type,
1158 release = meta.release,
1159 assoc = meta.association_code,
1160 ));
1161
1162 output.push_str(&body);
1164
1165 output.push_str(&format!(
1167 "UNT{elem}{segment_count}{elem}{ref}{seg_term}",
1168 ref = msg.message_ref,
1169 ));
1170
1171 message_count += 1;
1172 }
1173
1174 output.push_str(&format!(
1176 "UNZ{elem}{message_count}{elem}{interchange_ref}{seg_term}",
1177 ));
1178
1179 Ok(output)
1180 }
1181
1182 pub fn loaded_format_versions(&self) -> Vec<String> {
1184 self.bundles.lock().unwrap().keys().cloned().collect()
1185 }
1186
1187 pub fn variants(&self, fv: &str) -> Result<Vec<String>, MapperError> {
1191 self.ensure_bundle_loaded(fv)?;
1192 let bundles = self.bundles.lock().unwrap();
1193 let bundle = bundles.get(fv).unwrap();
1194 Ok(bundle.variants.keys().cloned().collect())
1195 }
1196}
1197
1198#[derive(Debug, Clone)]
1200pub struct MessageMetadata {
1201 pub message_type: String,
1203 pub release: String,
1205 pub association_code: String,
1207}
1208
1209#[derive(Debug, Clone)]
1211pub struct InterchangeEnvelope {
1212 pub sender: EdifactParty,
1214 pub receiver: EdifactParty,
1216 pub interchange_ref: String,
1218}
1219
1220#[derive(Debug, Clone)]
1222pub struct EdifactParty {
1223 pub id: String,
1225 pub qualifier: String,
1227}
1228
1229impl EdifactParty {
1230 pub fn bdew(id: &str) -> Self {
1232 Self {
1233 id: id.to_string(),
1234 qualifier: "500".to_string(),
1235 }
1236 }
1237
1238 pub fn gs1(id: &str) -> Self {
1240 Self {
1241 id: id.to_string(),
1242 qualifier: "14".to_string(),
1243 }
1244 }
1245}
1246
1247#[derive(Debug, Clone)]
1250pub struct InterchangeMessage {
1251 pub message_ref: String,
1253 pub msg_stammdaten: serde_json::Value,
1255 pub tx_stammdaten: Vec<serde_json::Value>,
1257 pub fv: String,
1259 pub variant: String,
1261 pub pid: String,
1263}
1264
1265#[derive(Debug, Clone, Copy)]
1267enum EntrySegmentCheck {
1268 Refuse,
1270 Render,
1272}
1273
1274fn describe_entry_segment_mappings<'d>(
1284 definition_sets: impl IntoIterator<Item = &'d [mig_bo4e::definition::MappingDefinition]>,
1285 source_path: &str,
1286 entry_segment: &str,
1287) -> (Vec<String>, Vec<String>) {
1288 fn qualifies(unqualified: &str, qualified: &str) -> bool {
1289 !unqualified.contains('_')
1290 && qualified.len() > unqualified.len()
1291 && qualified.is_char_boundary(unqualified.len())
1292 && qualified[..unqualified.len()].eq_ignore_ascii_case(unqualified)
1293 && qualified.as_bytes()[unqualified.len()] == b'_'
1294 }
1295 fn part_matches(mig_part: &str, def_part: &str) -> bool {
1296 def_part.eq_ignore_ascii_case(mig_part)
1297 || qualifies(mig_part, def_part)
1298 || qualifies(def_part, mig_part)
1299 }
1300 let mig_parts: Vec<&str> = source_path.split('.').collect();
1301
1302 let mut entities: Vec<String> = Vec::new();
1303 let mut entry_fields: Vec<String> = Vec::new();
1304 for def in definition_sets.into_iter().flatten() {
1305 let Some(def_path) = def.meta.source_path.as_deref() else {
1306 continue;
1307 };
1308 let def_parts: Vec<&str> = def_path.split('.').collect();
1309 if def_parts.len() != mig_parts.len()
1310 || !mig_parts
1311 .iter()
1312 .zip(&def_parts)
1313 .all(|(m, d)| part_matches(m, d))
1314 {
1315 continue;
1316 }
1317 if !entities.contains(&def.meta.entity) {
1318 entities.push(def.meta.entity.clone());
1319 }
1320 for (path, mapping) in &def.fields {
1321 let tag = path
1322 .split(['.', '['])
1323 .next()
1324 .unwrap_or_default()
1325 .to_ascii_uppercase();
1326 let target = match mapping {
1327 mig_bo4e::definition::FieldMapping::Simple(t) => t.as_str(),
1328 mig_bo4e::definition::FieldMapping::Structured(f) => f.target.as_str(),
1329 mig_bo4e::definition::FieldMapping::Nested(_) => continue,
1330 };
1331 if tag == entry_segment && !target.is_empty() {
1332 let field = format!("{}.{}", def.meta.entity, target);
1333 if !entry_fields.contains(&field) {
1334 entry_fields.push(field);
1335 }
1336 }
1337 }
1338 }
1339 (entities, entry_fields)
1340}
1341
1342fn release_code_for_message_type(msg_type: &str) -> String {
1346 mig_bo4e::model::release_code_for_message_type(msg_type).to_string()
1347}
1348
1349#[cfg(test)]
1350mod tests {
1351 use super::*;
1352 use std::path::Path;
1353
1354 fn data_dir() -> Option<std::path::PathBuf> {
1355 let dist = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../dist");
1357 if dist.join("edifact-data-FV2504.bin").exists() {
1358 return Some(dist);
1359 }
1360 let cache = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../cache/mappings");
1361 if cache.join("FV2504").exists() {
1362 return Some(cache);
1363 }
1364 eprintln!("Skipping test: no DataBundle files found");
1365 None
1366 }
1367
1368 #[test]
1369 fn test_to_edifact_produces_edifact_output() {
1370 let Some(data_dir) = data_dir() else {
1371 return;
1372 };
1373 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1374
1375 let msg_stammdaten = serde_json::json!({
1376 "marktteilnehmer": [{
1377 "marktrolle": "MS",
1378 "rollencodenummer": "9900123456789",
1379 "codepflegeCode": "293"
1380 }]
1381 });
1382 let tx_stammdaten = serde_json::json!({
1383 "prozessdaten": {
1384 "pruefidentifikator": "55001",
1385 "vorgangId": "ABC123",
1386 "transaktionsgrund": "E01"
1387 }
1388 });
1389
1390 let result = mapper.to_edifact(
1391 &msg_stammdaten,
1392 &[tx_stammdaten],
1393 "FV2504",
1394 "UTILMD_Strom",
1395 "55001",
1396 );
1397 assert!(result.is_ok(), "to_edifact failed: {:?}", result.err());
1398 let edifact = result.unwrap();
1399 assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
1400 assert!(edifact.contains("NAD"), "Should contain NAD segment");
1402 assert!(edifact.contains("IDE"), "Should contain IDE segment");
1404 }
1405
1406 #[test]
1407 fn test_to_edifact_struct_produces_edifact_output() {
1408 let Some(data_dir) = data_dir() else {
1409 return;
1410 };
1411 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1412
1413 let nachricht = serde_json::json!({
1414 "stammdaten": {
1415 "marktteilnehmer": [{
1416 "marktrolle": "MS",
1417 "rollencodenummer": "9900123456789",
1418 "codepflegeCode": "293"
1419 }]
1420 },
1421 "transaktionen": [{
1422 "prozessdaten": {
1423 "pruefidentifikator": "55001",
1424 "vorgangId": "ABC123"
1425 }
1426 }]
1427 });
1428
1429 let result = mapper.to_edifact_struct(&nachricht, "FV2504", "UTILMD_Strom", "55001");
1430 assert!(
1431 result.is_ok(),
1432 "to_edifact_struct failed: {:?}",
1433 result.err()
1434 );
1435 let edifact = result.unwrap();
1436 assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
1437 }
1438
1439 #[test]
1440 fn test_to_edifact_invalid_fv_returns_error() {
1441 let Some(data_dir) = data_dir() else {
1442 return;
1443 };
1444 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1445
1446 let result = mapper.to_edifact(
1447 &serde_json::json!({}),
1448 &[serde_json::json!({})],
1449 "FV9999",
1450 "UTILMD_Strom",
1451 "55001",
1452 );
1453 assert!(result.is_err());
1454 }
1455
1456 #[test]
1457 fn test_to_edifact_invalid_variant_returns_error() {
1458 let Some(data_dir) = data_dir() else {
1459 return;
1460 };
1461 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1462
1463 let result = mapper.to_edifact(
1464 &serde_json::json!({}),
1465 &[serde_json::json!({})],
1466 "FV2504",
1467 "NONEXISTENT",
1468 "55001",
1469 );
1470 assert!(result.is_err());
1471 }
1472
1473 #[test]
1474 fn test_to_edifact_invalid_pid_returns_error() {
1475 let Some(data_dir) = data_dir() else {
1476 return;
1477 };
1478 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1479
1480 let result = mapper.to_edifact(
1481 &serde_json::json!({}),
1482 &[serde_json::json!({})],
1483 "FV2504",
1484 "UTILMD_Strom",
1485 "99999",
1486 );
1487 assert!(result.is_err());
1488 }
1489
1490 #[test]
1491 fn test_association_code() {
1492 let Some(data_dir) = data_dir() else {
1493 return;
1494 };
1495 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1496
1497 let code = mapper.association_code("FV2504", "UTILMD_Strom").unwrap();
1498 assert_eq!(code, "S2.1");
1499
1500 let code = mapper.association_code("FV2504", "MSCONS").unwrap();
1501 assert_eq!(code, "2.4c");
1502 }
1503
1504 #[test]
1505 fn test_message_metadata() {
1506 let Some(data_dir) = data_dir() else {
1507 return;
1508 };
1509 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1510
1511 let meta = mapper.message_metadata("FV2504", "UTILMD_Strom").unwrap();
1512 assert_eq!(meta.message_type, "UTILMD");
1513 assert_eq!(meta.release, "11A");
1514 assert_eq!(meta.association_code, "S2.1");
1515 }
1516
1517 #[test]
1518 fn test_to_edifact_interchange() {
1519 let Some(data_dir) = data_dir() else {
1520 return;
1521 };
1522 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1523
1524 let result = mapper.to_edifact_interchange(
1525 &InterchangeEnvelope {
1526 sender: EdifactParty::bdew("9900000000003"),
1527 receiver: EdifactParty::bdew("9900000000001"),
1528 interchange_ref: "REF001".to_string(),
1529 },
1530 &[InterchangeMessage {
1531 message_ref: "MSG001".to_string(),
1532 msg_stammdaten: serde_json::json!({
1533 "marktteilnehmer": [{
1534 "marktrolle": "MS",
1535 "rollencodenummer": "9900123456789",
1536 "codepflegeCode": "293"
1537 }]
1538 }),
1539 tx_stammdaten: vec![serde_json::json!({
1540 "prozessdaten": {
1541 "pruefidentifikator": "55001",
1542 "vorgangId": "ABC123",
1543 "transaktionsgrund": "E01"
1544 }
1545 })],
1546 fv: "FV2504".to_string(),
1547 variant: "UTILMD_Strom".to_string(),
1548 pid: "55001".to_string(),
1549 }],
1550 );
1551 assert!(
1552 result.is_ok(),
1553 "to_edifact_interchange failed: {:?}",
1554 result.err()
1555 );
1556 let edifact = result.unwrap();
1557
1558 assert!(edifact.starts_with("UNA:+.? '"), "Should start with UNA");
1560 assert!(
1561 edifact.contains("UNB+UNOC:3+9900000000003:500+9900000000001:500+"),
1562 "Should contain UNB with sender/receiver"
1563 );
1564 assert!(
1565 edifact.contains("UNH+MSG001+UTILMD:D:11A:UN:S2.1'"),
1566 "Should contain UNH with correct S009"
1567 );
1568 assert!(edifact.contains("NAD"), "Should contain body NAD segment");
1569 assert!(edifact.contains("UNT+"), "Should contain UNT");
1570 assert!(
1571 edifact.contains("+MSG001'"),
1572 "UNT should reference message ref"
1573 );
1574 assert!(
1575 edifact.contains("UNZ+1+REF001'"),
1576 "Should contain UNZ with count and ref"
1577 );
1578 }
1579
1580 #[test]
1581 fn test_detect_pid_from_rff_z13() {
1582 let Some(data_dir) = data_dir() else {
1583 return;
1584 };
1585 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1586
1587 let edifact = "\
1588 UNB+UNOC:3+9978842000002:500+9900269000000:500+250331:1329+REF001'\
1589 UNH+MSG001+UTILMD:D:11A:UN:S2.1'\
1590 BGM+E01+DOC001'\
1591 DTM+137:202503311329?+00:303'\
1592 NAD+MS+9978842000002::293'\
1593 NAD+MR+9900269000000::293'\
1594 IDE+24+TX001'\
1595 DTM+92:202505312200?+00:303'\
1596 DTM+93:202512312300?+00:303'\
1597 STS+7++E01+ZW4+E03'\
1598 LOC+Z16+12345678900'\
1599 RFF+Z13:55001'\
1600 UNT+12+MSG001'\
1601 UNZ+1+REF001'";
1602
1603 let pid = mapper.detect_pid(edifact).unwrap();
1604 assert_eq!(pid, "55001");
1605 }
1606
1607 #[test]
1608 fn test_detect_pid_no_messages_returns_error() {
1609 let Some(data_dir) = data_dir() else {
1610 return;
1611 };
1612 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1613
1614 let edifact = "UNB+UNOC:3+SENDER:500+RECEIVER:500+250401:1200+REF'\
1615 UNZ+0+REF'";
1616 assert!(mapper.detect_pid(edifact).is_err());
1617 }
1618
1619 #[test]
1620 fn test_list_pids_returns_entries() {
1621 let Some(data_dir) = data_dir() else {
1622 return;
1623 };
1624 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir)).unwrap();
1625 let pids = mapper.list_pids().expect("list_pids should succeed");
1626 assert!(!pids.is_empty(), "should return at least one PID");
1627 assert!(
1628 pids.iter().any(|p| p.pid == "55001"),
1629 "should include PID 55001"
1630 );
1631 assert!(
1632 pids.iter().any(|p| p.fv == "FV2504"),
1633 "should include FV2504"
1634 );
1635 assert!(
1636 pids.iter().any(|p| p.variant == "UTILMD_Strom"),
1637 "should include UTILMD_Strom"
1638 );
1639 }
1640
1641 #[test]
1642 fn test_pid_requirements_returns_requirements() {
1643 let Some(data_dir) = data_dir() else {
1644 return;
1645 };
1646 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1647
1648 let req = mapper
1649 .pid_requirements("FV2504", "UTILMD_Strom", "55001")
1650 .expect("pid_requirements should succeed");
1651
1652 assert_eq!(req.pid, "55001");
1653 assert!(
1654 !req.entities.is_empty(),
1655 "55001 should have at least one entity"
1656 );
1657 assert!(
1658 req.entities.iter().any(|e| e.entity == "Prozessdaten"),
1659 "55001 should have a Prozessdaten entity"
1660 );
1661 }
1662}