Skip to main content

edifact_mapper/
mapper.rs

1//! High-level [`Mapper`] API for EDIFACT-to-BO4E conversion.
2
3use 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
13/// Result of a BO4E mapping operation.
14pub struct Bo4eResult {
15    /// The PID (Pruefidentifikator) that was detected or specified.
16    pub pid: String,
17    /// The EDIFACT message type (e.g., "UTILMD", "MSCONS").
18    pub message_type: String,
19    /// The message variant (e.g., "UTILMD_Strom", "MSCONS").
20    pub variant: String,
21    /// The mapped BO4E JSON output.
22    pub bo4e: serde_json::Value,
23}
24
25/// High-level facade for EDIFACT-to-BO4E conversion.
26///
27/// Wraps [`DataBundle`] loading with lazy/eager initialization, and provides
28/// convenient accessors for [`ConversionService`] and [`MappingEngine`] instances.
29///
30/// # Example
31///
32/// ```ignore
33/// use edifact_mapper::{DataDir, Mapper};
34///
35/// let mapper = Mapper::from_data_dir(
36///     DataDir::path("/opt/edifact/data").eager(&["FV2504"])
37/// )?;
38///
39/// let cs = mapper.conversion_service("FV2504", "UTILMD_Strom")?;
40/// let engine = mapper.engine("FV2504", "UTILMD_Strom", "55001")?;
41/// ```
42pub struct Mapper {
43    data_dir: DataDir,
44    bundles: Mutex<HashMap<String, DataBundle>>,
45}
46
47impl Mapper {
48    /// Create a new `Mapper` from a [`DataDir`] configuration.
49    ///
50    /// Any format versions marked as [`eager`](DataDir::eager) are loaded immediately.
51    /// All others are loaded lazily on first access.
52    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    /// Ensure that the bundle for `fv` is loaded into memory.
65    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    /// Get a [`ConversionService`] for the given format version and variant.
80    ///
81    /// The service can tokenize EDIFACT input and assemble it into a MIG tree.
82    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    /// Get a [`MappingEngine`] for a specific PID within a format version and variant.
107    ///
108    /// The engine can convert between assembled MIG trees and BO4E JSON.
109    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    /// Validate a BO4E JSON object against PID requirements.
132    ///
133    /// Returns a list of validation errors. Empty list = valid.
134    /// The `json` should be the transaction-level stammdaten (the entity map).
135    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    /// Validate a typed BO4E struct against PID requirements.
168    ///
169    /// Convenience wrapper that serializes the struct to JSON first.
170    /// Works with any `Pid*Interchange` or `Pid*MessageStammdaten` type.
171    ///
172    /// # Example
173    /// ```ignore
174    /// let interchange = build_55001_interchange();
175    /// let errors = mapper.validate_pid_struct(&interchange, "FV2504", "UTILMD_Strom", "55001")?;
176    /// assert!(errors.is_empty(), "Errors:\n{}", ValidationReport(errors));
177    /// ```
178    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    /// Validate with AHB condition awareness.
192    ///
193    /// Reverse-maps the JSON to EDIFACT segments, evaluates AHB conditions,
194    /// and reports fields as required/optional based on the actual data present.
195    ///
196    /// Falls back to basic validation (without conditions) if no condition
197    /// evaluator is available for the given variant/format version combination.
198    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        // Try to get a condition evaluator for this variant
226        let evaluator = crate::evaluator_factory::create_evaluator(variant, fv);
227
228        if let Some(evaluator) = evaluator {
229            // Reverse-map JSON to EDIFACT segments for condition evaluation context
230            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            // Convert AssembledTree to flat OwnedSegments for EvaluationContext
242            let segments = crate::tree_to_segments::tree_to_owned_segments(&tree);
243
244            // Validate with condition awareness
245            Ok(crate::evaluator_factory::validate_with_boxed_evaluator(
246                evaluator.as_ref(),
247                json,
248                requirements,
249                pid,
250                &segments,
251            ))
252        } else {
253            // No evaluator available — fall back to basic validation
254            Ok(mig_bo4e::pid_validation::validate_pid_json(
255                json,
256                requirements,
257            ))
258        }
259    }
260
261    /// List all format versions currently loaded in memory.
262    pub fn loaded_format_versions(&self) -> Vec<String> {
263        self.bundles.lock().unwrap().keys().cloned().collect()
264    }
265
266    /// List all variants available in a format version's bundle.
267    ///
268    /// Loads the bundle if not already loaded.
269    pub fn variants(&self, fv: &str) -> Result<Vec<String>, MapperError> {
270        self.ensure_bundle_loaded(fv)?;
271        let bundles = self.bundles.lock().unwrap();
272        let bundle = bundles.get(fv).unwrap();
273        Ok(bundle.variants.keys().cloned().collect())
274    }
275}