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
25pub struct Mapper {
43 data_dir: DataDir,
44 bundles: Mutex<HashMap<String, DataBundle>>,
45}
46
47impl Mapper {
48 pub fn from_data_dir(data_dir: DataDir) -> Result<Self, MapperError> {
53 let mapper = Self {
54 data_dir,
55 bundles: Mutex::new(HashMap::new()),
56 };
57 let eager_fvs: Vec<String> = mapper.data_dir.eager_fvs().to_vec();
58 for fv in &eager_fvs {
59 mapper.ensure_bundle_loaded(fv)?;
60 }
61 Ok(mapper)
62 }
63
64 fn ensure_bundle_loaded(&self, fv: &str) -> Result<(), MapperError> {
66 let mut bundles = self.bundles.lock().unwrap();
67 if bundles.contains_key(fv) {
68 return Ok(());
69 }
70 let path = self.data_dir.bundle_path(fv);
71 if !path.exists() {
72 return Err(MapperError::BundleNotFound { fv: fv.to_string() });
73 }
74 let bundle = DataBundle::load(&path)?;
75 bundles.insert(fv.to_string(), bundle);
76 Ok(())
77 }
78
79 pub fn conversion_service(
83 &self,
84 fv: &str,
85 variant: &str,
86 ) -> Result<ConversionService, MapperError> {
87 self.ensure_bundle_loaded(fv)?;
88 let bundles = self.bundles.lock().unwrap();
89 let bundle = bundles.get(fv).unwrap();
90 let vc = bundle
91 .variant(variant)
92 .ok_or_else(|| MapperError::VariantNotFound {
93 fv: fv.to_string(),
94 variant: variant.to_string(),
95 })?;
96 let mig = vc
97 .mig_schema
98 .as_ref()
99 .ok_or_else(|| MapperError::VariantNotFound {
100 fv: fv.to_string(),
101 variant: format!("{variant} (no MIG schema in bundle)"),
102 })?;
103 Ok(ConversionService::from_mig(mig.clone()))
104 }
105
106 pub fn engine(&self, fv: &str, variant: &str, pid: &str) -> Result<MappingEngine, MapperError> {
110 self.ensure_bundle_loaded(fv)?;
111 let bundles = self.bundles.lock().unwrap();
112 let bundle = bundles.get(fv).unwrap();
113 let vc = bundle
114 .variant(variant)
115 .ok_or_else(|| MapperError::VariantNotFound {
116 fv: fv.to_string(),
117 variant: variant.to_string(),
118 })?;
119 let pid_key = format!("pid_{pid}");
120 let defs = vc
121 .combined_defs
122 .get(&pid_key)
123 .ok_or_else(|| MapperError::PidNotFound {
124 fv: fv.to_string(),
125 variant: variant.to_string(),
126 pid: pid.to_string(),
127 })?;
128 Ok(MappingEngine::from_definitions(defs.clone()))
129 }
130
131 pub fn validate_pid(
136 &self,
137 json: &serde_json::Value,
138 fv: &str,
139 variant: &str,
140 pid: &str,
141 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
142 self.ensure_bundle_loaded(fv)?;
143 let bundles = self.bundles.lock().unwrap();
144 let bundle = bundles.get(fv).unwrap();
145 let vc = bundle
146 .variant(variant)
147 .ok_or_else(|| MapperError::VariantNotFound {
148 fv: fv.to_string(),
149 variant: variant.to_string(),
150 })?;
151 let pid_key = format!("pid_{pid}");
152 let requirements =
153 vc.pid_requirements
154 .get(&pid_key)
155 .ok_or_else(|| MapperError::PidNotFound {
156 fv: fv.to_string(),
157 variant: variant.to_string(),
158 pid: pid.to_string(),
159 })?;
160
161 Ok(mig_bo4e::pid_validation::validate_pid_json(
162 json,
163 requirements,
164 ))
165 }
166
167 pub fn validate_pid_struct(
179 &self,
180 value: &impl serde::Serialize,
181 fv: &str,
182 variant: &str,
183 pid: &str,
184 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
185 let json = serde_json::to_value(value).map_err(|e| {
186 MapperError::Mapping(mig_bo4e::MappingError::TypeConversion(e.to_string()))
187 })?;
188 self.validate_pid(&json, fv, variant, pid)
189 }
190
191 pub fn validate_pid_with_conditions(
199 &self,
200 json: &serde_json::Value,
201 fv: &str,
202 variant: &str,
203 pid: &str,
204 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
205 self.ensure_bundle_loaded(fv)?;
206 let bundles = self.bundles.lock().unwrap();
207 let bundle = bundles.get(fv).unwrap();
208 let vc = bundle
209 .variant(variant)
210 .ok_or_else(|| MapperError::VariantNotFound {
211 fv: fv.to_string(),
212 variant: variant.to_string(),
213 })?;
214 let pid_key = format!("pid_{pid}");
215
216 let requirements =
217 vc.pid_requirements
218 .get(&pid_key)
219 .ok_or_else(|| MapperError::PidNotFound {
220 fv: fv.to_string(),
221 variant: variant.to_string(),
222 pid: pid.to_string(),
223 })?;
224
225 let evaluator = crate::evaluator_factory::create_evaluator(variant, fv);
227
228 if let Some(evaluator) = evaluator {
229 let defs = vc
231 .combined_defs
232 .get(&pid_key)
233 .ok_or_else(|| MapperError::PidNotFound {
234 fv: fv.to_string(),
235 variant: variant.to_string(),
236 pid: pid.to_string(),
237 })?;
238 let engine = MappingEngine::from_definitions(defs.clone());
239 let tree = engine.map_all_reverse(json, None);
240
241 let segments = crate::tree_to_segments::tree_to_owned_segments(&tree);
243
244 Ok(crate::evaluator_factory::validate_with_boxed_evaluator(
246 evaluator.as_ref(),
247 json,
248 requirements,
249 pid,
250 &segments,
251 ))
252 } else {
253 Ok(mig_bo4e::pid_validation::validate_pid_json(
255 json,
256 requirements,
257 ))
258 }
259 }
260
261 pub fn to_edifact(
287 &self,
288 msg_stammdaten: &serde_json::Value,
289 tx_stammdaten: &[serde_json::Value],
290 fv: &str,
291 variant: &str,
292 pid: &str,
293 ) -> Result<String, MapperError> {
294 self.ensure_bundle_loaded(fv)?;
295 let bundles = self.bundles.lock().unwrap();
296 let bundle = bundles.get(fv).unwrap();
297 let vc = bundle
298 .variant(variant)
299 .ok_or_else(|| MapperError::VariantNotFound {
300 fv: fv.to_string(),
301 variant: variant.to_string(),
302 })?;
303
304 let tx_group = vc
305 .tx_group(pid)
306 .ok_or_else(|| MapperError::PidNotFound {
307 fv: fv.to_string(),
308 variant: variant.to_string(),
309 pid: pid.to_string(),
310 })?;
311
312 let msg_engine = vc.msg_engine();
313 let tx_engine =
314 vc.tx_engine(pid)
315 .ok_or_else(|| MapperError::PidNotFound {
316 fv: fv.to_string(),
317 variant: variant.to_string(),
318 pid: pid.to_string(),
319 })?;
320
321 let filtered_mig =
322 vc.filtered_mig(pid)
323 .ok_or_else(|| MapperError::NoMigSchema {
324 fv: fv.to_string(),
325 variant: variant.to_string(),
326 })?;
327
328 let transaktionen: Vec<mig_bo4e::model::MappedTransaktion> = tx_stammdaten
330 .iter()
331 .map(|tx| mig_bo4e::model::MappedTransaktion {
332 stammdaten: tx.clone(),
333 nesting_info: Default::default(),
334 })
335 .collect();
336 let mapped = mig_bo4e::model::MappedMessage {
337 stammdaten: msg_stammdaten.clone(),
338 transaktionen,
339 nesting_info: Default::default(),
340 };
341
342 let tree = MappingEngine::map_interchange_reverse(
344 &msg_engine,
345 &tx_engine,
346 &mapped,
347 &tx_group,
348 Some(&filtered_mig),
349 );
350
351 let disassembler =
353 mig_assembly::disassembler::Disassembler::new(&filtered_mig);
354 let segments = disassembler.disassemble(&tree);
355
356 let delimiters = edifact_primitives::EdifactDelimiters::default();
358 Ok(mig_assembly::renderer::render_edifact(
359 &segments,
360 &delimiters,
361 ))
362 }
363
364 pub fn to_edifact_struct(
370 &self,
371 nachricht: &impl serde::Serialize,
372 fv: &str,
373 variant: &str,
374 pid: &str,
375 ) -> Result<String, MapperError> {
376 let json = serde_json::to_value(nachricht)
377 .map_err(|e| MapperError::Serialization(e.to_string()))?;
378
379 let msg_stammdaten = json
380 .get("stammdaten")
381 .cloned()
382 .unwrap_or(serde_json::Value::Object(Default::default()));
383
384 let tx_stammdaten: Vec<serde_json::Value> = json
385 .get("transaktionen")
386 .and_then(|v| v.as_array())
387 .cloned()
388 .unwrap_or_default();
389
390 self.to_edifact(&msg_stammdaten, &tx_stammdaten, fv, variant, pid)
391 }
392
393 pub fn association_code(&self, fv: &str, variant: &str) -> Result<String, MapperError> {
404 let meta = self.message_metadata(fv, variant)?;
405 Ok(meta.association_code)
406 }
407
408 pub fn message_metadata(
413 &self,
414 fv: &str,
415 variant: &str,
416 ) -> Result<MessageMetadata, MapperError> {
417 self.ensure_bundle_loaded(fv)?;
418 let bundles = self.bundles.lock().unwrap();
419 let bundle = bundles.get(fv).unwrap();
420 let vc = bundle
421 .variant(variant)
422 .ok_or_else(|| MapperError::VariantNotFound {
423 fv: fv.to_string(),
424 variant: variant.to_string(),
425 })?;
426 let mig = vc
427 .mig_schema
428 .as_ref()
429 .ok_or_else(|| MapperError::NoMigSchema {
430 fv: fv.to_string(),
431 variant: variant.to_string(),
432 })?;
433 Ok(MessageMetadata {
434 message_type: mig.message_type.clone(),
435 release: release_code_for_message_type(&mig.message_type),
436 association_code: mig.version.clone(),
437 })
438 }
439
440 pub fn to_edifact_interchange(
468 &self,
469 sender: &str,
470 receiver: &str,
471 interchange_ref: &str,
472 messages: &[InterchangeMessage],
473 ) -> Result<String, MapperError> {
474 let delimiters = edifact_primitives::EdifactDelimiters::default();
475 let sep = delimiters.component as char;
476 let elem = delimiters.element as char;
477 let seg_term = delimiters.segment as char;
478
479 let mut output = String::new();
480
481 output.push_str(&format!(
483 "UNA{}{}{}{}{}{}",
484 sep, elem, delimiters.decimal as char, delimiters.release as char, ' ', seg_term, ));
491
492 let now = chrono::Utc::now();
494 let date_str = now.format("%y%m%d").to_string();
495 let time_str = now.format("%H%M").to_string();
496 output.push_str(&format!(
497 "UNB{elem}UNOC{sep}3{elem}{sender}{sep}500{elem}{receiver}{sep}500{elem}{date_str}{sep}{time_str}{elem}{interchange_ref}{seg_term}",
498 ));
499
500 let mut message_count = 0u32;
501
502 for msg in messages {
503 let meta = self.message_metadata(&msg.fv, &msg.variant)?;
504
505 let body = self.to_edifact(
507 &msg.msg_stammdaten,
508 &msg.tx_stammdaten,
509 &msg.fv,
510 &msg.variant,
511 &msg.pid,
512 )?;
513
514 let body_seg_count = body
516 .split(seg_term)
517 .filter(|s: &&str| !s.is_empty())
518 .count();
519 let segment_count = body_seg_count + 2;
521
522 output.push_str(&format!(
524 "UNH{elem}{ref}{elem}{msg_type}{sep}D{sep}{release}{sep}UN{sep}{assoc}{seg_term}",
525 ref = msg.message_ref,
526 msg_type = meta.message_type,
527 release = meta.release,
528 assoc = meta.association_code,
529 ));
530
531 output.push_str(&body);
533
534 output.push_str(&format!(
536 "UNT{elem}{segment_count}{elem}{ref}{seg_term}",
537 ref = msg.message_ref,
538 ));
539
540 message_count += 1;
541 }
542
543 output.push_str(&format!(
545 "UNZ{elem}{message_count}{elem}{interchange_ref}{seg_term}",
546 ));
547
548 Ok(output)
549 }
550
551 pub fn loaded_format_versions(&self) -> Vec<String> {
553 self.bundles.lock().unwrap().keys().cloned().collect()
554 }
555
556 pub fn variants(&self, fv: &str) -> Result<Vec<String>, MapperError> {
560 self.ensure_bundle_loaded(fv)?;
561 let bundles = self.bundles.lock().unwrap();
562 let bundle = bundles.get(fv).unwrap();
563 Ok(bundle.variants.keys().cloned().collect())
564 }
565}
566
567#[derive(Debug, Clone)]
569pub struct MessageMetadata {
570 pub message_type: String,
572 pub release: String,
574 pub association_code: String,
576}
577
578#[derive(Debug, Clone)]
581pub struct InterchangeMessage {
582 pub message_ref: String,
584 pub msg_stammdaten: serde_json::Value,
586 pub tx_stammdaten: Vec<serde_json::Value>,
588 pub fv: String,
590 pub variant: String,
592 pub pid: String,
594}
595
596fn release_code_for_message_type(msg_type: &str) -> String {
600 match msg_type {
601 "APERAK" => "07B",
602 "COMDIS" => "17A",
603 "CONTRL" => "04B",
604 "IFTSTA" => "18A",
605 "INSRPT" => "18A",
606 "INVOIC" => "06A",
607 "MSCONS" => "04B",
608 "ORDCHG" => "09B",
609 "ORDERS" => "09B",
610 "ORDRSP" => "10A",
611 "PARTIN" => "20B",
612 "PRICAT" => "20B",
613 "QUOTES" => "10A",
614 "REMADV" => "05A",
615 "REQOTE" => "10A",
616 "UTILMD" => "11A",
617 "UTILTS" => "18A",
618 _ => "04B", }
620 .to_string()
621}
622
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627 use std::path::Path;
628
629 fn data_dir() -> Option<std::path::PathBuf> {
630 let dist = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../dist");
632 if dist.join("edifact-data-FV2504.bin").exists() {
633 return Some(dist);
634 }
635 let cache = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../cache/mappings");
636 if cache.join("FV2504").exists() {
637 return Some(cache);
638 }
639 eprintln!("Skipping test: no DataBundle files found");
640 None
641 }
642
643 #[test]
644 fn test_to_edifact_produces_edifact_output() {
645 let Some(data_dir) = data_dir() else {
646 return;
647 };
648 let mapper =
649 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
650
651 let msg_stammdaten = serde_json::json!({
652 "marktteilnehmer": [{
653 "marktrolle": "MS",
654 "rollencodenummer": "9900123456789",
655 "codepflegeCode": "293"
656 }]
657 });
658 let tx_stammdaten = serde_json::json!({
659 "prozessdaten": {
660 "pruefidentifikator": "55001",
661 "vorgangId": "ABC123",
662 "transaktionsgrund": "E01"
663 }
664 });
665
666 let result = mapper.to_edifact(
667 &msg_stammdaten,
668 &[tx_stammdaten],
669 "FV2504",
670 "UTILMD_Strom",
671 "55001",
672 );
673 assert!(result.is_ok(), "to_edifact failed: {:?}", result.err());
674 let edifact = result.unwrap();
675 assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
676 assert!(edifact.contains("NAD"), "Should contain NAD segment");
678 assert!(edifact.contains("IDE"), "Should contain IDE segment");
680 }
681
682 #[test]
683 fn test_to_edifact_struct_produces_edifact_output() {
684 let Some(data_dir) = data_dir() else {
685 return;
686 };
687 let mapper =
688 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
689
690 let nachricht = serde_json::json!({
691 "stammdaten": {
692 "marktteilnehmer": [{
693 "marktrolle": "MS",
694 "rollencodenummer": "9900123456789",
695 "codepflegeCode": "293"
696 }]
697 },
698 "transaktionen": [{
699 "prozessdaten": {
700 "pruefidentifikator": "55001",
701 "vorgangId": "ABC123"
702 }
703 }]
704 });
705
706 let result = mapper.to_edifact_struct(&nachricht, "FV2504", "UTILMD_Strom", "55001");
707 assert!(
708 result.is_ok(),
709 "to_edifact_struct failed: {:?}",
710 result.err()
711 );
712 let edifact = result.unwrap();
713 assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
714 }
715
716 #[test]
717 fn test_to_edifact_invalid_fv_returns_error() {
718 let Some(data_dir) = data_dir() else {
719 return;
720 };
721 let mapper =
722 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
723
724 let result = mapper.to_edifact(
725 &serde_json::json!({}),
726 &[serde_json::json!({})],
727 "FV9999",
728 "UTILMD_Strom",
729 "55001",
730 );
731 assert!(result.is_err());
732 }
733
734 #[test]
735 fn test_to_edifact_invalid_variant_returns_error() {
736 let Some(data_dir) = data_dir() else {
737 return;
738 };
739 let mapper =
740 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
741
742 let result = mapper.to_edifact(
743 &serde_json::json!({}),
744 &[serde_json::json!({})],
745 "FV2504",
746 "NONEXISTENT",
747 "55001",
748 );
749 assert!(result.is_err());
750 }
751
752 #[test]
753 fn test_to_edifact_invalid_pid_returns_error() {
754 let Some(data_dir) = data_dir() else {
755 return;
756 };
757 let mapper =
758 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
759
760 let result = mapper.to_edifact(
761 &serde_json::json!({}),
762 &[serde_json::json!({})],
763 "FV2504",
764 "UTILMD_Strom",
765 "99999",
766 );
767 assert!(result.is_err());
768 }
769
770 #[test]
771 fn test_association_code() {
772 let Some(data_dir) = data_dir() else {
773 return;
774 };
775 let mapper =
776 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
777
778 let code = mapper.association_code("FV2504", "UTILMD_Strom").unwrap();
779 assert_eq!(code, "S2.1");
780
781 let code = mapper.association_code("FV2504", "MSCONS").unwrap();
782 assert_eq!(code, "2.4c");
783 }
784
785 #[test]
786 fn test_message_metadata() {
787 let Some(data_dir) = data_dir() else {
788 return;
789 };
790 let mapper =
791 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
792
793 let meta = mapper.message_metadata("FV2504", "UTILMD_Strom").unwrap();
794 assert_eq!(meta.message_type, "UTILMD");
795 assert_eq!(meta.release, "11A");
796 assert_eq!(meta.association_code, "S2.1");
797 }
798
799 #[test]
800 fn test_to_edifact_interchange() {
801 let Some(data_dir) = data_dir() else {
802 return;
803 };
804 let mapper =
805 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
806
807 let result = mapper.to_edifact_interchange(
808 "9900000000003",
809 "9900000000001",
810 "REF001",
811 &[InterchangeMessage {
812 message_ref: "MSG001".to_string(),
813 msg_stammdaten: serde_json::json!({
814 "marktteilnehmer": [{
815 "marktrolle": "MS",
816 "rollencodenummer": "9900123456789",
817 "codepflegeCode": "293"
818 }]
819 }),
820 tx_stammdaten: vec![serde_json::json!({
821 "prozessdaten": {
822 "pruefidentifikator": "55001",
823 "vorgangId": "ABC123",
824 "transaktionsgrund": "E01"
825 }
826 })],
827 fv: "FV2504".to_string(),
828 variant: "UTILMD_Strom".to_string(),
829 pid: "55001".to_string(),
830 }],
831 );
832 assert!(
833 result.is_ok(),
834 "to_edifact_interchange failed: {:?}",
835 result.err()
836 );
837 let edifact = result.unwrap();
838
839 assert!(edifact.starts_with("UNA:+.? '"), "Should start with UNA");
841 assert!(edifact.contains("UNB+UNOC:3+9900000000003:500+9900000000001:500+"),
842 "Should contain UNB with sender/receiver");
843 assert!(edifact.contains("UNH+MSG001+UTILMD:D:11A:UN:S2.1'"),
844 "Should contain UNH with correct S009");
845 assert!(edifact.contains("NAD"), "Should contain body NAD segment");
846 assert!(edifact.contains("UNT+"), "Should contain UNT");
847 assert!(edifact.contains("+MSG001'"), "UNT should reference message ref");
848 assert!(edifact.contains("UNZ+1+REF001'"), "Should contain UNZ with count and ref");
849 }
850}