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
74impl Mapper {
75 pub fn from_data_dir(data_dir: DataDir) -> Result<Self, MapperError> {
80 let mapper = Self {
81 data_dir,
82 bundles: Mutex::new(HashMap::new()),
83 };
84 let eager_fvs: Vec<String> = mapper.data_dir.eager_fvs().to_vec();
85 for fv in &eager_fvs {
86 mapper.ensure_bundle_loaded(fv)?;
87 }
88 Ok(mapper)
89 }
90
91 fn ensure_bundle_loaded(&self, fv: &str) -> Result<(), MapperError> {
93 let mut bundles = self.bundles.lock().unwrap();
94 if bundles.contains_key(fv) {
95 return Ok(());
96 }
97 let path = self.data_dir.bundle_path(fv);
98 if !path.exists() {
99 return Err(MapperError::BundleNotFound { fv: fv.to_string() });
100 }
101 let bundle = DataBundle::load(&path)?;
102 bundles.insert(fv.to_string(), bundle);
103 Ok(())
104 }
105
106 pub fn conversion_service(
110 &self,
111 fv: &str,
112 variant: &str,
113 ) -> Result<ConversionService, MapperError> {
114 self.ensure_bundle_loaded(fv)?;
115 let bundles = self.bundles.lock().unwrap();
116 let bundle = bundles.get(fv).unwrap();
117 let vc = bundle
118 .variant(variant)
119 .ok_or_else(|| MapperError::VariantNotFound {
120 fv: fv.to_string(),
121 variant: variant.to_string(),
122 })?;
123 let mig = vc
124 .mig_schema
125 .as_ref()
126 .ok_or_else(|| MapperError::VariantNotFound {
127 fv: fv.to_string(),
128 variant: format!("{variant} (no MIG schema in bundle)"),
129 })?;
130 Ok(ConversionService::from_mig(mig.clone()))
131 }
132
133 pub fn engine(&self, fv: &str, variant: &str, pid: &str) -> Result<MappingEngine, MapperError> {
137 self.ensure_bundle_loaded(fv)?;
138 let bundles = self.bundles.lock().unwrap();
139 let bundle = bundles.get(fv).unwrap();
140 let vc = bundle
141 .variant(variant)
142 .ok_or_else(|| MapperError::VariantNotFound {
143 fv: fv.to_string(),
144 variant: variant.to_string(),
145 })?;
146 let pid_key = format!("pid_{pid}");
147 let defs = vc
148 .combined_defs
149 .get(&pid_key)
150 .ok_or_else(|| MapperError::PidNotFound {
151 fv: fv.to_string(),
152 variant: variant.to_string(),
153 pid: pid.to_string(),
154 })?;
155 Ok(MappingEngine::from_definitions(defs.clone()))
156 }
157
158 pub fn pid_requirements(
163 &self,
164 fv: &str,
165 variant: &str,
166 pid: &str,
167 ) -> Result<mig_bo4e::pid_requirements::PidRequirements, MapperError> {
168 self.ensure_bundle_loaded(fv)?;
169 let bundles = self.bundles.lock().unwrap();
170 let bundle = bundles.get(fv).unwrap();
171 let vc = bundle
172 .variant(variant)
173 .ok_or_else(|| MapperError::VariantNotFound {
174 fv: fv.to_string(),
175 variant: variant.to_string(),
176 })?;
177 let pid_key = format!("pid_{pid}");
178 vc.pid_requirements
179 .get(&pid_key)
180 .cloned()
181 .ok_or_else(|| MapperError::PidNotFound {
182 fv: fv.to_string(),
183 variant: variant.to_string(),
184 pid: pid.to_string(),
185 })
186 }
187
188 pub fn bo4e_catalog(
194 &self,
195 fv: &str,
196 ) -> Result<mig_bo4e::bo4e_catalog::Bo4eCatalog, MapperError> {
197 self.ensure_bundle_loaded(fv)?;
198 let bundles = self.bundles.lock().unwrap();
199 let bundle = bundles.get(fv).unwrap();
200 Ok(bundle.bo4e_catalog.clone())
201 }
202
203 pub fn list_pids(&self) -> Result<Vec<PidListEntry>, MapperError> {
208 let dir = self.data_dir.data_path();
209 let read_dir = std::fs::read_dir(dir).map_err(|_| MapperError::DataDirNotFound {
210 path: dir.display().to_string(),
211 })?;
212
213 let mut result = Vec::new();
214
215 for entry in read_dir.flatten() {
216 let path = entry.path();
217 if path.extension().is_some_and(|e| e == "bin") {
218 let stem = path
219 .file_stem()
220 .and_then(|s| s.to_str())
221 .unwrap_or("")
222 .to_string();
223 let fv = match stem.strip_prefix("edifact-data-") {
224 Some(v) => v.to_string(),
225 None => continue,
226 };
227 self.ensure_bundle_loaded(&fv)?;
228 let bundles = self.bundles.lock().unwrap();
229 if let Some(bundle) = bundles.get(&fv) {
230 for (variant, vc) in &bundle.variants {
231 for (pid_key, req) in &vc.pid_requirements {
232 let pid = pid_key.strip_prefix("pid_").unwrap_or(pid_key).to_string();
233 result.push(PidListEntry {
234 fv: fv.clone(),
235 variant: variant.clone(),
236 pid,
237 beschreibung: req.beschreibung.clone(),
238 });
239 }
240 }
241 }
242 }
243 }
244
245 result.sort_by(|a, b| a.pid.cmp(&b.pid));
246 Ok(result)
247 }
248
249 pub fn validate_pid(
254 &self,
255 json: &serde_json::Value,
256 fv: &str,
257 variant: &str,
258 pid: &str,
259 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
260 self.ensure_bundle_loaded(fv)?;
261 let bundles = self.bundles.lock().unwrap();
262 let bundle = bundles.get(fv).unwrap();
263 let vc = bundle
264 .variant(variant)
265 .ok_or_else(|| MapperError::VariantNotFound {
266 fv: fv.to_string(),
267 variant: variant.to_string(),
268 })?;
269 let pid_key = format!("pid_{pid}");
270 let requirements =
271 vc.pid_requirements
272 .get(&pid_key)
273 .ok_or_else(|| MapperError::PidNotFound {
274 fv: fv.to_string(),
275 variant: variant.to_string(),
276 pid: pid.to_string(),
277 })?;
278
279 Ok(mig_bo4e::pid_validation::validate_pid_json(
280 json,
281 requirements,
282 ))
283 }
284
285 pub fn validate_pid_struct(
297 &self,
298 value: &impl serde::Serialize,
299 fv: &str,
300 variant: &str,
301 pid: &str,
302 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
303 let json = serde_json::to_value(value).map_err(|e| {
304 MapperError::Mapping(mig_bo4e::MappingError::TypeConversion(e.to_string()))
305 })?;
306 self.validate_pid(&json, fv, variant, pid)
307 }
308
309 pub fn validate_pid_with_conditions(
317 &self,
318 json: &serde_json::Value,
319 fv: &str,
320 variant: &str,
321 pid: &str,
322 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
323 self.ensure_bundle_loaded(fv)?;
324 let bundles = self.bundles.lock().unwrap();
325 let bundle = bundles.get(fv).unwrap();
326 let vc = bundle
327 .variant(variant)
328 .ok_or_else(|| MapperError::VariantNotFound {
329 fv: fv.to_string(),
330 variant: variant.to_string(),
331 })?;
332 let pid_key = format!("pid_{pid}");
333
334 let requirements =
335 vc.pid_requirements
336 .get(&pid_key)
337 .ok_or_else(|| MapperError::PidNotFound {
338 fv: fv.to_string(),
339 variant: variant.to_string(),
340 pid: pid.to_string(),
341 })?;
342
343 let evaluator = crate::evaluator_factory::create_evaluator(variant, fv);
345
346 if let Some(evaluator) = evaluator {
347 let defs = vc
349 .combined_defs
350 .get(&pid_key)
351 .ok_or_else(|| MapperError::PidNotFound {
352 fv: fv.to_string(),
353 variant: variant.to_string(),
354 pid: pid.to_string(),
355 })?;
356 let engine = MappingEngine::from_definitions(defs.clone());
357 let tree = engine.map_all_reverse(json, None);
358
359 let segments = crate::tree_to_segments::tree_to_owned_segments(&tree);
361
362 Ok(crate::evaluator_factory::validate_with_boxed_evaluator(
364 evaluator.as_ref(),
365 json,
366 requirements,
367 pid,
368 &segments,
369 ))
370 } else {
371 Ok(mig_bo4e::pid_validation::validate_pid_json_transaction(
373 json,
374 requirements,
375 ))
376 }
377 }
378
379 pub fn to_edifact(
405 &self,
406 msg_stammdaten: &serde_json::Value,
407 tx_stammdaten: &[serde_json::Value],
408 fv: &str,
409 variant: &str,
410 pid: &str,
411 ) -> Result<String, MapperError> {
412 self.ensure_bundle_loaded(fv)?;
413 let bundles = self.bundles.lock().unwrap();
414 let bundle = bundles.get(fv).unwrap();
415 let vc = bundle
416 .variant(variant)
417 .ok_or_else(|| MapperError::VariantNotFound {
418 fv: fv.to_string(),
419 variant: variant.to_string(),
420 })?;
421
422 let tx_group = vc.tx_group(pid).ok_or_else(|| MapperError::PidNotFound {
423 fv: fv.to_string(),
424 variant: variant.to_string(),
425 pid: pid.to_string(),
426 })?;
427
428 let msg_engine = vc.msg_engine(pid);
429 let tx_engine = vc.tx_engine(pid).ok_or_else(|| MapperError::PidNotFound {
430 fv: fv.to_string(),
431 variant: variant.to_string(),
432 pid: pid.to_string(),
433 })?;
434
435 let filtered_mig = vc
436 .filtered_mig(pid)
437 .ok_or_else(|| MapperError::NoMigSchema {
438 fv: fv.to_string(),
439 variant: variant.to_string(),
440 })?;
441
442 let transaktionen: Vec<mig_bo4e::model::MappedTransaktion> = tx_stammdaten
444 .iter()
445 .map(|tx| mig_bo4e::model::MappedTransaktion {
446 stammdaten: tx.clone(),
447 nesting_info: Default::default(),
448 dp_routing: Default::default(),
449 })
450 .collect();
451 let mapped = mig_bo4e::model::MappedMessage {
452 stammdaten: msg_stammdaten.clone(),
453 transaktionen,
454 nesting_info: Default::default(),
455 dp_routing: Default::default(),
456 inter_group_segments: Default::default(),
457 };
458
459 let tree = MappingEngine::map_interchange_reverse(
461 &msg_engine,
462 &tx_engine,
463 &mapped,
464 tx_group,
465 Some(&filtered_mig),
466 );
467
468 let disassembler = mig_assembly::disassembler::Disassembler::new(&filtered_mig);
470 let segments = disassembler.disassemble(&tree);
471
472 let delimiters = edifact_primitives::EdifactDelimiters::default();
474 Ok(mig_assembly::renderer::render_edifact(
475 &segments,
476 &delimiters,
477 ))
478 }
479
480 pub fn to_edifact_struct(
486 &self,
487 nachricht: &impl serde::Serialize,
488 fv: &str,
489 variant: &str,
490 pid: &str,
491 ) -> Result<String, MapperError> {
492 let json = serde_json::to_value(nachricht)
493 .map_err(|e| MapperError::Serialization(e.to_string()))?;
494
495 let msg_stammdaten = json
496 .get("stammdaten")
497 .cloned()
498 .unwrap_or(serde_json::Value::Object(Default::default()));
499
500 let tx_stammdaten: Vec<serde_json::Value> = json
501 .get("transaktionen")
502 .and_then(|v| v.as_array())
503 .cloned()
504 .unwrap_or_default();
505
506 self.to_edifact(&msg_stammdaten, &tx_stammdaten, fv, variant, pid)
507 }
508
509 pub fn from_edifact<M, T>(
533 &self,
534 edifact: &str,
535 fv: &str,
536 variant: &str,
537 pid: &str,
538 ) -> Result<mig_bo4e::model::Interchange<M, T>, MapperError>
539 where
540 M: serde::de::DeserializeOwned,
541 T: serde::de::DeserializeOwned,
542 {
543 self.from_edifact_with_diagnostics(edifact, fv, variant, pid)
544 .map(|(interchange, _)| interchange)
545 }
546
547 pub fn from_edifact_with_diagnostics<M, T>(
557 &self,
558 edifact: &str,
559 fv: &str,
560 variant: &str,
561 pid: &str,
562 ) -> Result<
563 (
564 mig_bo4e::model::Interchange<M, T>,
565 Vec<mig_assembly::StructureDiagnostic>,
566 ),
567 MapperError,
568 >
569 where
570 M: serde::de::DeserializeOwned,
571 T: serde::de::DeserializeOwned,
572 {
573 self.ensure_bundle_loaded(fv)?;
574 let bundles = self.bundles.lock().unwrap();
575 let bundle = bundles.get(fv).unwrap();
576 let vc = bundle
577 .variant(variant)
578 .ok_or_else(|| MapperError::VariantNotFound {
579 fv: fv.to_string(),
580 variant: variant.to_string(),
581 })?;
582
583 let tx_group = vc.tx_group(pid).ok_or_else(|| MapperError::PidNotFound {
584 fv: fv.to_string(),
585 variant: variant.to_string(),
586 pid: pid.to_string(),
587 })?;
588
589 let msg_engine = vc.msg_engine(pid);
590 let tx_engine = vc.tx_engine(pid).ok_or_else(|| MapperError::PidNotFound {
591 fv: fv.to_string(),
592 variant: variant.to_string(),
593 pid: pid.to_string(),
594 })?;
595
596 let filtered_mig = vc
597 .filtered_mig(pid)
598 .ok_or_else(|| MapperError::NoMigSchema {
599 fv: fv.to_string(),
600 variant: variant.to_string(),
601 })?;
602
603 let svc = ConversionService::from_mig(filtered_mig);
609 let (chunks, trees, assembly_diagnostics) = svc
610 .convert_interchange_to_trees_with_diagnostics(
611 edifact,
612 mig_assembly::assembler::AssemblerConfig {
613 strict_code_matching: true,
614 skip_unknown_segments: true,
615 ..Default::default()
616 },
617 )?;
618
619 let tree = trees.first().ok_or_else(|| {
620 MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
621 "No messages in interchange".to_string(),
622 ))
623 })?;
624
625 let interchangedaten = mig_bo4e::model::extract_interchangedaten(&chunks.envelope);
627 let msg_chunk = chunks.messages.first().ok_or_else(|| {
628 MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
629 "No message chunks".to_string(),
630 ))
631 })?;
632 let (unh_ref, nachrichten_typ) = mig_bo4e::model::extract_unh_fields(&msg_chunk.unh);
633 let nachrichtendaten = mig_bo4e::model::Nachrichtendaten {
634 unh_referenz: unh_ref,
635 nachrichten_typ,
636 };
637
638 let interchange = MappingEngine::map_interchange_typed::<M, T>(
640 &msg_engine,
641 &tx_engine,
642 tree,
643 tx_group,
644 true,
645 nachrichtendaten,
646 interchangedaten,
647 )
648 .map_err(|e| MapperError::Serialization(e.to_string()))?;
649
650 Ok((interchange, assembly_diagnostics))
651 }
652
653 pub fn detect_pid(&self, edifact: &str) -> Result<String, MapperError> {
665 let segments = mig_assembly::tokenize::parse_to_segments(edifact.as_bytes())?;
666 let chunks = mig_assembly::split_messages(segments)?;
667 let msg_chunk = chunks.messages.first().ok_or_else(|| {
668 MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
669 "No messages found in EDIFACT content".to_string(),
670 ))
671 })?;
672 let msg_segments = msg_chunk.message_segments();
673 mig_assembly::pid_detect::detect_pid(&msg_segments).map_err(MapperError::Assembly)
674 }
675
676 pub fn validate_edifact(
692 &self,
693 edifact: &str,
694 fv: &str,
695 level: automapper_validation::ValidationLevel,
696 ) -> Result<automapper_validation::ValidationReport, MapperError> {
697 self.ensure_bundle_loaded(fv)?;
698 let bundles = self.bundles.lock().unwrap();
699 let bundle = bundles.get(fv).unwrap();
700
701 let segments = mig_assembly::tokenize::parse_to_segments(edifact.as_bytes())?;
703 let chunks = mig_assembly::split_messages(segments)?;
704 let msg_chunk = chunks.messages.first().ok_or_else(|| {
705 MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
706 "No messages found in EDIFACT content".to_string(),
707 ))
708 })?;
709
710 let pid = mig_assembly::pid_detect::detect_pid(&msg_chunk.message_segments())
711 .map_err(MapperError::Assembly)?;
712
713 let pid_key = format!("pid_{pid}");
715 let (variant, vc) = bundle
716 .variants
717 .iter()
718 .find(|(_, vc)| vc.pid_ahb_workflows.contains_key(&pid_key))
719 .ok_or_else(|| MapperError::PidNotFound {
720 fv: fv.to_string(),
721 variant: "?".to_string(),
722 pid: pid.clone(),
723 })?;
724
725 let workflow = vc
726 .pid_ahb_workflows
727 .get(&pid_key)
728 .expect("presence checked by find() above");
729 let filtered_mig = vc
730 .filtered_mig(&pid)
731 .ok_or_else(|| MapperError::NoMigSchema {
732 fv: fv.to_string(),
733 variant: variant.clone(),
734 })?;
735
736 let mut all_segments = msg_chunk.segments_for_mig(&filtered_mig);
739 if filtered_mig.segments.iter().any(|s| s.id == "UNZ") {
740 if let Some(unz) = &chunks.unz {
741 all_segments.push(unz.clone());
742 }
743 }
744
745 let evaluator: std::sync::Arc<dyn automapper_validation::ConditionEvaluator> =
749 match crate::evaluator_factory::create_evaluator(variant, fv) {
750 Some(boxed) => std::sync::Arc::from(boxed),
751 None => std::sync::Arc::new(
752 automapper_validation::UtilmdStromConditionEvaluatorFV2504::default(),
753 ),
754 };
755 let external = automapper_validation::eval::NoOpExternalProvider;
756
757 let mut report = automapper_validation::validate_edifact_message(
758 &all_segments,
759 &filtered_mig,
760 workflow,
761 evaluator,
762 &external,
763 level,
764 );
765
766 if let (Some(mig), Some(defs)) = (vc.mig_schema.as_ref(), vc.combined_defs.get(&pid_key)) {
772 let reverse = mig_bo4e::path_resolver::ReversePathResolver::from_mig(mig);
773 let field_index =
774 mig_bo4e::Bo4eFieldIndex::build_with_resolver(defs, &filtered_mig, &reverse);
775 report.enrich_bo4e_paths(|path, hint| field_index.resolve(path, hint));
776 }
777
778 Ok(report)
779 }
780
781 pub fn validate_bo4e(
804 &self,
805 msg_stammdaten: &serde_json::Value,
806 tx_stammdaten: &[serde_json::Value],
807 fv: &str,
808 variant: &str,
809 pid: &str,
810 envelope: Option<&InterchangeEnvelope>,
811 level: automapper_validation::ValidationLevel,
812 ) -> Result<automapper_validation::ValidationReport, MapperError> {
813 let placeholder;
814 let envelope = match envelope {
815 Some(e) => e,
816 None => {
817 placeholder = InterchangeEnvelope {
818 sender: EdifactParty::bdew("9900000000001"),
819 receiver: EdifactParty::bdew("9900000000002"),
820 interchange_ref: "1".to_string(),
821 };
822 &placeholder
823 }
824 };
825
826 let edifact = self.to_edifact_interchange(
827 envelope,
828 &[InterchangeMessage {
829 message_ref: "1".to_string(),
830 msg_stammdaten: msg_stammdaten.clone(),
831 tx_stammdaten: tx_stammdaten.to_vec(),
832 fv: fv.to_string(),
833 variant: variant.to_string(),
834 pid: pid.to_string(),
835 }],
836 )?;
837
838 self.validate_edifact(&edifact, fv, level)
839 }
840
841 pub fn association_code(&self, fv: &str, variant: &str) -> Result<String, MapperError> {
852 let meta = self.message_metadata(fv, variant)?;
853 Ok(meta.association_code)
854 }
855
856 pub fn message_metadata(
861 &self,
862 fv: &str,
863 variant: &str,
864 ) -> Result<MessageMetadata, MapperError> {
865 self.ensure_bundle_loaded(fv)?;
866 let bundles = self.bundles.lock().unwrap();
867 let bundle = bundles.get(fv).unwrap();
868 let vc = bundle
869 .variant(variant)
870 .ok_or_else(|| MapperError::VariantNotFound {
871 fv: fv.to_string(),
872 variant: variant.to_string(),
873 })?;
874 let mig = vc
875 .mig_schema
876 .as_ref()
877 .ok_or_else(|| MapperError::NoMigSchema {
878 fv: fv.to_string(),
879 variant: variant.to_string(),
880 })?;
881 Ok(MessageMetadata {
882 message_type: mig.message_type.clone(),
883 release: release_code_for_message_type(&mig.message_type),
884 association_code: mig.version.clone(),
885 })
886 }
887
888 pub fn to_edifact_interchange(
912 &self,
913 envelope: &InterchangeEnvelope,
914 messages: &[InterchangeMessage],
915 ) -> Result<String, MapperError> {
916 let delimiters = edifact_primitives::EdifactDelimiters::default();
917 let sep = delimiters.component as char;
918 let elem = delimiters.element as char;
919 let seg_term = delimiters.segment as char;
920
921 let mut output = String::new();
922
923 output.push_str(&format!(
925 "UNA{}{}{}{}{}{}",
926 sep, elem, delimiters.decimal as char, delimiters.release as char, ' ', seg_term, ));
933
934 let now = chrono::Utc::now();
936 let date_str = now.format("%y%m%d").to_string();
937 let time_str = now.format("%H%M").to_string();
938 let sender = &envelope.sender;
939 let receiver = &envelope.receiver;
940 let interchange_ref = &envelope.interchange_ref;
941 output.push_str(&format!(
942 "UNB{elem}UNOC{sep}3{elem}{sid}{sep}{sq}{elem}{rid}{sep}{rq}{elem}{date_str}{sep}{time_str}{elem}{interchange_ref}{seg_term}",
943 sid = sender.id,
944 sq = sender.qualifier,
945 rid = receiver.id,
946 rq = receiver.qualifier,
947 ));
948
949 let mut message_count = 0u32;
950
951 for msg in messages {
952 let meta = self.message_metadata(&msg.fv, &msg.variant)?;
953
954 let body = self.to_edifact(
956 &msg.msg_stammdaten,
957 &msg.tx_stammdaten,
958 &msg.fv,
959 &msg.variant,
960 &msg.pid,
961 )?;
962
963 let body_seg_count = body
965 .split(seg_term)
966 .filter(|s: &&str| !s.is_empty())
967 .count();
968 let segment_count = body_seg_count + 2;
970
971 output.push_str(&format!(
973 "UNH{elem}{ref}{elem}{msg_type}{sep}D{sep}{release}{sep}UN{sep}{assoc}{seg_term}",
974 ref = msg.message_ref,
975 msg_type = meta.message_type,
976 release = meta.release,
977 assoc = meta.association_code,
978 ));
979
980 output.push_str(&body);
982
983 output.push_str(&format!(
985 "UNT{elem}{segment_count}{elem}{ref}{seg_term}",
986 ref = msg.message_ref,
987 ));
988
989 message_count += 1;
990 }
991
992 output.push_str(&format!(
994 "UNZ{elem}{message_count}{elem}{interchange_ref}{seg_term}",
995 ));
996
997 Ok(output)
998 }
999
1000 pub fn loaded_format_versions(&self) -> Vec<String> {
1002 self.bundles.lock().unwrap().keys().cloned().collect()
1003 }
1004
1005 pub fn variants(&self, fv: &str) -> Result<Vec<String>, MapperError> {
1009 self.ensure_bundle_loaded(fv)?;
1010 let bundles = self.bundles.lock().unwrap();
1011 let bundle = bundles.get(fv).unwrap();
1012 Ok(bundle.variants.keys().cloned().collect())
1013 }
1014}
1015
1016#[derive(Debug, Clone)]
1018pub struct MessageMetadata {
1019 pub message_type: String,
1021 pub release: String,
1023 pub association_code: String,
1025}
1026
1027#[derive(Debug, Clone)]
1029pub struct InterchangeEnvelope {
1030 pub sender: EdifactParty,
1032 pub receiver: EdifactParty,
1034 pub interchange_ref: String,
1036}
1037
1038#[derive(Debug, Clone)]
1040pub struct EdifactParty {
1041 pub id: String,
1043 pub qualifier: String,
1045}
1046
1047impl EdifactParty {
1048 pub fn bdew(id: &str) -> Self {
1050 Self {
1051 id: id.to_string(),
1052 qualifier: "500".to_string(),
1053 }
1054 }
1055
1056 pub fn gs1(id: &str) -> Self {
1058 Self {
1059 id: id.to_string(),
1060 qualifier: "14".to_string(),
1061 }
1062 }
1063}
1064
1065#[derive(Debug, Clone)]
1068pub struct InterchangeMessage {
1069 pub message_ref: String,
1071 pub msg_stammdaten: serde_json::Value,
1073 pub tx_stammdaten: Vec<serde_json::Value>,
1075 pub fv: String,
1077 pub variant: String,
1079 pub pid: String,
1081}
1082
1083fn release_code_for_message_type(msg_type: &str) -> String {
1087 mig_bo4e::model::release_code_for_message_type(msg_type).to_string()
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092 use super::*;
1093 use std::path::Path;
1094
1095 fn data_dir() -> Option<std::path::PathBuf> {
1096 let dist = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../dist");
1098 if dist.join("edifact-data-FV2504.bin").exists() {
1099 return Some(dist);
1100 }
1101 let cache = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../cache/mappings");
1102 if cache.join("FV2504").exists() {
1103 return Some(cache);
1104 }
1105 eprintln!("Skipping test: no DataBundle files found");
1106 None
1107 }
1108
1109 #[test]
1110 fn test_to_edifact_produces_edifact_output() {
1111 let Some(data_dir) = data_dir() else {
1112 return;
1113 };
1114 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1115
1116 let msg_stammdaten = serde_json::json!({
1117 "marktteilnehmer": [{
1118 "marktrolle": "MS",
1119 "rollencodenummer": "9900123456789",
1120 "codepflegeCode": "293"
1121 }]
1122 });
1123 let tx_stammdaten = serde_json::json!({
1124 "prozessdaten": {
1125 "pruefidentifikator": "55001",
1126 "vorgangId": "ABC123",
1127 "transaktionsgrund": "E01"
1128 }
1129 });
1130
1131 let result = mapper.to_edifact(
1132 &msg_stammdaten,
1133 &[tx_stammdaten],
1134 "FV2504",
1135 "UTILMD_Strom",
1136 "55001",
1137 );
1138 assert!(result.is_ok(), "to_edifact failed: {:?}", result.err());
1139 let edifact = result.unwrap();
1140 assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
1141 assert!(edifact.contains("NAD"), "Should contain NAD segment");
1143 assert!(edifact.contains("IDE"), "Should contain IDE segment");
1145 }
1146
1147 #[test]
1148 fn test_to_edifact_struct_produces_edifact_output() {
1149 let Some(data_dir) = data_dir() else {
1150 return;
1151 };
1152 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1153
1154 let nachricht = serde_json::json!({
1155 "stammdaten": {
1156 "marktteilnehmer": [{
1157 "marktrolle": "MS",
1158 "rollencodenummer": "9900123456789",
1159 "codepflegeCode": "293"
1160 }]
1161 },
1162 "transaktionen": [{
1163 "prozessdaten": {
1164 "pruefidentifikator": "55001",
1165 "vorgangId": "ABC123"
1166 }
1167 }]
1168 });
1169
1170 let result = mapper.to_edifact_struct(&nachricht, "FV2504", "UTILMD_Strom", "55001");
1171 assert!(
1172 result.is_ok(),
1173 "to_edifact_struct failed: {:?}",
1174 result.err()
1175 );
1176 let edifact = result.unwrap();
1177 assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
1178 }
1179
1180 #[test]
1181 fn test_to_edifact_invalid_fv_returns_error() {
1182 let Some(data_dir) = data_dir() else {
1183 return;
1184 };
1185 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1186
1187 let result = mapper.to_edifact(
1188 &serde_json::json!({}),
1189 &[serde_json::json!({})],
1190 "FV9999",
1191 "UTILMD_Strom",
1192 "55001",
1193 );
1194 assert!(result.is_err());
1195 }
1196
1197 #[test]
1198 fn test_to_edifact_invalid_variant_returns_error() {
1199 let Some(data_dir) = data_dir() else {
1200 return;
1201 };
1202 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1203
1204 let result = mapper.to_edifact(
1205 &serde_json::json!({}),
1206 &[serde_json::json!({})],
1207 "FV2504",
1208 "NONEXISTENT",
1209 "55001",
1210 );
1211 assert!(result.is_err());
1212 }
1213
1214 #[test]
1215 fn test_to_edifact_invalid_pid_returns_error() {
1216 let Some(data_dir) = data_dir() else {
1217 return;
1218 };
1219 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1220
1221 let result = mapper.to_edifact(
1222 &serde_json::json!({}),
1223 &[serde_json::json!({})],
1224 "FV2504",
1225 "UTILMD_Strom",
1226 "99999",
1227 );
1228 assert!(result.is_err());
1229 }
1230
1231 #[test]
1232 fn test_association_code() {
1233 let Some(data_dir) = data_dir() else {
1234 return;
1235 };
1236 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1237
1238 let code = mapper.association_code("FV2504", "UTILMD_Strom").unwrap();
1239 assert_eq!(code, "S2.1");
1240
1241 let code = mapper.association_code("FV2504", "MSCONS").unwrap();
1242 assert_eq!(code, "2.4c");
1243 }
1244
1245 #[test]
1246 fn test_message_metadata() {
1247 let Some(data_dir) = data_dir() else {
1248 return;
1249 };
1250 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1251
1252 let meta = mapper.message_metadata("FV2504", "UTILMD_Strom").unwrap();
1253 assert_eq!(meta.message_type, "UTILMD");
1254 assert_eq!(meta.release, "11A");
1255 assert_eq!(meta.association_code, "S2.1");
1256 }
1257
1258 #[test]
1259 fn test_to_edifact_interchange() {
1260 let Some(data_dir) = data_dir() else {
1261 return;
1262 };
1263 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1264
1265 let result = mapper.to_edifact_interchange(
1266 &InterchangeEnvelope {
1267 sender: EdifactParty::bdew("9900000000003"),
1268 receiver: EdifactParty::bdew("9900000000001"),
1269 interchange_ref: "REF001".to_string(),
1270 },
1271 &[InterchangeMessage {
1272 message_ref: "MSG001".to_string(),
1273 msg_stammdaten: serde_json::json!({
1274 "marktteilnehmer": [{
1275 "marktrolle": "MS",
1276 "rollencodenummer": "9900123456789",
1277 "codepflegeCode": "293"
1278 }]
1279 }),
1280 tx_stammdaten: vec![serde_json::json!({
1281 "prozessdaten": {
1282 "pruefidentifikator": "55001",
1283 "vorgangId": "ABC123",
1284 "transaktionsgrund": "E01"
1285 }
1286 })],
1287 fv: "FV2504".to_string(),
1288 variant: "UTILMD_Strom".to_string(),
1289 pid: "55001".to_string(),
1290 }],
1291 );
1292 assert!(
1293 result.is_ok(),
1294 "to_edifact_interchange failed: {:?}",
1295 result.err()
1296 );
1297 let edifact = result.unwrap();
1298
1299 assert!(edifact.starts_with("UNA:+.? '"), "Should start with UNA");
1301 assert!(
1302 edifact.contains("UNB+UNOC:3+9900000000003:500+9900000000001:500+"),
1303 "Should contain UNB with sender/receiver"
1304 );
1305 assert!(
1306 edifact.contains("UNH+MSG001+UTILMD:D:11A:UN:S2.1'"),
1307 "Should contain UNH with correct S009"
1308 );
1309 assert!(edifact.contains("NAD"), "Should contain body NAD segment");
1310 assert!(edifact.contains("UNT+"), "Should contain UNT");
1311 assert!(
1312 edifact.contains("+MSG001'"),
1313 "UNT should reference message ref"
1314 );
1315 assert!(
1316 edifact.contains("UNZ+1+REF001'"),
1317 "Should contain UNZ with count and ref"
1318 );
1319 }
1320
1321 #[test]
1322 fn test_detect_pid_from_rff_z13() {
1323 let Some(data_dir) = data_dir() else {
1324 return;
1325 };
1326 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1327
1328 let edifact = "\
1329 UNB+UNOC:3+9978842000002:500+9900269000000:500+250331:1329+REF001'\
1330 UNH+MSG001+UTILMD:D:11A:UN:S2.1'\
1331 BGM+E01+DOC001'\
1332 DTM+137:202503311329?+00:303'\
1333 NAD+MS+9978842000002::293'\
1334 NAD+MR+9900269000000::293'\
1335 IDE+24+TX001'\
1336 DTM+92:202505312200?+00:303'\
1337 DTM+93:202512312300?+00:303'\
1338 STS+7++E01+ZW4+E03'\
1339 LOC+Z16+12345678900'\
1340 RFF+Z13:55001'\
1341 UNT+12+MSG001'\
1342 UNZ+1+REF001'";
1343
1344 let pid = mapper.detect_pid(edifact).unwrap();
1345 assert_eq!(pid, "55001");
1346 }
1347
1348 #[test]
1349 fn test_detect_pid_no_messages_returns_error() {
1350 let Some(data_dir) = data_dir() else {
1351 return;
1352 };
1353 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1354
1355 let edifact = "UNB+UNOC:3+SENDER:500+RECEIVER:500+250401:1200+REF'\
1356 UNZ+0+REF'";
1357 assert!(mapper.detect_pid(edifact).is_err());
1358 }
1359
1360 #[test]
1361 fn test_list_pids_returns_entries() {
1362 let Some(data_dir) = data_dir() else {
1363 return;
1364 };
1365 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir)).unwrap();
1366 let pids = mapper.list_pids().expect("list_pids should succeed");
1367 assert!(!pids.is_empty(), "should return at least one PID");
1368 assert!(
1369 pids.iter().any(|p| p.pid == "55001"),
1370 "should include PID 55001"
1371 );
1372 assert!(
1373 pids.iter().any(|p| p.fv == "FV2504"),
1374 "should include FV2504"
1375 );
1376 assert!(
1377 pids.iter().any(|p| p.variant == "UTILMD_Strom"),
1378 "should include UTILMD_Strom"
1379 );
1380 }
1381
1382 #[test]
1383 fn test_pid_requirements_returns_requirements() {
1384 let Some(data_dir) = data_dir() else {
1385 return;
1386 };
1387 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1388
1389 let req = mapper
1390 .pid_requirements("FV2504", "UTILMD_Strom", "55001")
1391 .expect("pid_requirements should succeed");
1392
1393 assert_eq!(req.pid, "55001");
1394 assert!(
1395 !req.entities.is_empty(),
1396 "55001 should have at least one entity"
1397 );
1398 assert!(
1399 req.entities.iter().any(|e| e.entity == "Prozessdaten"),
1400 "55001 should have a Prozessdaten entity"
1401 );
1402 }
1403}