use std::collections::HashMap;
use std::sync::Mutex;
use mig_assembly::ConversionService;
use mig_bo4e::engine::DataBundle;
use mig_bo4e::MappingEngine;
use crate::data_dir::DataDir;
use crate::error::MapperError;
pub struct Bo4eResult {
pub pid: String,
pub message_type: String,
pub variant: String,
pub bo4e: serde_json::Value,
}
#[derive(Debug, Clone)]
pub struct PidListEntry {
pub fv: String,
pub variant: String,
pub pid: String,
pub beschreibung: String,
}
pub struct Mapper {
data_dir: DataDir,
bundles: Mutex<HashMap<String, DataBundle>>,
}
impl Mapper {
pub fn from_data_dir(data_dir: DataDir) -> Result<Self, MapperError> {
let mapper = Self {
data_dir,
bundles: Mutex::new(HashMap::new()),
};
let eager_fvs: Vec<String> = mapper.data_dir.eager_fvs().to_vec();
for fv in &eager_fvs {
mapper.ensure_bundle_loaded(fv)?;
}
Ok(mapper)
}
fn ensure_bundle_loaded(&self, fv: &str) -> Result<(), MapperError> {
let mut bundles = self.bundles.lock().unwrap();
if bundles.contains_key(fv) {
return Ok(());
}
let path = self.data_dir.bundle_path(fv);
if !path.exists() {
return Err(MapperError::BundleNotFound { fv: fv.to_string() });
}
let bundle = DataBundle::load(&path)?;
bundles.insert(fv.to_string(), bundle);
Ok(())
}
pub fn conversion_service(
&self,
fv: &str,
variant: &str,
) -> Result<ConversionService, MapperError> {
self.ensure_bundle_loaded(fv)?;
let bundles = self.bundles.lock().unwrap();
let bundle = bundles.get(fv).unwrap();
let vc = bundle
.variant(variant)
.ok_or_else(|| MapperError::VariantNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
})?;
let mig = vc
.mig_schema
.as_ref()
.ok_or_else(|| MapperError::VariantNotFound {
fv: fv.to_string(),
variant: format!("{variant} (no MIG schema in bundle)"),
})?;
Ok(ConversionService::from_mig(mig.clone()))
}
pub fn engine(&self, fv: &str, variant: &str, pid: &str) -> Result<MappingEngine, MapperError> {
self.ensure_bundle_loaded(fv)?;
let bundles = self.bundles.lock().unwrap();
let bundle = bundles.get(fv).unwrap();
let vc = bundle
.variant(variant)
.ok_or_else(|| MapperError::VariantNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
})?;
let pid_key = format!("pid_{pid}");
let defs = vc
.combined_defs
.get(&pid_key)
.ok_or_else(|| MapperError::PidNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
pid: pid.to_string(),
})?;
Ok(MappingEngine::from_definitions(defs.clone()))
}
pub fn pid_requirements(
&self,
fv: &str,
variant: &str,
pid: &str,
) -> Result<mig_bo4e::pid_requirements::PidRequirements, MapperError> {
self.ensure_bundle_loaded(fv)?;
let bundles = self.bundles.lock().unwrap();
let bundle = bundles.get(fv).unwrap();
let vc = bundle
.variant(variant)
.ok_or_else(|| MapperError::VariantNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
})?;
let pid_key = format!("pid_{pid}");
vc.pid_requirements
.get(&pid_key)
.cloned()
.ok_or_else(|| MapperError::PidNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
pid: pid.to_string(),
})
}
pub fn list_pids(&self) -> Result<Vec<PidListEntry>, MapperError> {
let dir = self.data_dir.data_path();
let read_dir = std::fs::read_dir(dir).map_err(|_| MapperError::DataDirNotFound {
path: dir.display().to_string(),
})?;
let mut result = Vec::new();
for entry in read_dir.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "bin") {
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
let fv = match stem.strip_prefix("edifact-data-") {
Some(v) => v.to_string(),
None => continue,
};
self.ensure_bundle_loaded(&fv)?;
let bundles = self.bundles.lock().unwrap();
if let Some(bundle) = bundles.get(&fv) {
for (variant, vc) in &bundle.variants {
for (pid_key, req) in &vc.pid_requirements {
let pid = pid_key
.strip_prefix("pid_")
.unwrap_or(pid_key)
.to_string();
result.push(PidListEntry {
fv: fv.clone(),
variant: variant.clone(),
pid,
beschreibung: req.beschreibung.clone(),
});
}
}
}
}
}
result.sort_by(|a, b| a.pid.cmp(&b.pid));
Ok(result)
}
pub fn validate_pid(
&self,
json: &serde_json::Value,
fv: &str,
variant: &str,
pid: &str,
) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
self.ensure_bundle_loaded(fv)?;
let bundles = self.bundles.lock().unwrap();
let bundle = bundles.get(fv).unwrap();
let vc = bundle
.variant(variant)
.ok_or_else(|| MapperError::VariantNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
})?;
let pid_key = format!("pid_{pid}");
let requirements =
vc.pid_requirements
.get(&pid_key)
.ok_or_else(|| MapperError::PidNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
pid: pid.to_string(),
})?;
Ok(mig_bo4e::pid_validation::validate_pid_json(
json,
requirements,
))
}
pub fn validate_pid_struct(
&self,
value: &impl serde::Serialize,
fv: &str,
variant: &str,
pid: &str,
) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
let json = serde_json::to_value(value).map_err(|e| {
MapperError::Mapping(mig_bo4e::MappingError::TypeConversion(e.to_string()))
})?;
self.validate_pid(&json, fv, variant, pid)
}
pub fn validate_pid_with_conditions(
&self,
json: &serde_json::Value,
fv: &str,
variant: &str,
pid: &str,
) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
self.ensure_bundle_loaded(fv)?;
let bundles = self.bundles.lock().unwrap();
let bundle = bundles.get(fv).unwrap();
let vc = bundle
.variant(variant)
.ok_or_else(|| MapperError::VariantNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
})?;
let pid_key = format!("pid_{pid}");
let requirements =
vc.pid_requirements
.get(&pid_key)
.ok_or_else(|| MapperError::PidNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
pid: pid.to_string(),
})?;
let evaluator = crate::evaluator_factory::create_evaluator(variant, fv);
if let Some(evaluator) = evaluator {
let defs = vc
.combined_defs
.get(&pid_key)
.ok_or_else(|| MapperError::PidNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
pid: pid.to_string(),
})?;
let engine = MappingEngine::from_definitions(defs.clone());
let tree = engine.map_all_reverse(json, None);
let segments = crate::tree_to_segments::tree_to_owned_segments(&tree);
Ok(crate::evaluator_factory::validate_with_boxed_evaluator(
evaluator.as_ref(),
json,
requirements,
pid,
&segments,
))
} else {
Ok(mig_bo4e::pid_validation::validate_pid_json_transaction(
json,
requirements,
))
}
}
pub fn to_edifact(
&self,
msg_stammdaten: &serde_json::Value,
tx_stammdaten: &[serde_json::Value],
fv: &str,
variant: &str,
pid: &str,
) -> Result<String, MapperError> {
self.ensure_bundle_loaded(fv)?;
let bundles = self.bundles.lock().unwrap();
let bundle = bundles.get(fv).unwrap();
let vc = bundle
.variant(variant)
.ok_or_else(|| MapperError::VariantNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
})?;
let tx_group = vc
.tx_group(pid)
.ok_or_else(|| MapperError::PidNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
pid: pid.to_string(),
})?;
let msg_engine = vc.msg_engine(pid);
let tx_engine =
vc.tx_engine(pid)
.ok_or_else(|| MapperError::PidNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
pid: pid.to_string(),
})?;
let filtered_mig =
vc.filtered_mig(pid)
.ok_or_else(|| MapperError::NoMigSchema {
fv: fv.to_string(),
variant: variant.to_string(),
})?;
let transaktionen: Vec<mig_bo4e::model::MappedTransaktion> = tx_stammdaten
.iter()
.map(|tx| mig_bo4e::model::MappedTransaktion {
stammdaten: tx.clone(),
nesting_info: Default::default(),
dp_routing: Default::default(),
})
.collect();
let mapped = mig_bo4e::model::MappedMessage {
stammdaten: msg_stammdaten.clone(),
transaktionen,
nesting_info: Default::default(),
dp_routing: Default::default(),
inter_group_segments: Default::default(),
};
let tree = MappingEngine::map_interchange_reverse(
&msg_engine,
&tx_engine,
&mapped,
tx_group,
Some(&filtered_mig),
);
let disassembler =
mig_assembly::disassembler::Disassembler::new(&filtered_mig);
let segments = disassembler.disassemble(&tree);
let delimiters = edifact_primitives::EdifactDelimiters::default();
Ok(mig_assembly::renderer::render_edifact(
&segments,
&delimiters,
))
}
pub fn to_edifact_struct(
&self,
nachricht: &impl serde::Serialize,
fv: &str,
variant: &str,
pid: &str,
) -> Result<String, MapperError> {
let json = serde_json::to_value(nachricht)
.map_err(|e| MapperError::Serialization(e.to_string()))?;
let msg_stammdaten = json
.get("stammdaten")
.cloned()
.unwrap_or(serde_json::Value::Object(Default::default()));
let tx_stammdaten: Vec<serde_json::Value> = json
.get("transaktionen")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
self.to_edifact(&msg_stammdaten, &tx_stammdaten, fv, variant, pid)
}
pub fn from_edifact<M, T>(
&self,
edifact: &str,
fv: &str,
variant: &str,
pid: &str,
) -> Result<mig_bo4e::model::Interchange<M, T>, MapperError>
where
M: serde::de::DeserializeOwned,
T: serde::de::DeserializeOwned,
{
self.ensure_bundle_loaded(fv)?;
let bundles = self.bundles.lock().unwrap();
let bundle = bundles.get(fv).unwrap();
let vc = bundle
.variant(variant)
.ok_or_else(|| MapperError::VariantNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
})?;
let tx_group = vc
.tx_group(pid)
.ok_or_else(|| MapperError::PidNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
pid: pid.to_string(),
})?;
let msg_engine = vc.msg_engine(pid);
let tx_engine =
vc.tx_engine(pid)
.ok_or_else(|| MapperError::PidNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
pid: pid.to_string(),
})?;
let filtered_mig =
vc.filtered_mig(pid)
.ok_or_else(|| MapperError::NoMigSchema {
fv: fv.to_string(),
variant: variant.to_string(),
})?;
let svc = ConversionService::from_mig(filtered_mig);
let (chunks, trees) = svc.convert_interchange_to_trees(edifact)?;
let tree = trees
.first()
.ok_or_else(|| MapperError::Assembly(
mig_assembly::AssemblyError::ParseError("No messages in interchange".to_string()),
))?;
let interchangedaten =
mig_bo4e::model::extract_interchangedaten(&chunks.envelope);
let msg_chunk = chunks.messages.first().ok_or_else(|| {
MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
"No message chunks".to_string(),
))
})?;
let (unh_ref, nachrichten_typ) =
mig_bo4e::model::extract_unh_fields(&msg_chunk.unh);
let nachrichtendaten = mig_bo4e::model::Nachrichtendaten {
unh_referenz: unh_ref,
nachrichten_typ,
};
MappingEngine::map_interchange_typed::<M, T>(
&msg_engine,
&tx_engine,
tree,
tx_group,
true,
nachrichtendaten,
interchangedaten,
)
.map_err(|e| MapperError::Serialization(e.to_string()))
}
pub fn detect_pid(&self, edifact: &str) -> Result<String, MapperError> {
let segments = mig_assembly::tokenize::parse_to_segments(edifact.as_bytes())?;
let chunks = mig_assembly::split_messages(segments)?;
let msg_chunk =
chunks
.messages
.first()
.ok_or_else(|| MapperError::Assembly(
mig_assembly::AssemblyError::ParseError(
"No messages found in EDIFACT content".to_string(),
),
))?;
let msg_segments = msg_chunk.message_segments();
mig_assembly::pid_detect::detect_pid(&msg_segments).map_err(MapperError::Assembly)
}
pub fn association_code(&self, fv: &str, variant: &str) -> Result<String, MapperError> {
let meta = self.message_metadata(fv, variant)?;
Ok(meta.association_code)
}
pub fn message_metadata(
&self,
fv: &str,
variant: &str,
) -> Result<MessageMetadata, MapperError> {
self.ensure_bundle_loaded(fv)?;
let bundles = self.bundles.lock().unwrap();
let bundle = bundles.get(fv).unwrap();
let vc = bundle
.variant(variant)
.ok_or_else(|| MapperError::VariantNotFound {
fv: fv.to_string(),
variant: variant.to_string(),
})?;
let mig = vc
.mig_schema
.as_ref()
.ok_or_else(|| MapperError::NoMigSchema {
fv: fv.to_string(),
variant: variant.to_string(),
})?;
Ok(MessageMetadata {
message_type: mig.message_type.clone(),
release: release_code_for_message_type(&mig.message_type),
association_code: mig.version.clone(),
})
}
pub fn to_edifact_interchange(
&self,
envelope: &InterchangeEnvelope,
messages: &[InterchangeMessage],
) -> Result<String, MapperError> {
let delimiters = edifact_primitives::EdifactDelimiters::default();
let sep = delimiters.component as char;
let elem = delimiters.element as char;
let seg_term = delimiters.segment as char;
let mut output = String::new();
output.push_str(&format!(
"UNA{}{}{}{}{}{}",
sep, elem, delimiters.decimal as char, delimiters.release as char, ' ', seg_term, ));
let now = chrono::Utc::now();
let date_str = now.format("%y%m%d").to_string();
let time_str = now.format("%H%M").to_string();
let sender = &envelope.sender;
let receiver = &envelope.receiver;
let interchange_ref = &envelope.interchange_ref;
output.push_str(&format!(
"UNB{elem}UNOC{sep}3{elem}{sid}{sep}{sq}{elem}{rid}{sep}{rq}{elem}{date_str}{sep}{time_str}{elem}{interchange_ref}{seg_term}",
sid = sender.id,
sq = sender.qualifier,
rid = receiver.id,
rq = receiver.qualifier,
));
let mut message_count = 0u32;
for msg in messages {
let meta = self.message_metadata(&msg.fv, &msg.variant)?;
let body = self.to_edifact(
&msg.msg_stammdaten,
&msg.tx_stammdaten,
&msg.fv,
&msg.variant,
&msg.pid,
)?;
let body_seg_count = body
.split(seg_term)
.filter(|s: &&str| !s.is_empty())
.count();
let segment_count = body_seg_count + 2;
output.push_str(&format!(
"UNH{elem}{ref}{elem}{msg_type}{sep}D{sep}{release}{sep}UN{sep}{assoc}{seg_term}",
ref = msg.message_ref,
msg_type = meta.message_type,
release = meta.release,
assoc = meta.association_code,
));
output.push_str(&body);
output.push_str(&format!(
"UNT{elem}{segment_count}{elem}{ref}{seg_term}",
ref = msg.message_ref,
));
message_count += 1;
}
output.push_str(&format!(
"UNZ{elem}{message_count}{elem}{interchange_ref}{seg_term}",
));
Ok(output)
}
pub fn loaded_format_versions(&self) -> Vec<String> {
self.bundles.lock().unwrap().keys().cloned().collect()
}
pub fn variants(&self, fv: &str) -> Result<Vec<String>, MapperError> {
self.ensure_bundle_loaded(fv)?;
let bundles = self.bundles.lock().unwrap();
let bundle = bundles.get(fv).unwrap();
Ok(bundle.variants.keys().cloned().collect())
}
}
#[derive(Debug, Clone)]
pub struct MessageMetadata {
pub message_type: String,
pub release: String,
pub association_code: String,
}
#[derive(Debug, Clone)]
pub struct InterchangeEnvelope {
pub sender: EdifactParty,
pub receiver: EdifactParty,
pub interchange_ref: String,
}
#[derive(Debug, Clone)]
pub struct EdifactParty {
pub id: String,
pub qualifier: String,
}
impl EdifactParty {
pub fn bdew(id: &str) -> Self {
Self {
id: id.to_string(),
qualifier: "500".to_string(),
}
}
pub fn gs1(id: &str) -> Self {
Self {
id: id.to_string(),
qualifier: "14".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct InterchangeMessage {
pub message_ref: String,
pub msg_stammdaten: serde_json::Value,
pub tx_stammdaten: Vec<serde_json::Value>,
pub fv: String,
pub variant: String,
pub pid: String,
}
fn release_code_for_message_type(msg_type: &str) -> String {
match msg_type {
"APERAK" => "07B",
"COMDIS" => "17A",
"CONTRL" => "04B",
"IFTSTA" => "18A",
"INSRPT" => "18A",
"INVOIC" => "06A",
"MSCONS" => "04B",
"ORDCHG" => "09B",
"ORDERS" => "09B",
"ORDRSP" => "10A",
"PARTIN" => "20B",
"PRICAT" => "20B",
"QUOTES" => "10A",
"REMADV" => "05A",
"REQOTE" => "10A",
"UTILMD" => "11A",
"UTILTS" => "18A",
_ => "04B", }
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn data_dir() -> Option<std::path::PathBuf> {
let dist = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../dist");
if dist.join("edifact-data-FV2504.bin").exists() {
return Some(dist);
}
let cache = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../cache/mappings");
if cache.join("FV2504").exists() {
return Some(cache);
}
eprintln!("Skipping test: no DataBundle files found");
None
}
#[test]
fn test_to_edifact_produces_edifact_output() {
let Some(data_dir) = data_dir() else {
return;
};
let mapper =
Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
let msg_stammdaten = serde_json::json!({
"marktteilnehmer": [{
"marktrolle": "MS",
"rollencodenummer": "9900123456789",
"codepflegeCode": "293"
}]
});
let tx_stammdaten = serde_json::json!({
"prozessdaten": {
"pruefidentifikator": "55001",
"vorgangId": "ABC123",
"transaktionsgrund": "E01"
}
});
let result = mapper.to_edifact(
&msg_stammdaten,
&[tx_stammdaten],
"FV2504",
"UTILMD_Strom",
"55001",
);
assert!(result.is_ok(), "to_edifact failed: {:?}", result.err());
let edifact = result.unwrap();
assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
assert!(edifact.contains("NAD"), "Should contain NAD segment");
assert!(edifact.contains("IDE"), "Should contain IDE segment");
}
#[test]
fn test_to_edifact_struct_produces_edifact_output() {
let Some(data_dir) = data_dir() else {
return;
};
let mapper =
Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
let nachricht = serde_json::json!({
"stammdaten": {
"marktteilnehmer": [{
"marktrolle": "MS",
"rollencodenummer": "9900123456789",
"codepflegeCode": "293"
}]
},
"transaktionen": [{
"prozessdaten": {
"pruefidentifikator": "55001",
"vorgangId": "ABC123"
}
}]
});
let result = mapper.to_edifact_struct(&nachricht, "FV2504", "UTILMD_Strom", "55001");
assert!(
result.is_ok(),
"to_edifact_struct failed: {:?}",
result.err()
);
let edifact = result.unwrap();
assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
}
#[test]
fn test_to_edifact_invalid_fv_returns_error() {
let Some(data_dir) = data_dir() else {
return;
};
let mapper =
Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
let result = mapper.to_edifact(
&serde_json::json!({}),
&[serde_json::json!({})],
"FV9999",
"UTILMD_Strom",
"55001",
);
assert!(result.is_err());
}
#[test]
fn test_to_edifact_invalid_variant_returns_error() {
let Some(data_dir) = data_dir() else {
return;
};
let mapper =
Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
let result = mapper.to_edifact(
&serde_json::json!({}),
&[serde_json::json!({})],
"FV2504",
"NONEXISTENT",
"55001",
);
assert!(result.is_err());
}
#[test]
fn test_to_edifact_invalid_pid_returns_error() {
let Some(data_dir) = data_dir() else {
return;
};
let mapper =
Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
let result = mapper.to_edifact(
&serde_json::json!({}),
&[serde_json::json!({})],
"FV2504",
"UTILMD_Strom",
"99999",
);
assert!(result.is_err());
}
#[test]
fn test_association_code() {
let Some(data_dir) = data_dir() else {
return;
};
let mapper =
Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
let code = mapper.association_code("FV2504", "UTILMD_Strom").unwrap();
assert_eq!(code, "S2.1");
let code = mapper.association_code("FV2504", "MSCONS").unwrap();
assert_eq!(code, "2.4c");
}
#[test]
fn test_message_metadata() {
let Some(data_dir) = data_dir() else {
return;
};
let mapper =
Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
let meta = mapper.message_metadata("FV2504", "UTILMD_Strom").unwrap();
assert_eq!(meta.message_type, "UTILMD");
assert_eq!(meta.release, "11A");
assert_eq!(meta.association_code, "S2.1");
}
#[test]
fn test_to_edifact_interchange() {
let Some(data_dir) = data_dir() else {
return;
};
let mapper =
Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
let result = mapper.to_edifact_interchange(
&InterchangeEnvelope {
sender: EdifactParty::bdew("9900000000003"),
receiver: EdifactParty::bdew("9900000000001"),
interchange_ref: "REF001".to_string(),
},
&[InterchangeMessage {
message_ref: "MSG001".to_string(),
msg_stammdaten: serde_json::json!({
"marktteilnehmer": [{
"marktrolle": "MS",
"rollencodenummer": "9900123456789",
"codepflegeCode": "293"
}]
}),
tx_stammdaten: vec![serde_json::json!({
"prozessdaten": {
"pruefidentifikator": "55001",
"vorgangId": "ABC123",
"transaktionsgrund": "E01"
}
})],
fv: "FV2504".to_string(),
variant: "UTILMD_Strom".to_string(),
pid: "55001".to_string(),
}],
);
assert!(
result.is_ok(),
"to_edifact_interchange failed: {:?}",
result.err()
);
let edifact = result.unwrap();
assert!(edifact.starts_with("UNA:+.? '"), "Should start with UNA");
assert!(edifact.contains("UNB+UNOC:3+9900000000003:500+9900000000001:500+"),
"Should contain UNB with sender/receiver");
assert!(edifact.contains("UNH+MSG001+UTILMD:D:11A:UN:S2.1'"),
"Should contain UNH with correct S009");
assert!(edifact.contains("NAD"), "Should contain body NAD segment");
assert!(edifact.contains("UNT+"), "Should contain UNT");
assert!(edifact.contains("+MSG001'"), "UNT should reference message ref");
assert!(edifact.contains("UNZ+1+REF001'"), "Should contain UNZ with count and ref");
}
#[test]
fn test_detect_pid_from_rff_z13() {
let Some(data_dir) = data_dir() else {
return;
};
let mapper =
Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
let edifact = "\
UNB+UNOC:3+9978842000002:500+9900269000000:500+250331:1329+REF001'\
UNH+MSG001+UTILMD:D:11A:UN:S2.1'\
BGM+E01+DOC001'\
DTM+137:202503311329?+00:303'\
NAD+MS+9978842000002::293'\
NAD+MR+9900269000000::293'\
IDE+24+TX001'\
DTM+92:202505312200?+00:303'\
DTM+93:202512312300?+00:303'\
STS+7++E01+ZW4+E03'\
LOC+Z16+12345678900'\
RFF+Z13:55001'\
UNT+12+MSG001'\
UNZ+1+REF001'";
let pid = mapper.detect_pid(edifact).unwrap();
assert_eq!(pid, "55001");
}
#[test]
fn test_detect_pid_no_messages_returns_error() {
let Some(data_dir) = data_dir() else {
return;
};
let mapper =
Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
let edifact = "UNB+UNOC:3+SENDER:500+RECEIVER:500+250401:1200+REF'\
UNZ+0+REF'";
assert!(mapper.detect_pid(edifact).is_err());
}
#[test]
fn test_list_pids_returns_entries() {
let Some(data_dir) = data_dir() else {
return;
};
let mapper = Mapper::from_data_dir(DataDir::path(&data_dir)).unwrap();
let pids = mapper.list_pids().expect("list_pids should succeed");
assert!(!pids.is_empty(), "should return at least one PID");
assert!(
pids.iter().any(|p| p.pid == "55001"),
"should include PID 55001"
);
assert!(
pids.iter().any(|p| p.fv == "FV2504"),
"should include FV2504"
);
assert!(
pids.iter().any(|p| p.variant == "UTILMD_Strom"),
"should include UTILMD_Strom"
);
}
#[test]
fn test_pid_requirements_returns_requirements() {
let Some(data_dir) = data_dir() else {
return;
};
let mapper =
Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
let req = mapper
.pid_requirements("FV2504", "UTILMD_Strom", "55001")
.expect("pid_requirements should succeed");
assert_eq!(req.pid, "55001");
assert!(
!req.entities.is_empty(),
"55001 should have at least one entity"
);
assert!(
req.entities.iter().any(|e| e.entity == "Prozessdaten"),
"55001 should have a Prozessdaten entity"
);
}
}