use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::Path;
use mig_assembly::assembler::{
AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
};
use mig_types::schema::mig::MigSchema;
use mig_types::segment::OwnedSegment;
use crate::definition::{FieldMapping, MappingDefinition};
use crate::error::MappingError;
use crate::segment_structure::SegmentStructure;
pub struct MappingEngine {
definitions: Vec<MappingDefinition>,
segment_structure: Option<SegmentStructure>,
code_lookup: Option<crate::code_lookup::CodeLookup>,
transaction_group: Option<String>,
current_pid: Option<String>,
code_lists: std::sync::Arc<crate::code_lists::CodeLists>,
raw_codes: bool,
}
impl MappingEngine {
pub fn new_empty() -> Self {
Self {
definitions: Vec::new(),
segment_structure: None,
code_lookup: None,
transaction_group: None,
current_pid: None,
raw_codes: false,
code_lists: std::sync::Arc::new(crate::code_lists::CodeLists::default()),
}
}
pub fn load(dir: &Path) -> Result<Self, MappingError> {
let mut definitions = Vec::new();
let mut entries: Vec<_> = std::fs::read_dir(dir)?.filter_map(|e| e.ok()).collect();
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let path = entry.path();
if path.extension().map(|e| e == "toml").unwrap_or(false) {
let content = std::fs::read_to_string(&path)?;
let def = MappingDefinition::from_toml_str(&content).map_err(|message| {
MappingError::TomlParse {
file: path.display().to_string(),
message,
}
})?;
definitions.push(def);
}
}
definitions.sort_by_key(|d| d.meta.order.unwrap_or(u32::MAX));
Ok(Self {
definitions,
segment_structure: None,
code_lookup: None,
transaction_group: None,
current_pid: None,
raw_codes: false,
code_lists: crate::code_lists::CodeLists::discover(dir),
})
}
pub fn load_split(
message_dir: &Path,
transaction_dir: &Path,
) -> Result<(Self, Self), MappingError> {
let msg_engine = Self::load(message_dir)?;
let tx_engine = Self::load(transaction_dir)?;
Ok((msg_engine, tx_engine))
}
pub fn load_merged(dirs: &[&Path]) -> Result<Self, MappingError> {
let mut definitions = Vec::new();
for dir in dirs {
let engine = Self::load(dir)?;
definitions.extend(engine.definitions);
}
Ok(Self {
definitions,
segment_structure: None,
code_lookup: None,
transaction_group: None,
current_pid: None,
raw_codes: false,
code_lists: crate::code_lists::CodeLists::discover(
dirs.first().copied().unwrap_or(Path::new("")),
),
})
}
pub fn load_with_common(
common_dir: &Path,
pid_dir: &Path,
schema_index: &crate::pid_schema_index::PidSchemaIndex,
) -> Result<Self, MappingError> {
let mut common_defs = Self::load(common_dir)?.definitions;
common_defs.retain(|d| {
d.meta
.source_path
.as_deref()
.map(|sp| schema_index.has_group(sp))
.unwrap_or(true)
});
let pid_defs = Self::load(pid_dir)?.definitions;
let normalize_sg = |sg: &str| -> String {
sg.split('.')
.map(|part| part.split(':').next().unwrap_or(part))
.collect::<Vec<_>>()
.join(".")
};
let pid_keys: HashSet<(String, Option<String>)> = pid_defs
.iter()
.flat_map(|d| {
let sg = normalize_sg(&d.meta.source_group);
let disc = d.meta.discriminator.clone();
let mut keys = vec![(sg.clone(), disc.clone())];
if let Some(ref disc_str) = disc {
if let Some(base) = disc_str.rsplit_once('#') {
if base.1.chars().all(|c| c.is_ascii_digit()) {
keys.push((sg, Some(base.0.to_string())));
}
}
}
keys
})
.collect();
common_defs.retain(|d| {
let key = (
normalize_sg(&d.meta.source_group),
d.meta.discriminator.clone(),
);
!pid_keys.contains(&key)
});
let mut definitions = common_defs;
definitions.extend(pid_defs);
Ok(Self {
definitions,
segment_structure: None,
code_lookup: None,
transaction_group: None,
current_pid: None,
raw_codes: false,
code_lists: crate::code_lists::CodeLists::discover(pid_dir),
})
}
pub fn load_common_only(
common_dir: &Path,
schema_index: &crate::pid_schema_index::PidSchemaIndex,
) -> Result<Self, MappingError> {
let mut common_defs = Self::load(common_dir)?.definitions;
common_defs.retain(|d| {
d.meta
.source_path
.as_deref()
.map(|sp| schema_index.has_group(sp))
.unwrap_or(true)
});
Ok(Self {
definitions: common_defs,
segment_structure: None,
code_lookup: None,
transaction_group: None,
current_pid: None,
raw_codes: false,
code_lists: crate::code_lists::CodeLists::discover(common_dir),
})
}
pub fn load_split_with_common(
message_dir: &Path,
common_dir: &Path,
transaction_dir: &Path,
schema_index: &crate::pid_schema_index::PidSchemaIndex,
) -> Result<(Self, Self), MappingError> {
let msg_engine = Self::load(message_dir)?;
let tx_engine = Self::load_with_common(common_dir, transaction_dir, schema_index)?;
Ok((msg_engine, tx_engine))
}
fn names_a_code_list(definitions: &[MappingDefinition]) -> bool {
definitions.iter().any(|d| {
d.fields.values().any(|f| {
matches!(f, FieldMapping::Structured(s)
if s.code_list.is_some() || s.also_code_list.is_some())
})
})
}
fn table<'a>(
&'a self,
inline: Option<&'a BTreeMap<String, String>>,
named: Option<&str>,
) -> Option<&'a BTreeMap<String, String>> {
self.code_lists.resolve(inline, named)
}
pub fn with_code_lists(
mut self,
code_lists: std::sync::Arc<crate::code_lists::CodeLists>,
) -> Self {
self.code_lists = code_lists;
self
}
pub fn code_lists(&self) -> &std::sync::Arc<crate::code_lists::CodeLists> {
&self.code_lists
}
pub fn from_definitions_with_code_lists(
code_lists: std::sync::Arc<crate::code_lists::CodeLists>,
definitions: Vec<MappingDefinition>,
) -> Self {
debug_assert!(
!(code_lists.is_empty() && Self::names_a_code_list(&definitions)),
"definitions name a shared code list but the supplied tables are \
empty — whatever produced them (a bundle, a cache) is not carrying \
them, and every code they translate will reach the output raw"
);
Self {
definitions,
segment_structure: None,
code_lookup: None,
transaction_group: None,
current_pid: None,
raw_codes: false,
code_lists,
}
}
pub fn from_definitions(definitions: Vec<MappingDefinition>) -> Self {
debug_assert!(
!Self::names_a_code_list(&definitions),
"definitions name a shared code list but none were supplied — build \
this engine with `from_definitions_with_code_lists`, or the codes \
they translate will reach the output untranslated"
);
Self {
definitions,
segment_structure: None,
code_lookup: None,
transaction_group: None,
current_pid: None,
raw_codes: false,
code_lists: std::sync::Arc::new(crate::code_lists::CodeLists::default()),
}
}
pub fn save_cached(&self, path: &Path) -> Result<(), MappingError> {
let encoded =
serde_json::to_vec(&self.definitions).map_err(|e| MappingError::CacheWrite {
path: path.display().to_string(),
message: e.to_string(),
})?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, encoded)?;
Ok(())
}
pub fn load_cached_or_toml(cache_path: &Path, toml_dir: &Path) -> Result<Self, MappingError> {
if cache_path.exists() {
Self::load_cached(cache_path)
} else {
Self::load(toml_dir)
}
}
pub fn load_cached(path: &Path) -> Result<Self, MappingError> {
let bytes = std::fs::read(path)?;
let definitions: Vec<MappingDefinition> =
serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
path: path.display().to_string(),
message: e.to_string(),
})?;
Ok(Self {
definitions,
segment_structure: None,
code_lookup: None,
transaction_group: None,
current_pid: None,
raw_codes: false,
code_lists: crate::code_lists::CodeLists::discover(path),
})
}
pub fn with_segment_structure(mut self, ss: SegmentStructure) -> Self {
self.segment_structure = Some(ss);
self
}
pub fn with_code_lookup(mut self, cl: crate::code_lookup::CodeLookup) -> Self {
self.code_lookup = Some(cl);
self
}
pub fn with_pid(mut self, pid: impl Into<String>) -> Self {
self.current_pid = Some(pid.into());
self
}
pub fn with_raw_codes(mut self, raw: bool) -> Self {
self.raw_codes = raw;
self
}
pub fn with_path_resolver(mut self, resolver: crate::path_resolver::PathResolver) -> Self {
for def in &mut self.definitions {
def.normalize_paths(&resolver);
}
self
}
pub fn with_transaction_group(mut self, tx: impl Into<String>) -> Self {
self.transaction_group = Some(tx.into());
self
}
pub fn extend_definitions(mut self, defs: impl IntoIterator<Item = MappingDefinition>) -> Self {
fn key(d: &MappingDefinition) -> (String, String, String, String, String) {
(
d.meta.entity.clone(),
d.meta.source_group.clone(),
d.meta.source_path.clone().unwrap_or_default(),
d.meta.discriminator.clone().unwrap_or_default(),
d.meta.parent_field.clone().unwrap_or_default(),
)
}
let mut seen: std::collections::HashSet<_> = self.definitions.iter().map(key).collect();
for def in defs {
if seen.insert(key(&def)) {
self.definitions.push(def);
}
}
self
}
pub fn definitions(&self) -> &[MappingDefinition] {
&self.definitions
}
pub fn definition_for_entity(&self, entity: &str) -> Option<&MappingDefinition> {
self.definitions.iter().find(|d| {
d.meta.entity == entity
&& d.meta.parent_field.is_none()
&& !is_bound_child(&self.definitions, d)
})
}
pub fn extract_field(
&self,
tree: &AssembledTree,
group_path: &str,
path: &str,
repetition: usize,
) -> Option<String> {
let instance = Self::resolve_group_instance(tree, group_path, repetition)?;
Self::extract_from_instance(instance, path)
}
pub fn resolve_group_instance<'a>(
tree: &'a AssembledTree,
group_path: &str,
repetition: usize,
) -> Option<&'a AssembledGroupInstance> {
let parts: Vec<&str> = group_path.split('.').collect();
let (first_id, first_rep) = parse_group_spec(parts[0]);
let first_group = tree.groups.iter().find(|g| g.group_id == first_id)?;
if parts.len() == 1 {
let rep = first_rep.unwrap_or(repetition);
return first_group.repetitions.get(rep);
}
let mut current_instance = first_group.repetitions.get(first_rep.unwrap_or(0))?;
for (i, part) in parts[1..].iter().enumerate() {
let (group_id, explicit_rep) = parse_group_spec(part);
let child_group = current_instance
.child_groups
.iter()
.find(|g| g.group_id == group_id)?;
if i == parts.len() - 2 {
let rep = explicit_rep.unwrap_or(repetition);
return child_group.repetitions.get(rep);
}
current_instance = child_group.repetitions.get(explicit_rep.unwrap_or(0))?;
}
None
}
pub fn resolve_by_source_path<'a>(
tree: &'a AssembledTree,
source_path: &str,
) -> Option<&'a AssembledGroupInstance> {
let parts: Vec<&str> = source_path.split('.').collect();
if parts.is_empty() {
return None;
}
let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
let first_group = tree
.groups
.iter()
.find(|g| g.group_id.eq_ignore_ascii_case(first_id))?;
let mut current_instance = if let Some(q) = first_qualifier {
find_rep_by_entry_qualifier(&first_group.repetitions, q)?
} else {
first_group.repetitions.first()?
};
if parts.len() == 1 {
return Some(current_instance);
}
for part in &parts[1..] {
let (group_id, qualifier) = parse_source_path_part(part);
let child_group = current_instance
.child_groups
.iter()
.find(|g| g.group_id.eq_ignore_ascii_case(group_id))?;
current_instance = if let Some(q) = qualifier {
find_rep_by_entry_qualifier(&child_group.repetitions, q)?
} else {
child_group.repetitions.first()?
};
}
Some(current_instance)
}
pub fn resolve_all_by_source_path<'a>(
tree: &'a AssembledTree,
source_path: &str,
) -> Vec<&'a AssembledGroupInstance> {
let parts: Vec<&str> = source_path.split('.').collect();
if parts.is_empty() {
return vec![];
}
let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
let first_group = match tree
.groups
.iter()
.find(|g| g.group_id.eq_ignore_ascii_case(first_id))
{
Some(g) => g,
None => return vec![],
};
let mut current_instances: Vec<&AssembledGroupInstance> = if let Some(q) = first_qualifier {
find_all_reps_by_entry_qualifier(&first_group.repetitions, q)
} else {
first_group.repetitions.iter().collect()
};
for part in &parts[1..] {
let (group_id, qualifier) = parse_source_path_part(part);
let mut next_instances = Vec::new();
for instance in ¤t_instances {
if let Some(child_group) = instance
.child_groups
.iter()
.find(|g| g.group_id.eq_ignore_ascii_case(group_id))
{
if let Some(q) = qualifier {
next_instances.extend(find_all_reps_by_entry_qualifier(
&child_group.repetitions,
q,
));
} else {
next_instances.extend(child_group.repetitions.iter());
}
}
}
current_instances = next_instances;
}
current_instances
}
fn compute_child_indices(
tree: &AssembledTree,
source_path: &str,
indexed: &[(usize, &AssembledGroupInstance)],
) -> Vec<usize> {
let parts: Vec<&str> = source_path.split('.').collect();
if parts.len() < 2 {
return vec![];
}
let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
let first_group = match tree
.groups
.iter()
.find(|g| g.group_id.eq_ignore_ascii_case(first_id))
{
Some(g) => g,
None => return vec![],
};
let parent_reps: Vec<&AssembledGroupInstance> = if let Some(q) = first_qualifier {
find_all_reps_by_entry_qualifier(&first_group.repetitions, q)
} else {
first_group.repetitions.iter().collect()
};
let (child_id, _child_qualifier) = parse_source_path_part(parts[parts.len() - 1]);
let mut result = Vec::new();
for (_, inst) in indexed {
let mut found = false;
for parent in &parent_reps {
if let Some(child_group) = parent
.child_groups
.iter()
.find(|g| g.group_id.eq_ignore_ascii_case(child_id))
{
if let Some(pos) = child_group
.repetitions
.iter()
.position(|r| std::ptr::eq(r, *inst))
{
result.push(pos);
found = true;
break;
}
}
}
if !found {
result.push(usize::MAX); }
}
result
}
pub fn resolve_all_with_parent_indices<'a>(
tree: &'a AssembledTree,
source_path: &str,
) -> Vec<(usize, &'a AssembledGroupInstance)> {
let parts: Vec<&str> = source_path.split('.').collect();
if parts.is_empty() {
return vec![];
}
let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
let first_group = match tree
.groups
.iter()
.find(|g| g.group_id.eq_ignore_ascii_case(first_id))
{
Some(g) => g,
None => return vec![],
};
if parts.len() == 1 {
let instances: Vec<&AssembledGroupInstance> = if let Some(q) = first_qualifier {
find_all_reps_by_entry_qualifier(&first_group.repetitions, q)
} else {
first_group.repetitions.iter().collect()
};
return instances.into_iter().map(|i| (0, i)).collect();
}
let first_reps: Vec<(usize, &AssembledGroupInstance)> = if let Some(q) = first_qualifier {
let matching = find_all_reps_by_entry_qualifier(&first_group.repetitions, q);
let mut result = Vec::new();
for m in matching {
let idx = first_group
.repetitions
.iter()
.position(|r| std::ptr::eq(r, m))
.unwrap_or(0);
result.push((idx, m));
}
result
} else {
first_group.repetitions.iter().enumerate().collect()
};
let mut current: Vec<(usize, &AssembledGroupInstance)> = first_reps;
let remaining = &parts[1..];
for (level, part) in remaining.iter().enumerate() {
let is_leaf = level == remaining.len() - 1;
let (group_id, qualifier) = parse_source_path_part(part);
let mut next: Vec<(usize, &AssembledGroupInstance)> = Vec::new();
for (prev_parent_idx, instance) in ¤t {
if let Some(child_group) = instance
.child_groups
.iter()
.find(|g| g.group_id.eq_ignore_ascii_case(group_id))
{
let matching: Vec<(usize, &AssembledGroupInstance)> = if let Some(q) = qualifier
{
let filtered =
find_all_reps_by_entry_qualifier(&child_group.repetitions, q);
filtered
.into_iter()
.map(|m| {
let idx = child_group
.repetitions
.iter()
.position(|r| std::ptr::eq(r, m))
.unwrap_or(0);
(idx, m)
})
.collect()
} else {
child_group.repetitions.iter().enumerate().collect()
};
for (rep_idx, child_rep) in matching {
if is_leaf {
next.push((*prev_parent_idx, child_rep));
} else {
next.push((rep_idx, child_rep));
}
}
}
}
current = next;
}
current
}
pub fn extract_from_instance(instance: &AssembledGroupInstance, path: &str) -> Option<String> {
let parts: Vec<&str> = path.split('.').collect();
if parts.is_empty() {
return None;
}
let (segment_tag, qualifier, occurrence) = parse_tag_qualifier(parts[0]);
let segment = if let Some(q) = qualifier {
instance
.segments
.iter()
.filter(|s| {
s.tag.eq_ignore_ascii_case(&segment_tag)
&& s.elements
.first()
.and_then(|e| e.first())
.map(|v| v.as_str())
== Some(q)
})
.nth(occurrence)?
} else {
instance
.segments
.iter()
.filter(|s| s.tag.eq_ignore_ascii_case(&segment_tag))
.nth(occurrence)?
};
Self::resolve_field_path(segment, &parts[1..])
}
pub fn extract_all_from_instance(instance: &AssembledGroupInstance, path: &str) -> Vec<String> {
let parts: Vec<&str> = path.split('.').collect();
if parts.is_empty() {
return vec![];
}
let (segment_tag, qualifier, _) = parse_tag_qualifier(parts[0]);
let matching_segments: Vec<&AssembledSegment> = if let Some(q) = qualifier {
instance
.segments
.iter()
.filter(|s| {
s.tag.eq_ignore_ascii_case(&segment_tag)
&& s.elements
.first()
.and_then(|e| e.first())
.map(|v| v.as_str())
== Some(q)
})
.collect()
} else {
instance
.segments
.iter()
.filter(|s| s.tag.eq_ignore_ascii_case(&segment_tag))
.collect()
};
matching_segments
.into_iter()
.filter_map(|seg| Self::resolve_field_path(seg, &parts[1..]))
.collect()
}
pub fn map_forward(
&self,
tree: &AssembledTree,
def: &MappingDefinition,
repetition: usize,
) -> serde_json::Value {
self.map_forward_inner(tree, def, repetition, true)
}
fn map_forward_inner(
&self,
tree: &AssembledTree,
def: &MappingDefinition,
repetition: usize,
enrich_codes: bool,
) -> serde_json::Value {
let mut result = serde_json::Map::new();
if def.meta.source_group.is_empty() {
let mut all_root_segs = tree.segments.clone();
for segs in tree.inter_group_segments.values() {
all_root_segs.extend(segs.iter().cloned());
}
let root_instance = AssembledGroupInstance {
segments: all_root_segs,
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: Vec::new(),
skipped_positions: Vec::new(),
};
self.extract_fields_from_instance(&root_instance, def, &mut result, enrich_codes);
return serde_json::Value::Object(result);
}
let instance = if let Some(ref sp) = def.meta.source_path {
if has_source_path_qualifiers(sp) && !def.meta.source_group.contains(':') {
Self::resolve_by_source_path(tree, sp).or_else(|| {
Self::resolve_group_instance(tree, &def.meta.source_group, repetition)
})
} else {
Self::resolve_group_instance(tree, &def.meta.source_group, repetition)
}
} else {
Self::resolve_group_instance(tree, &def.meta.source_group, repetition)
};
if let Some(instance) = instance {
if let Some(ref tag) = def.meta.repeat_on_tag {
let matching: Vec<_> = instance
.segments
.iter()
.filter(|s| s.tag.eq_ignore_ascii_case(tag))
.collect();
if matching.len() > 1 {
let mut arr = Vec::new();
for seg in &matching {
let sub_instance = AssembledGroupInstance {
segments: vec![(*seg).clone()],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: Vec::new(),
skipped_positions: Vec::new(),
};
let mut elem_result = serde_json::Map::new();
self.extract_fields_from_instance(
&sub_instance,
def,
&mut elem_result,
enrich_codes,
);
if !elem_result.is_empty() {
arr.push(serde_json::Value::Object(elem_result));
}
}
if !arr.is_empty() {
return serde_json::Value::Array(arr);
}
}
}
self.extract_fields_from_instance(instance, def, &mut result, enrich_codes);
}
serde_json::Value::Object(result)
}
fn extract_fields_from_instance(
&self,
instance: &AssembledGroupInstance,
def: &MappingDefinition,
result: &mut serde_json::Map<String, serde_json::Value>,
enrich_codes: bool,
) {
for (path, field_mapping) in &def.fields {
let (target, enum_map) = match field_mapping {
FieldMapping::Simple(t) => (t.as_str(), None),
FieldMapping::Structured(s) => (
s.target.as_str(),
self.table(s.enum_map.as_ref(), s.code_list.as_deref()),
),
FieldMapping::Nested(_) => continue,
};
if target.is_empty() {
continue;
}
if let Some((list, sub)) = list_target(target) {
self.extract_list_field(
instance,
def,
path,
list,
sub,
enum_map,
enrich_codes,
result,
);
continue;
}
if let Some(val) = Self::extract_from_instance(instance, path) {
if let FieldMapping::Structured(s) = field_mapping {
if let (false, Some(also), Some(also_map)) = (
self.raw_codes,
s.also_target.as_deref(),
self.table(s.also_enum_map.as_ref(), s.also_code_list.as_deref()),
) {
if let Some(also_val) = also_map.get(&val) {
set_nested_value(result, also, also_val.clone());
}
}
}
let mapped_val = match enum_map {
Some(map) if !self.raw_codes => {
map.get(&val).cloned().unwrap_or_else(|| val.clone())
}
_ => val.clone(),
};
if enrich_codes {
if let (Some(ref code_lookup), Some(ref source_path)) =
(&self.code_lookup, &def.meta.source_path)
{
let parts: Vec<&str> = path.split('.').collect();
let (seg_tag, path_qualifier, _occ) = parse_tag_qualifier(parts[0]);
let (element_idx, component_idx) =
Self::parse_element_component(&parts[1..]);
let disc_qualifier = Self::discriminator_qualifier_for_tag(def, &seg_tag);
let q = disc_qualifier.as_deref();
if let Some(codes) = code_lookup.enrichment_codes(
source_path,
&seg_tag,
path_qualifier,
q,
element_idx,
component_idx,
) {
if let Some(ref pid) = self.current_pid {
if codes.len() == 1 && codes.contains_key(pid.as_str()) {
set_nested_value(result, target, mapped_val);
continue;
}
}
let enrichment = codes.get(&val);
let meaning = enrichment
.map(|e| serde_json::Value::String(e.meaning.clone()))
.unwrap_or(serde_json::Value::Null);
let mut obj = serde_json::Map::new();
obj.insert("code".into(), serde_json::json!(mapped_val));
obj.insert("meaning".into(), meaning);
if let Some(enum_key) = enrichment.and_then(|e| e.enum_key.as_ref()) {
obj.insert("enum".into(), serde_json::json!(enum_key));
}
let enriched = serde_json::Value::Object(obj);
set_nested_value_json(result, target, enriched);
continue;
}
}
}
set_nested_value(result, target, mapped_val);
}
}
if !instance.child_groups.is_empty() {
self.extract_nested_children(instance, def, result, enrich_codes);
}
}
#[allow(clippy::too_many_arguments)]
fn extract_list_field(
&self,
instance: &AssembledGroupInstance,
def: &MappingDefinition,
path: &str,
list: &str,
sub: &str,
enum_map: Option<&std::collections::BTreeMap<String, String>>,
enrich_codes: bool,
result: &mut serde_json::Map<String, serde_json::Value>,
) {
let parts: Vec<&str> = path.split('.').collect();
if parts.len() < 2 {
return;
}
let (seg_tag, qualifier, _) = parse_tag_qualifier(parts[0]);
let segments: Vec<&AssembledSegment> = instance
.segments
.iter()
.filter(|s| {
s.tag.eq_ignore_ascii_case(&seg_tag)
&& qualifier.map_or(true, |q| {
s.elements
.first()
.and_then(|e| e.first())
.map(|v| v.as_str())
== Some(q)
})
})
.collect();
if segments.is_empty() {
return;
}
let items = result
.entry(list.to_string())
.or_insert_with(|| serde_json::Value::Array(Vec::new()));
let Some(items) = items.as_array_mut() else {
return;
};
while items.len() < segments.len() {
items.push(serde_json::Value::Object(serde_json::Map::new()));
}
let codes = if enrich_codes {
match (&self.code_lookup, &def.meta.source_path) {
(Some(lookup), Some(source_path)) => {
let (element_idx, component_idx) = Self::parse_element_component(&parts[1..]);
let disc = Self::discriminator_qualifier_for_tag(def, &seg_tag);
lookup
.enrichment_codes(
source_path,
&seg_tag,
qualifier,
disc.as_deref(),
element_idx,
component_idx,
)
.cloned()
}
_ => None,
}
} else {
None
};
for (item, segment) in items.iter_mut().zip(segments) {
let Some(val) = Self::resolve_field_path(segment, &parts[1..]) else {
continue;
};
let mapped = match enum_map {
Some(map) if !self.raw_codes => {
map.get(&val).cloned().unwrap_or_else(|| val.clone())
}
_ => val.clone(),
};
let value = match &codes {
Some(codes) => {
let enrichment = codes.get(&val);
let mut obj = serde_json::Map::new();
obj.insert("code".into(), serde_json::json!(mapped));
obj.insert(
"meaning".into(),
enrichment
.map(|e| serde_json::Value::String(e.meaning.clone()))
.unwrap_or(serde_json::Value::Null),
);
if let Some(enum_key) = enrichment.and_then(|e| e.enum_key.as_ref()) {
obj.insert("enum".into(), serde_json::json!(enum_key));
}
serde_json::Value::Object(obj)
}
None => serde_json::Value::String(mapped),
};
if let Some(obj) = item.as_object_mut() {
set_nested_value_json(obj, sub, value);
}
}
}
fn extract_nested_children(
&self,
instance: &AssembledGroupInstance,
def: &MappingDefinition,
result: &mut serde_json::Map<String, serde_json::Value>,
enrich_codes: bool,
) {
for child in self
.definitions
.iter()
.filter(|c| is_nested_child_of(c, def))
{
if nested_parent_qualifier(child).is_some_and(|q| !entry_qualifier_matches(instance, q))
{
continue;
}
let (leaf_id, leaf_qualifier) = nested_child_leaf(child);
let Some(group) = instance
.child_groups
.iter()
.find(|g| g.group_id.eq_ignore_ascii_case(&leaf_id))
else {
continue;
};
let reps: Vec<&AssembledGroupInstance> = match leaf_qualifier {
Some(q) => find_all_reps_by_entry_qualifier(&group.repetitions, q),
None => group.repetitions.iter().collect(),
};
let mut items: Vec<serde_json::Value> = Vec::new();
let mut push_item = |sub: &AssembledGroupInstance| {
let mut obj = serde_json::Map::new();
self.extract_fields_from_instance(sub, child, &mut obj, enrich_codes);
if !obj.is_empty() {
items.push(serde_json::Value::Object(obj));
}
};
for rep in reps {
let repeat_tag = child
.meta
.repeat_on_tag
.as_deref()
.filter(|tag| rep.segments.iter().any(|s| s.tag.eq_ignore_ascii_case(tag)));
let Some(tag) = repeat_tag else {
push_item(rep);
continue;
};
let shared: Vec<AssembledSegment> = rep
.segments
.iter()
.filter(|s| !s.tag.eq_ignore_ascii_case(tag))
.cloned()
.collect();
for seg in rep
.segments
.iter()
.filter(|s| s.tag.eq_ignore_ascii_case(tag))
{
let mut segments = shared.clone();
segments.push(seg.clone());
push_item(&AssembledGroupInstance {
segments,
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: Vec::new(),
skipped_positions: Vec::new(),
});
}
}
if items.is_empty() {
continue;
}
let field = child.meta.parent_field.as_deref().unwrap_or_default();
match result.get_mut(field) {
Some(serde_json::Value::Array(existing)) => existing.extend(items),
_ => {
result.insert(field.to_string(), serde_json::Value::Array(items));
}
}
}
for child in self
.definitions
.iter()
.filter(|c| is_bound_child_of(c, def))
{
for rep in bound_child_reps(instance, child) {
self.extract_fields_from_instance(rep, child, result, enrich_codes);
}
}
}
fn reverse_nested_children(
&self,
bo4e_value: &serde_json::Value,
def: &MappingDefinition,
instance: &mut AssembledGroupInstance,
) {
let mut handled_fields: Vec<&str> = Vec::new();
for child in self
.definitions
.iter()
.filter(|c| is_nested_child_of(c, def))
{
let field = child.meta.parent_field.as_deref().unwrap_or_default();
if handled_fields.contains(&field) {
continue;
}
if nested_parent_qualifier(child)
.is_some_and(|q| !rebuilt_entry_qualifier_matches(instance, def, q))
{
continue;
}
let elements: Vec<&serde_json::Value> = match bo4e_value.get(field) {
Some(serde_json::Value::Array(arr)) => arr.iter().collect(),
Some(serde_json::Value::Null) | None => continue,
Some(other) => vec![other],
};
let mut reps: Vec<AssembledGroupInstance> = Vec::new();
if let Some(tag) = child.meta.repeat_on_tag.as_deref() {
let mut merged: Option<AssembledGroupInstance> = None;
for element in elements {
let sub = self.map_reverse_single(element, child);
if sub.segments.is_empty() {
continue;
}
match merged.as_mut() {
None => merged = Some(sub),
Some(m) => m.segments.extend(
sub.segments
.into_iter()
.filter(|s| s.tag.eq_ignore_ascii_case(tag)),
),
}
}
reps.extend(merged);
} else {
for element in elements {
let mut sub = self.map_reverse_single(element, child);
if sub.segments.is_empty() {
continue;
}
self.reverse_nested_children(element, child, &mut sub);
reps.push(sub);
}
}
if reps.is_empty() {
continue;
}
handled_fields.push(field);
let (leaf_id, _) = nested_child_leaf(child);
push_child_reps(instance, leaf_id, reps);
}
for child in self
.definitions
.iter()
.filter(|c| is_bound_child_of(c, def))
{
let mut sub = self.map_reverse_single(bo4e_value, child);
if sub.segments.is_empty() {
continue;
}
self.reverse_nested_children(bo4e_value, child, &mut sub);
let (leaf_id, _) = nested_child_leaf(child);
push_child_reps(instance, leaf_id, vec![sub]);
}
}
pub(crate) fn discriminator_qualifier(def: &MappingDefinition) -> Option<String> {
def.meta
.discriminator
.as_deref()
.and_then(|d| d.split_once('=').map(|(_, v)| v.to_string()))
}
pub(crate) fn discriminator_qualifier_for_tag(
def: &MappingDefinition,
segment_tag: &str,
) -> Option<String> {
let (lhs, value) = def.meta.discriminator.as_deref()?.split_once('=')?;
let disc_tag = lhs.split('.').next().unwrap_or(lhs);
disc_tag
.eq_ignore_ascii_case(segment_tag)
.then(|| value.to_string())
}
pub fn map_forward_from_segments(
&self,
segments: &[OwnedSegment],
def: &MappingDefinition,
) -> serde_json::Value {
let assembled_segments: Vec<AssembledSegment> = segments
.iter()
.map(|s| AssembledSegment {
tag: s.id.clone(),
elements: s.elements.clone(),
mig_number: None,
segment_number: Some(s.segment_number),
})
.collect();
let instance = AssembledGroupInstance {
segments: assembled_segments,
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: Vec::new(),
skipped_positions: Vec::new(),
};
let mut result = serde_json::Map::new();
self.extract_fields_from_instance(&instance, def, &mut result, true);
serde_json::Value::Object(result)
}
pub fn map_reverse(
&self,
bo4e_value: &serde_json::Value,
def: &MappingDefinition,
) -> AssembledGroupInstance {
if def.meta.repeat_on_tag.is_some() {
if let Some(arr) = bo4e_value.as_array() {
let mut all_segments = Vec::new();
for elem in arr {
let sub = self.map_reverse_single(elem, def);
all_segments.extend(sub.segments);
}
return AssembledGroupInstance {
segments: all_segments,
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: Vec::new(),
skipped_positions: Vec::new(),
};
}
}
let mut instance = self.map_reverse_single(bo4e_value, def);
if def.meta.parent_field.is_some() {
return instance;
}
if !instance.segments.is_empty() {
self.reverse_nested_children(bo4e_value, def, &mut instance);
return instance;
}
let mut children = AssembledGroupInstance {
segments: vec![],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: Vec::new(),
skipped_positions: Vec::new(),
};
self.reverse_nested_children(bo4e_value, def, &mut children);
if children.child_groups.is_empty() {
return instance;
}
let mut instance = self.map_reverse_single_inner(bo4e_value, def, true);
if !instance.segments.is_empty() {
instance.child_groups = children.child_groups;
}
instance
}
fn map_reverse_single(
&self,
bo4e_value: &serde_json::Value,
def: &MappingDefinition,
) -> AssembledGroupInstance {
self.map_reverse_single_inner(bo4e_value, def, false)
}
fn map_reverse_single_inner(
&self,
bo4e_value: &serde_json::Value,
def: &MappingDefinition,
keep_constants: bool,
) -> AssembledGroupInstance {
let mut field_values: Vec<(String, String, usize, usize, String)> =
Vec::with_capacity(def.fields.len());
let mut has_real_data = false;
let mut has_data_fields = false;
let mut seg_has_data_field: HashSet<String> = HashSet::new();
let mut seg_has_real_data: HashSet<String> = HashSet::new();
let mut injected_qualifiers: HashSet<String> = HashSet::new();
type ListField<'a> = (
&'a str,
&'a str,
String,
Option<String>,
usize,
usize,
Option<&'a std::collections::BTreeMap<String, String>>,
);
let mut list_fields: Vec<ListField<'_>> = Vec::new();
for (path, field_mapping) in &def.fields {
let (target, default, enum_map, when_filled, also_target, also_enum_map) =
match field_mapping {
FieldMapping::Simple(t) => (t.as_str(), None, None, None, None, None),
FieldMapping::Structured(s) => (
s.target.as_str(),
s.default.as_ref(),
self.table(s.enum_map.as_ref(), s.code_list.as_deref()),
s.when_filled.as_ref(),
s.also_target.as_deref(),
self.table(s.also_enum_map.as_ref(), s.also_code_list.as_deref()),
),
FieldMapping::Nested(_) => continue,
};
let parts: Vec<&str> = path.split('.').collect();
if parts.len() < 2 {
continue;
}
let (seg_tag, qualifier, _occ) = parse_tag_qualifier(parts[0]);
let seg_key = parts[0].to_uppercase();
let sub_path = &parts[1..];
let (element_idx, component_idx) = if let Ok(ei) = sub_path[0].parse::<usize>() {
let ci = if sub_path.len() > 1 {
sub_path[1].parse::<usize>().unwrap_or(0)
} else {
0
};
(ei, ci)
} else {
match sub_path.len() {
1 => (0, 0),
2 => (1, 0),
_ => continue,
}
};
if let Some((list, sub)) = list_target(target) {
list_fields.push((
list,
sub,
seg_tag.clone(),
qualifier.map(str::to_string),
element_idx,
component_idx,
enum_map,
));
continue;
}
let val = if target.is_empty() {
match (default, when_filled) {
(Some(d), Some(fields)) => {
let any_filled = fields.iter().any(|f| field_is_filled(bo4e_value, f));
if any_filled {
has_real_data = true;
Some(d.clone())
} else {
None
}
}
(Some(d), None) => Some(d.clone()),
(None, _) => None,
}
} else {
has_data_fields = true;
seg_has_data_field.insert(seg_key.clone());
let bo4e_val = self.populate_field(bo4e_value, target);
if bo4e_val.is_some() {
has_real_data = true;
seg_has_real_data.insert(seg_key.clone());
}
let mapped_val = match (bo4e_val, enum_map) {
(Some(v), Some(map)) => {
let joint = match (also_target, also_enum_map) {
(Some(also), Some(also_map)) => {
self.populate_field(bo4e_value, also).and_then(|also_v| {
map.iter()
.find(|(code, bo4e_v)| {
*bo4e_v == &v && also_map.get(*code) == Some(&also_v)
})
.map(|(code, _)| code.clone())
})
}
_ => None,
};
joint
.or_else(|| {
map.iter()
.find(|(_, bo4e_v)| *bo4e_v == &v)
.map(|(edifact_k, _)| edifact_k.clone())
})
.or(Some(v))
}
(v, _) => v,
};
mapped_val.or_else(|| default.cloned())
};
if let Some(val) = val {
field_values.push((
seg_key.clone(),
seg_tag.clone(),
element_idx,
component_idx,
val,
));
}
if let Some(q) = qualifier {
if injected_qualifiers.insert(seg_key.clone()) {
field_values.push((seg_key, seg_tag, 0, 0, q.to_string()));
}
}
}
let longest = list_fields
.iter()
.filter_map(|(list, ..)| bo4e_value.get(*list).and_then(|v| v.as_array()))
.map(|a| a.len())
.max()
.unwrap_or(0);
if !list_fields.is_empty() {
has_data_fields = true;
}
for i in 0..longest {
for (list, sub, seg_tag, qualifier, element_idx, component_idx, enum_map) in
&list_fields
{
let key = match qualifier {
Some(q) => format!("{seg_tag}[{q},{i}]"),
None => format!("{seg_tag}[*,{i}]"),
};
seg_has_data_field.insert(key.clone());
let Some(item) = bo4e_value
.get(*list)
.and_then(|v| v.as_array())
.and_then(|a| a.get(i))
else {
continue;
};
let Some(value) = self.populate_field(item, sub) else {
continue;
};
let value = match enum_map {
Some(map) => map
.iter()
.find(|(_, name)| **name == value)
.map(|(code, _)| code.clone())
.unwrap_or(value),
None => value,
};
has_real_data = true;
seg_has_real_data.insert(key.clone());
field_values.push((
key.clone(),
seg_tag.clone(),
*element_idx,
*component_idx,
value,
));
if let Some(q) = qualifier {
if injected_qualifiers.insert(key.clone()) {
field_values.push((key, seg_tag.clone(), 0, 0, q.clone()));
}
}
}
}
field_values.retain(|(seg_key, _, _, _, _)| {
if !seg_key.contains('[') {
return true; }
!seg_has_data_field.contains(seg_key) || seg_has_real_data.contains(seg_key)
});
if has_data_fields && !has_real_data && !keep_constants {
return AssembledGroupInstance {
segments: vec![],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: Vec::new(),
skipped_positions: Vec::new(),
};
}
let mut segments: Vec<AssembledSegment> = Vec::with_capacity(field_values.len());
let mut seen_keys: HashMap<String, usize> = HashMap::new();
for (seg_key, seg_tag, element_idx, component_idx, val) in &field_values {
let seg = if let Some(&pos) = seen_keys.get(seg_key) {
&mut segments[pos]
} else {
let pos = segments.len();
seen_keys.insert(seg_key.clone(), pos);
segments.push(AssembledSegment {
tag: seg_tag.clone(),
elements: vec![],
mig_number: None,
segment_number: None,
});
&mut segments[pos]
};
while seg.elements.len() <= *element_idx {
seg.elements.push(vec![]);
}
while seg.elements[*element_idx].len() <= *component_idx {
seg.elements[*element_idx].push(String::new());
}
seg.elements[*element_idx][*component_idx] = val.clone();
}
for seg in &mut segments {
let last_populated = seg.elements.iter().rposition(|e| !e.is_empty());
if let Some(last_idx) = last_populated {
for i in 0..last_idx {
if seg.elements[i].is_empty() {
seg.elements[i] = vec![String::new()];
}
}
}
}
if let Some(ref ss) = self.segment_structure {
for seg in &mut segments {
if let Some(expected) = ss.element_count(&seg.tag) {
while seg.elements.len() < expected {
seg.elements.push(vec![String::new()]);
}
}
}
}
AssembledGroupInstance {
segments,
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: Vec::new(),
skipped_positions: Vec::new(),
}
}
fn resolve_field_path(segment: &AssembledSegment, path: &[&str]) -> Option<String> {
if path.is_empty() {
return None;
}
if let Ok(element_idx) = path[0].parse::<usize>() {
let component_idx = if path.len() > 1 {
path[1].parse::<usize>().unwrap_or(0)
} else {
0
};
return segment
.elements
.get(element_idx)?
.get(component_idx)
.filter(|v| !v.is_empty())
.cloned();
}
None
}
pub(crate) fn parse_element_component(parts: &[&str]) -> (usize, usize) {
if parts.is_empty() {
return (0, 0);
}
let element_idx = parts[0].parse::<usize>().unwrap_or(0);
let component_idx = if parts.len() > 1 {
parts[1].parse::<usize>().unwrap_or(0)
} else {
0
};
(element_idx, component_idx)
}
pub fn populate_field(
&self,
bo4e_value: &serde_json::Value,
target_field: &str,
) -> Option<String> {
let mut current = bo4e_value;
for part in target_field.split('.') {
current = current.get(part)?;
}
if let Some(code) = current.get("code").and_then(|v| v.as_str()) {
return Some(code.to_string());
}
current.as_str().map(|s| s.to_string())
}
pub fn build_segment_from_bo4e(
&self,
bo4e_value: &serde_json::Value,
segment_tag: &str,
target_field: &str,
) -> AssembledSegment {
let value = self.populate_field(bo4e_value, target_field);
let elements = if let Some(val) = value {
vec![vec![val]]
} else {
vec![]
};
AssembledSegment {
tag: segment_tag.to_uppercase(),
elements,
mig_number: None,
segment_number: None,
}
}
pub fn resolve_repetition(
tree: &AssembledTree,
group_path: &str,
discriminator: &str,
) -> Option<usize> {
let (spec, expected) = discriminator.split_once('=')?;
let parts: Vec<&str> = spec.split('.').collect();
if parts.len() != 3 {
return None;
}
let tag = parts[0];
let element_idx: usize = parts[1].parse().ok()?;
let component_idx: usize = parts[2].parse().ok()?;
let path_parts: Vec<&str> = group_path.split('.').collect();
let leaf_group = if path_parts.len() == 1 {
let (group_id, _) = parse_group_spec(path_parts[0]);
tree.groups.iter().find(|g| g.group_id == group_id)?
} else {
let parent_parts = &path_parts[..path_parts.len() - 1];
let mut current_instance = {
let (first_id, first_rep) = parse_group_spec(parent_parts[0]);
let first_group = tree.groups.iter().find(|g| g.group_id == first_id)?;
first_group.repetitions.get(first_rep.unwrap_or(0))?
};
for part in &parent_parts[1..] {
let (group_id, explicit_rep) = parse_group_spec(part);
let child_group = current_instance
.child_groups
.iter()
.find(|g| g.group_id == group_id)?;
current_instance = child_group.repetitions.get(explicit_rep.unwrap_or(0))?;
}
let (leaf_id, _) = parse_group_spec(path_parts.last()?);
current_instance
.child_groups
.iter()
.find(|g| g.group_id == leaf_id)?
};
let expected_values: Vec<&str> = expected.split('|').collect();
for (rep_idx, instance) in leaf_group.repetitions.iter().enumerate() {
let matches = instance.segments.iter().any(|s| {
s.tag.eq_ignore_ascii_case(tag)
&& s.elements
.get(element_idx)
.and_then(|e| e.get(component_idx))
.map(|v| expected_values.iter().any(|ev| v == ev))
.unwrap_or(false)
});
if matches {
return Some(rep_idx);
}
}
None
}
pub fn resolve_all_repetitions(
tree: &AssembledTree,
group_path: &str,
discriminator: &str,
) -> Vec<usize> {
let Some((spec, expected)) = discriminator.split_once('=') else {
return Vec::new();
};
let parts: Vec<&str> = spec.split('.').collect();
if parts.len() != 3 {
return Vec::new();
}
let tag = parts[0];
let element_idx: usize = match parts[1].parse() {
Ok(v) => v,
Err(_) => return Vec::new(),
};
let component_idx: usize = match parts[2].parse() {
Ok(v) => v,
Err(_) => return Vec::new(),
};
let path_parts: Vec<&str> = group_path.split('.').collect();
let leaf_group = if path_parts.len() == 1 {
let (group_id, _) = parse_group_spec(path_parts[0]);
match tree.groups.iter().find(|g| g.group_id == group_id) {
Some(g) => g,
None => return Vec::new(),
}
} else {
let parent_parts = &path_parts[..path_parts.len() - 1];
let mut current_instance = {
let (first_id, first_rep) = parse_group_spec(parent_parts[0]);
let first_group = match tree.groups.iter().find(|g| g.group_id == first_id) {
Some(g) => g,
None => return Vec::new(),
};
match first_group.repetitions.get(first_rep.unwrap_or(0)) {
Some(i) => i,
None => return Vec::new(),
}
};
for part in &parent_parts[1..] {
let (group_id, explicit_rep) = parse_group_spec(part);
let child_group = match current_instance
.child_groups
.iter()
.find(|g| g.group_id == group_id)
{
Some(g) => g,
None => return Vec::new(),
};
current_instance = match child_group.repetitions.get(explicit_rep.unwrap_or(0)) {
Some(i) => i,
None => return Vec::new(),
};
}
let (leaf_id, _) = match path_parts.last() {
Some(p) => parse_group_spec(p),
None => return Vec::new(),
};
match current_instance
.child_groups
.iter()
.find(|g| g.group_id == leaf_id)
{
Some(g) => g,
None => return Vec::new(),
}
};
let (expected_raw, occurrence) = parse_discriminator_occurrence(expected);
let expected_values: Vec<&str> = expected_raw.split('|').collect();
let mut result = Vec::new();
for (rep_idx, instance) in leaf_group.repetitions.iter().enumerate() {
let matches = instance.segments.iter().any(|s| {
s.tag.eq_ignore_ascii_case(tag)
&& s.elements
.get(element_idx)
.and_then(|e| e.get(component_idx))
.map(|v| expected_values.iter().any(|ev| v == ev))
.unwrap_or(false)
});
if matches {
result.push(rep_idx);
}
}
if let Some(occ) = occurrence {
result.into_iter().nth(occ).into_iter().collect()
} else {
result
}
}
pub fn map_all_forward(&self, tree: &AssembledTree) -> serde_json::Value {
self.map_all_forward_inner(tree, true).0
}
pub fn map_all_forward_enriched(
&self,
tree: &AssembledTree,
enrich_codes: bool,
) -> serde_json::Value {
self.map_all_forward_inner(tree, enrich_codes).0
}
fn map_all_forward_inner(
&self,
tree: &AssembledTree,
enrich_codes: bool,
) -> (
serde_json::Value,
std::collections::HashMap<String, Vec<usize>>,
) {
self.map_all_forward_inner_with_tx(tree, enrich_codes, self.transaction_group.as_deref())
}
fn map_all_forward_inner_with_tx(
&self,
tree: &AssembledTree,
enrich_codes: bool,
tx_group_override: Option<&str>,
) -> (
serde_json::Value,
std::collections::HashMap<String, Vec<usize>>,
) {
let mut result = serde_json::Map::new();
let mut nesting_info: std::collections::HashMap<String, Vec<usize>> =
std::collections::HashMap::new();
let mut contributors: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for def in &self.definitions {
if def.meta.parent_field.is_some() || is_bound_child(&self.definitions, def) {
continue;
}
let entity = &def.meta.entity;
let bo4e = if let Some(ref disc) = def.meta.discriminator {
let use_source_path = def
.meta
.source_path
.as_ref()
.is_some_and(|sp| has_source_path_qualifiers(sp));
if use_source_path {
let sp = def.meta.source_path.as_deref().unwrap();
let all_instances = Self::resolve_all_by_source_path(tree, sp);
let instances: Vec<_> = if let Some(matcher) = DiscriminatorMatcher::parse(disc)
{
matcher.filter_instances(all_instances)
} else {
all_instances
};
let extract = |instance: &AssembledGroupInstance| {
let mut r = serde_json::Map::new();
self.extract_fields_from_instance(instance, def, &mut r, enrich_codes);
serde_json::Value::Object(r)
};
match instances.len() {
0 => None,
1 => Some(extract(instances[0])),
_ => Some(serde_json::Value::Array(
instances.iter().map(|i| extract(i)).collect(),
)),
}
} else {
let reps = Self::resolve_all_repetitions(tree, &def.meta.source_group, disc);
match reps.len() {
0 => None,
1 => Some(self.map_forward_inner(tree, def, reps[0], enrich_codes)),
_ => Some(serde_json::Value::Array(
reps.iter()
.map(|&rep| self.map_forward_inner(tree, def, rep, enrich_codes))
.collect(),
)),
}
}
} else if def.meta.source_group.is_empty() {
Some(self.map_forward_inner(tree, def, 0, enrich_codes))
} else if def.meta.source_path.as_ref().is_some_and(|sp| {
has_source_path_qualifiers(sp) || def.meta.source_group.contains('.')
}) {
let sp = def.meta.source_path.as_deref().unwrap();
let mut indexed = Self::resolve_all_with_parent_indices(tree, sp);
if let Some(last_part) = sp.rsplit('.').next() {
if !last_part.contains('_') {
let base_prefix = if let Some(parent) = sp.rsplit_once('.') {
format!("{}.", parent.0)
} else {
String::new()
};
let sibling_qualifiers: Vec<String> = self
.definitions
.iter()
.filter_map(|d| d.meta.source_path.as_deref())
.filter(|other_sp| {
*other_sp != sp
&& other_sp.starts_with(&base_prefix)
&& other_sp.split('.').count() == sp.split('.').count()
})
.filter_map(|other_sp| {
let other_last = other_sp.rsplit('.').next()?;
let (base, q) = other_last.split_once('_')?;
if base == last_part {
Some(q.to_string())
} else {
None
}
})
.collect();
if !sibling_qualifiers.is_empty() {
indexed.retain(|(_, inst)| {
let entry_qual = inst
.segments
.first()
.and_then(|seg| seg.elements.first())
.and_then(|el| el.first())
.map(|v| v.to_lowercase());
!entry_qual.is_some_and(|q| {
sibling_qualifiers.iter().any(|sq| {
sq.split('_').any(|part| part.eq_ignore_ascii_case(&q))
})
})
});
}
}
}
let extract = |instance: &AssembledGroupInstance| {
let mut r = serde_json::Map::new();
self.extract_fields_from_instance(instance, def, &mut r, enrich_codes);
serde_json::Value::Object(r)
};
if def.meta.source_group.contains('.') && !indexed.is_empty() {
if let Some(sp) = &def.meta.source_path {
let parent_indices: Vec<usize> =
indexed.iter().map(|(idx, _)| *idx).collect();
nesting_info.entry(sp.clone()).or_insert(parent_indices);
let child_key = format!("{sp}#child");
if let std::collections::hash_map::Entry::Vacant(e) =
nesting_info.entry(child_key)
{
let child_indices: Vec<usize> =
Self::compute_child_indices(tree, sp, &indexed);
if !child_indices.is_empty() {
e.insert(child_indices);
}
}
}
}
match indexed.len() {
0 => None,
1 => Some(extract(indexed[0].1)),
_ => Some(serde_json::Value::Array(
indexed.iter().map(|(_, i)| extract(i)).collect(),
)),
}
} else {
let num_reps = Self::count_repetitions(tree, &def.meta.source_group);
if num_reps <= 1 {
Some(self.map_forward_inner(tree, def, 0, enrich_codes))
} else {
let mut items = Vec::with_capacity(num_reps);
for rep in 0..num_reps {
items.push(self.map_forward_inner(tree, def, rep, enrich_codes));
}
Some(serde_json::Value::Array(items))
}
};
if let Some(bo4e) = bo4e {
let key = to_camel_case(entity);
match def.meta.target_list.as_deref() {
Some(list_field) => append_to_list_field(&mut result, &key, list_field, bo4e),
None => {
let group = def
.meta
.source_path
.clone()
.unwrap_or_else(|| def.meta.source_group.to_lowercase());
let seen = contributors.entry(key.clone()).or_default();
let nested = seen.iter().any(|other: &String| {
group.starts_with(&format!("{other}."))
|| other.starts_with(&format!("{group}."))
});
seen.push(group);
merge_entity(&mut result, &key, bo4e, !nested);
}
}
}
}
nest_child_entities_in_result(
&mut result,
&self.definitions,
&nesting_info,
tx_group_override,
);
(serde_json::Value::Object(result), nesting_info)
}
pub fn map_all_reverse(
&self,
entities: &serde_json::Value,
nesting_info: Option<&std::collections::HashMap<String, Vec<usize>>>,
) -> AssembledTree {
self.map_all_reverse_with_mig(entities, nesting_info, None)
}
pub fn map_all_reverse_with_mig(
&self,
entities: &serde_json::Value,
nesting_info: Option<&std::collections::HashMap<String, Vec<usize>>>,
mig: Option<&MigSchema>,
) -> AssembledTree {
let mut root_segments: Vec<AssembledSegment> = Vec::new();
let mut groups: Vec<AssembledGroup> = Vec::new();
let mut inferred_nesting: std::collections::HashMap<String, Vec<usize>> =
std::collections::HashMap::new();
for def in &self.definitions {
if def.meta.parent_field.is_some() || is_bound_child(&self.definitions, def) {
continue;
}
let entity_key = to_camel_case(&def.meta.entity);
let _extracted: Option<serde_json::Value>;
let entity_value = if let Some(list_field) = def.meta.target_list.as_deref() {
match entities.get(&entity_key).and_then(|e| e.get(list_field)) {
Some(v) if v.is_array() => {
_extracted = None;
v
}
_ => continue,
}
} else if let Some(v) = entities.get(&entity_key) {
_extracted = None;
v
} else if def.meta.source_group.contains('.') {
match extract_child_from_parent_with_indices(entities, &self.definitions, def) {
Some((v, parent_indices)) => {
if let Some(sp) = def.meta.source_path.as_deref() {
inferred_nesting
.entry(sp.to_string())
.or_insert(parent_indices);
}
_extracted = Some(v);
_extracted.as_ref().unwrap()
}
None => continue,
}
} else {
continue;
};
let unwrapped: Option<serde_json::Value>;
let entity_value = if entity_value.is_object() && !entity_value.is_array() {
if let Some(disc_value) = def
.meta
.discriminator
.as_deref()
.and_then(|d| d.split_once('='))
.map(|(_, v)| v)
{
if let Some(inner) = entity_value.get(disc_value) {
let mut injected = inner.clone();
if let Some(qualifier_field) =
find_qualifier_companion_field(&self.definitions, &def.meta.entity)
{
if let Some(obj) = injected.as_object_mut() {
let entry = obj
.entry(qualifier_field)
.or_insert(serde_json::Value::Null);
if entry.is_null() {
*entry = serde_json::Value::String(disc_value.to_string());
}
}
}
unwrapped = Some(injected);
unwrapped.as_ref().unwrap()
} else {
entity_value
}
} else if is_map_keyed_object(entity_value) {
let map = entity_value.as_object().unwrap();
let arr: Vec<serde_json::Value> = map
.iter()
.map(|(key, val)| {
let mut item = val.clone();
if let Some(obj) = item.as_object_mut() {
if let Some(qualifier_field) = find_qualifier_companion_field(
&self.definitions,
&def.meta.entity,
) {
let entry = obj
.entry(qualifier_field)
.or_insert(serde_json::Value::Null);
if entry.is_null() {
*entry = serde_json::Value::String(key.clone());
}
}
}
item
})
.collect();
unwrapped = Some(serde_json::Value::Array(arr));
unwrapped.as_ref().unwrap()
} else {
entity_value
}
} else {
entity_value
};
let leaf_group = def
.meta
.source_group
.rsplit('.')
.next()
.unwrap_or(&def.meta.source_group);
if def.meta.source_group.is_empty() {
let instance = self.map_reverse(entity_value, def);
root_segments.extend(instance.segments);
} else if entity_value.is_array() {
let arr = entity_value.as_array().unwrap();
let reps: Vec<_> = arr.iter().map(|item| self.map_reverse(item, def)).collect();
if let Some(existing) = groups.iter_mut().find(|g| g.group_id == leaf_group) {
existing.repetitions.extend(reps);
} else {
groups.push(AssembledGroup {
group_id: leaf_group.to_string(),
repetitions: reps,
});
}
} else {
let instance = self.map_reverse(entity_value, def);
if let Some(existing) = groups.iter_mut().find(|g| g.group_id == leaf_group) {
existing.repetitions.push(instance);
} else {
groups.push(AssembledGroup {
group_id: leaf_group.to_string(),
repetitions: vec![instance],
});
}
}
}
let nested_specs: Vec<(String, String)> = self
.definitions
.iter()
.filter(|def| def.meta.parent_field.is_none())
.filter_map(|def| {
let parts: Vec<&str> = def.meta.source_group.split('.').collect();
if parts.len() > 1 {
Some((parts[0].to_string(), parts[parts.len() - 1].to_string()))
} else {
None
}
})
.collect();
for (parent_id, child_id) in &nested_specs {
let has_parent = groups.iter().any(|g| g.group_id == *parent_id);
let has_child = groups.iter().any(|g| g.group_id == *child_id);
if has_parent && has_child {
let child_idx = groups.iter().position(|g| g.group_id == *child_id).unwrap();
let child_group = groups.remove(child_idx);
let parent = groups
.iter_mut()
.find(|g| g.group_id == *parent_id)
.unwrap();
let child_source_path = self
.definitions
.iter()
.find(|d| {
let parts: Vec<&str> = d.meta.source_group.split('.').collect();
d.meta.parent_field.is_none()
&& parts.len() > 1
&& parts[parts.len() - 1] == *child_id
})
.and_then(|d| d.meta.source_path.as_deref());
let distribution = child_source_path.and_then(|key| {
nesting_info
.and_then(|ni| ni.get(key))
.or_else(|| inferred_nesting.get(key))
});
let unlinked_target = mig
.and_then(|m| {
mig_assembly::repetition_order::preferred_parent_repetition(
parent,
&m.segment_groups,
child_id,
)
})
.unwrap_or(0);
for (i, child_rep) in child_group.repetitions.into_iter().enumerate() {
let target_idx = distribution
.and_then(|dist| dist.get(i))
.copied()
.unwrap_or(unlinked_target);
if let Some(target_rep) = parent.repetitions.get_mut(target_idx) {
if let Some(existing) = target_rep
.child_groups
.iter_mut()
.find(|g| g.group_id == *child_id)
{
existing.repetitions.push(child_rep);
} else {
target_rep.child_groups.push(AssembledGroup {
group_id: child_id.clone(),
repetitions: vec![child_rep],
});
}
}
}
}
}
let post_group_start = root_segments.len();
AssembledTree {
segments: root_segments,
groups,
post_group_start,
inter_group_segments: std::collections::BTreeMap::new(),
}
}
fn count_repetitions(tree: &AssembledTree, group_path: &str) -> usize {
let parts: Vec<&str> = group_path.split('.').collect();
let (first_id, first_rep) = parse_group_spec(parts[0]);
let first_group = match tree.groups.iter().find(|g| g.group_id == first_id) {
Some(g) => g,
None => return 0,
};
if parts.len() == 1 {
return first_group.repetitions.len();
}
let mut current_instance = match first_group.repetitions.get(first_rep.unwrap_or(0)) {
Some(i) => i,
None => return 0,
};
for (i, part) in parts[1..].iter().enumerate() {
let (group_id, explicit_rep) = parse_group_spec(part);
let child_group = match current_instance
.child_groups
.iter()
.find(|g| g.group_id == group_id)
{
Some(g) => g,
None => return 0,
};
if i == parts.len() - 2 {
return child_group.repetitions.len();
}
current_instance = match child_group.repetitions.get(explicit_rep.unwrap_or(0)) {
Some(i) => i,
None => return 0,
};
}
0
}
pub fn translate_edifact_to_bo4e(
msg_engine: &MappingEngine,
tx_engine: &MappingEngine,
tree: &AssembledTree,
transaction_group: &str,
) -> crate::model::MappedMessage {
Self::map_interchange_inner(msg_engine, tx_engine, tree, transaction_group, false)
}
pub fn enrich_bo4e_types(
msg_engine: &MappingEngine,
tx_engine: &MappingEngine,
mapped: &mut crate::model::MappedMessage,
) {
msg_engine.enrich_entities(&mut mapped.stammdaten);
for tx in &mut mapped.transaktionen {
tx_engine.enrich_entities(&mut tx.stammdaten);
}
msg_engine.enrich_named_entity(
&mut mapped.nachricht_meta,
crate::model::MSG_METADATA_ENTITY,
);
for tx in &mut mapped.transaktionen {
tx_engine
.enrich_named_entity(&mut tx.transaktionsdaten, crate::model::TX_METADATA_ENTITY);
}
}
fn enrich_named_entity(&self, value: &mut serde_json::Value, entity_key: &str) {
if self.code_lookup.is_none() || value.is_null() {
return;
}
let sites = self.code_sites();
if let Some(entity_sites) = sites.get(entity_key) {
Self::apply_sites(self, value, entity_sites);
}
}
fn enrich_entities(&self, value: &mut serde_json::Value) {
if self.code_lookup.is_none() {
return;
}
let sites: HashMap<String, Vec<CodeSite<'_>>> = self.code_sites();
if sites.is_empty() {
return;
}
Self::walk_and_enrich(self, value, &sites);
}
fn code_sites(&self) -> HashMap<String, Vec<CodeSite<'_>>> {
let Some(ref code_lookup) = self.code_lookup else {
return HashMap::new();
};
let mut sites: HashMap<String, Vec<CodeSite<'_>>> = HashMap::new();
for def in &self.definitions {
let Some(ref source_path) = def.meta.source_path else {
continue;
};
let entity_key = to_camel_case(&def.meta.entity);
for (path, field_mapping) in &def.fields {
let (target, enum_map, also_target, also_enum_map) = match field_mapping {
FieldMapping::Simple(t) => (t.as_str(), None, None, None),
FieldMapping::Structured(s) => (
s.target.as_str(),
self.table(s.enum_map.as_ref(), s.code_list.as_deref()),
s.also_target.as_deref(),
self.table(s.also_enum_map.as_ref(), s.also_code_list.as_deref()),
),
FieldMapping::Nested(_) => continue,
};
if target.is_empty() {
continue;
}
let parts: Vec<&str> = path.split('.').collect();
let (seg_tag, path_qualifier, _occ) = parse_tag_qualifier(parts[0]);
let (element_idx, component_idx) = Self::parse_element_component(&parts[1..]);
let disc_qualifier = Self::discriminator_qualifier_for_tag(def, &seg_tag);
if code_lookup
.enrichment_codes(
source_path,
&seg_tag,
path_qualifier,
disc_qualifier.as_deref(),
element_idx,
component_idx,
)
.is_none()
{
continue;
}
sites.entry(entity_key.clone()).or_default().push(CodeSite {
target,
parent_field: def.meta.parent_field.as_deref(),
source_path,
seg_tag,
path_qualifier: path_qualifier.map(str::to_string),
disc_qualifier,
element_idx,
component_idx,
enum_map,
also_target,
also_enum_map,
});
}
}
sites
}
fn walk_and_enrich(
engine: &MappingEngine,
value: &mut serde_json::Value,
sites: &HashMap<String, Vec<CodeSite<'_>>>,
) {
match value {
serde_json::Value::Object(map) => {
for (key, child) in map.iter_mut() {
if let Some(entity_sites) = sites.get(key.as_str()) {
Self::apply_sites(engine, child, entity_sites);
}
Self::walk_and_enrich(engine, child, sites);
}
}
serde_json::Value::Array(items) => {
for item in items.iter_mut() {
Self::walk_and_enrich(engine, item, sites);
}
}
_ => {}
}
}
fn apply_sites(engine: &MappingEngine, value: &mut serde_json::Value, sites: &[CodeSite<'_>]) {
match value {
serde_json::Value::Array(items) => {
for item in items.iter_mut() {
Self::apply_sites(engine, item, sites);
}
}
serde_json::Value::Object(_) => {
for site in sites {
match site.parent_field {
None => engine.enrich_one(value, site),
Some(field) => {
if let Some(nested) = value.get_mut(field) {
Self::apply_nested_site(engine, nested, site);
}
}
}
}
}
_ => {}
}
}
fn apply_nested_site(
engine: &MappingEngine,
value: &mut serde_json::Value,
site: &CodeSite<'_>,
) {
match value {
serde_json::Value::Array(items) => {
for item in items.iter_mut() {
Self::apply_nested_site(engine, item, site);
}
}
serde_json::Value::Object(_) => engine.enrich_one(value, site),
_ => {}
}
}
fn enrich_one(&self, entity: &mut serde_json::Value, site: &CodeSite<'_>) {
if let Some((list, sub)) = list_target(site.target) {
if let Some(items) = entity.get_mut(list).and_then(|v| v.as_array_mut()) {
let element_site = CodeSite {
target: sub,
..site.clone()
};
for item in items {
self.enrich_one(item, &element_site);
}
}
return;
}
let Some(ref code_lookup) = self.code_lookup else {
return;
};
let Some(mapped_val) = Self::read_plain_string(entity, site.target) else {
return;
};
let raw = match site.enum_map {
None => mapped_val.clone(),
Some(map) => {
let joint = match (site.also_target, site.also_enum_map) {
(Some(also), Some(also_map)) => {
Self::read_plain_string(entity, also).and_then(|also_v| {
map.iter()
.find(|(code, bo4e_v)| {
*bo4e_v == &mapped_val && also_map.get(*code) == Some(&also_v)
})
.map(|(code, _)| code.clone())
})
}
_ => None,
};
joint
.or_else(|| {
map.iter()
.find(|(_, bo4e_v)| *bo4e_v == &mapped_val)
.map(|(code, _)| code.clone())
})
.unwrap_or_else(|| mapped_val.clone())
}
};
let Some(codes) = code_lookup.enrichment_codes(
site.source_path,
&site.seg_tag,
site.path_qualifier.as_deref(),
site.disc_qualifier.as_deref(),
site.element_idx,
site.component_idx,
) else {
return;
};
if let Some(ref pid) = self.current_pid {
if codes.len() == 1 && codes.contains_key(pid.as_str()) {
return;
}
}
let enrichment = codes.get(&raw);
let meaning = enrichment
.map(|e| serde_json::Value::String(e.meaning.clone()))
.unwrap_or(serde_json::Value::Null);
let mut obj = serde_json::Map::new();
obj.insert("code".into(), serde_json::json!(mapped_val));
obj.insert("meaning".into(), meaning);
if let Some(enum_key) = enrichment.and_then(|e| e.enum_key.as_ref()) {
obj.insert("enum".into(), serde_json::json!(enum_key));
}
if let serde_json::Value::Object(map) = entity {
set_nested_value_json(map, site.target, serde_json::Value::Object(obj));
}
}
fn read_plain_string(entity: &serde_json::Value, target: &str) -> Option<String> {
let mut current = entity;
for part in target.split('.') {
current = current.get(part)?;
}
current.as_str().map(str::to_string)
}
pub fn map_interchange(
msg_engine: &MappingEngine,
tx_engine: &MappingEngine,
tree: &AssembledTree,
transaction_group: &str,
enrich_codes: bool,
) -> crate::model::MappedMessage {
let mut mapped =
Self::translate_edifact_to_bo4e(msg_engine, tx_engine, tree, transaction_group);
if enrich_codes {
Self::enrich_bo4e_types(msg_engine, tx_engine, &mut mapped);
}
mapped
}
#[doc(hidden)]
pub fn map_interchange_inner_for_test(
msg_engine: &MappingEngine,
tx_engine: &MappingEngine,
tree: &AssembledTree,
transaction_group: &str,
enrich_codes: bool,
) -> crate::model::MappedMessage {
Self::map_interchange_inner(msg_engine, tx_engine, tree, transaction_group, enrich_codes)
}
pub(crate) fn map_interchange_inner(
msg_engine: &MappingEngine,
tx_engine: &MappingEngine,
tree: &AssembledTree,
transaction_group: &str,
enrich_codes: bool,
) -> crate::model::MappedMessage {
let (stammdaten, nesting_info) = msg_engine.map_all_forward_inner(tree, enrich_codes);
let transaktionen = tree
.groups
.iter()
.find(|g| g.group_id == transaction_group)
.map(|sg| {
sg.repetitions
.iter()
.map(|instance| {
let wrapped_tree = AssembledTree {
segments: vec![],
groups: vec![AssembledGroup {
group_id: transaction_group.to_string(),
repetitions: vec![instance.clone()],
}],
post_group_start: 0,
inter_group_segments: std::collections::BTreeMap::new(),
};
let (tx_result, tx_nesting) = tx_engine.map_all_forward_inner_with_tx(
&wrapped_tree,
enrich_codes,
Some(transaction_group),
);
let mut tx_result = tx_result;
let transaktionsdaten = crate::model::take_entity(
&mut tx_result,
crate::model::TX_METADATA_ENTITY,
);
crate::model::MappedTransaktion {
stammdaten: tx_result,
transaktionsdaten,
nesting_info: tx_nesting,
}
})
.collect()
})
.unwrap_or_default();
let mut stammdaten = stammdaten;
let nachricht_meta =
crate::model::take_entity(&mut stammdaten, crate::model::MSG_METADATA_ENTITY);
crate::model::MappedMessage {
stammdaten,
nachricht_meta,
transaktionen,
nesting_info,
inter_group_segments: tree.inter_group_segments.clone(),
}
}
pub fn map_interchange_reverse(
msg_engine: &MappingEngine,
tx_engine: &MappingEngine,
mapped: &crate::model::MappedMessage,
transaction_group: &str,
filtered_mig: Option<&MigSchema>,
) -> AssembledTree {
let _owned_msg: Option<serde_json::Value>;
let msg_stammdaten = if !mapped.nachricht_meta.is_null() {
let mut merged = mapped.stammdaten.clone();
crate::model::restore_entity(
&mut merged,
crate::model::MSG_METADATA_ENTITY,
&mapped.nachricht_meta,
);
_owned_msg = Some(merged);
_owned_msg.as_ref().unwrap()
} else {
_owned_msg = None;
&mapped.stammdaten
};
let msg_tree = msg_engine.map_all_reverse_with_mig(
msg_stammdaten,
if mapped.nesting_info.is_empty() {
None
} else {
Some(&mapped.nesting_info)
},
filtered_mig,
);
let mut sg4_reps: Vec<AssembledGroupInstance> = Vec::new();
struct DefWithMeta<'a> {
def: &'a MappingDefinition,
relative: String,
depth: usize,
}
let mut sorted_defs: Vec<DefWithMeta> = tx_engine
.definitions
.iter()
.filter(|def| {
def.meta.parent_field.is_none() && !is_bound_child(&tx_engine.definitions, def)
})
.map(|def| {
let relative = strip_tx_group_prefix(&def.meta.source_group, transaction_group);
let depth = if relative.is_empty() {
0
} else {
relative.chars().filter(|c| *c == '.').count() + 1
};
DefWithMeta {
def,
relative,
depth,
}
})
.collect();
let mut parent_rep_map: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
for dm in &sorted_defs {
if dm.depth >= 2 {
let parts: Vec<&str> = dm.relative.split('.').collect();
let (_, parent_rep) = parse_group_spec(parts[0]);
if let Some(rep_idx) = parent_rep {
if let Some(sp) = &dm.def.meta.source_path {
if let Some((parent_path, _)) = sp.rsplit_once('.') {
parent_rep_map
.entry(parent_path.to_string())
.or_insert(rep_idx);
}
}
}
}
}
for dm in &mut sorted_defs {
if dm.depth == 1 && !dm.relative.contains(':') {
if let Some(sp) = &dm.def.meta.source_path {
if let Some(rep_idx) = parent_rep_map.get(sp.as_str()) {
dm.relative = format!("{}:{}", dm.relative, rep_idx);
}
}
}
}
if let Some(mig) = filtered_mig {
let mig_order = build_reverse_mig_group_order(mig, transaction_group);
sorted_defs.sort_by(|a, b| {
a.depth.cmp(&b.depth).then_with(|| {
let a_id = a.relative.split(':').next().unwrap_or(&a.relative);
let b_id = b.relative.split(':').next().unwrap_or(&b.relative);
let a_pos = variant_mig_position(a.def, a_id, &mig_order);
let b_pos = variant_mig_position(b.def, b_id, &mig_order);
a_pos.cmp(&b_pos).then(a.relative.cmp(&b.relative))
})
});
} else {
sorted_defs.sort_by(|a, b| a.depth.cmp(&b.depth).then(a.relative.cmp(&b.relative)));
}
for tx in &mapped.transaktionen {
let mut root_segs: Vec<AssembledSegment> = Vec::new();
let mut child_groups: Vec<AssembledGroup> = Vec::new();
let _owned_tx: Option<serde_json::Value>;
let tx_stammdaten: &serde_json::Value = if !tx.transaktionsdaten.is_null() {
let mut merged = tx.stammdaten.clone();
crate::model::restore_entity(
&mut merged,
crate::model::TX_METADATA_ENTITY,
&tx.transaktionsdaten,
);
_owned_tx = Some(merged);
_owned_tx.as_ref().unwrap()
} else {
_owned_tx = None;
&tx.stammdaten
};
let mut source_path_to_rep: std::collections::HashMap<String, Vec<usize>> =
std::collections::HashMap::new();
for dm in &sorted_defs {
let entity_key = to_camel_case(&dm.def.meta.entity);
let _tx_extracted: Option<serde_json::Value>;
let bo4e_value = if let Some(v) = tx_stammdaten.get(&entity_key) {
_tx_extracted = None;
v
} else if dm.def.meta.source_group.contains('.') {
match extract_child_from_parent(tx_stammdaten, &tx_engine.definitions, dm.def) {
Some(v) => {
_tx_extracted = Some(v);
_tx_extracted.as_ref().unwrap()
}
None => continue,
}
} else {
continue;
};
let unwrapped_value: Option<serde_json::Value>;
let bo4e_value = if bo4e_value.is_object() && !bo4e_value.is_array() {
if let Some(disc_value) = dm
.def
.meta
.discriminator
.as_deref()
.and_then(|d| d.split_once('='))
.map(|(_, v)| v)
{
if let Some(inner) = bo4e_value.get(disc_value) {
let mut injected = inner.clone();
if let Some(qualifier_field) = find_qualifier_companion_field(
&tx_engine.definitions,
&dm.def.meta.entity,
) {
if let Some(obj) = injected.as_object_mut() {
obj.entry(qualifier_field).or_insert_with(|| {
serde_json::Value::String(disc_value.to_string())
});
}
}
unwrapped_value = Some(injected);
unwrapped_value.as_ref().unwrap()
} else {
bo4e_value
}
} else if is_map_keyed_object(bo4e_value) {
let map = bo4e_value.as_object().unwrap();
let arr: Vec<serde_json::Value> = map
.iter()
.map(|(key, val)| {
let mut item = val.clone();
if let Some(obj) = item.as_object_mut() {
if let Some(qualifier_field) = find_qualifier_companion_field(
&tx_engine.definitions,
&dm.def.meta.entity,
) {
let entry = obj
.entry(qualifier_field)
.or_insert(serde_json::Value::Null);
if entry.is_null() {
*entry = serde_json::Value::String(key.clone());
}
}
}
item
})
.collect();
unwrapped_value = Some(serde_json::Value::Array(arr));
unwrapped_value.as_ref().unwrap()
} else {
bo4e_value
}
} else {
bo4e_value
};
let items: Vec<&serde_json::Value> = if bo4e_value.is_array() {
bo4e_value.as_array().unwrap().iter().collect()
} else {
vec![bo4e_value]
};
for (item_idx, item) in items.iter().enumerate() {
let instance = tx_engine.map_reverse(item, dm.def);
if instance.segments.is_empty() && instance.child_groups.is_empty() {
continue;
}
if dm.relative.is_empty() {
root_segs.extend(instance.segments);
for child in instance.child_groups {
match child_groups
.iter_mut()
.find(|g| g.group_id == child.group_id)
{
Some(existing) => existing.repetitions.extend(child.repetitions),
None => child_groups.push(child),
}
}
} else {
let effective_relative = if dm.depth >= 2 {
let rel = if items.len() > 1 {
strip_all_rep_indices(&dm.relative)
} else {
dm.relative.clone()
};
let skip_nesting = dm
.def
.meta
.source_path
.as_ref()
.and_then(|sp| sp.rsplit_once('.'))
.and_then(|(parent_path, _)| source_path_to_rep.get(parent_path))
.is_some_and(|reps| reps.len() == 1);
let nesting_idx = if items.len() > 1 && !skip_nesting {
dm.def
.meta
.source_path
.as_ref()
.and_then(|sp| tx.nesting_info.get(sp))
.and_then(|dist| dist.get(item_idx))
.copied()
} else {
None
};
if let Some(parent_rep) = nesting_idx {
let parts: Vec<&str> = rel.split('.').collect();
let parent_id = parts[0].split(':').next().unwrap_or(parts[0]);
let rest = parts[1..].join(".");
format!("{}:{}.{}", parent_id, parent_rep, rest)
} else {
resolve_child_relative(
&rel,
dm.def.meta.source_path.as_deref(),
&source_path_to_rep,
item_idx,
)
}
} else if dm.depth == 1 {
let child_key = dm
.def
.meta
.source_path
.as_ref()
.map(|sp| format!("{sp}#child"));
if let Some(child_indices) =
child_key.as_ref().and_then(|ck| tx.nesting_info.get(ck))
{
if let Some(&target) = child_indices.get(item_idx) {
if target != usize::MAX {
let base =
dm.relative.split(':').next().unwrap_or(&dm.relative);
format!("{}:{}", base, target)
} else {
dm.relative.clone()
}
} else if items.len() > 1 && item_idx > 0 {
strip_rep_index(&dm.relative)
} else {
dm.relative.clone()
}
} else if items.len() > 1 && item_idx > 0 {
strip_rep_index(&dm.relative)
} else {
dm.relative.clone()
}
} else if items.len() > 1 && item_idx > 0 {
strip_rep_index(&dm.relative)
} else {
dm.relative.clone()
};
let rep_used =
place_in_groups(&mut child_groups, &effective_relative, instance);
if dm.depth == 1 {
if let Some(sp) = &dm.def.meta.source_path {
source_path_to_rep
.entry(sp.clone())
.or_default()
.push(rep_used);
}
}
}
}
}
sg4_reps.push(AssembledGroupInstance {
segments: root_segs,
child_groups,
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: Vec::new(),
skipped_positions: Vec::new(),
});
}
let mut root_segments = Vec::new();
let mut uns_segments = Vec::new();
let mut uns_is_summary = false;
let mut found_uns = false;
for seg in msg_tree.segments {
if seg.tag == "UNS" {
uns_is_summary = seg
.elements
.first()
.and_then(|el| el.first())
.map(|v| v == "S")
.unwrap_or(false);
uns_segments.push(seg);
found_uns = true;
} else if found_uns {
uns_segments.push(seg);
} else {
root_segments.push(seg);
}
}
let pre_group_count = root_segments.len();
let mut all_groups = msg_tree.groups;
let mut inter_group = msg_tree.inter_group_segments;
let sg_num = |id: &str| -> usize {
id.strip_prefix("SG")
.and_then(|n| n.parse::<usize>().ok())
.unwrap_or(0)
};
if !sg4_reps.is_empty() {
if uns_is_summary {
all_groups.push(AssembledGroup {
group_id: transaction_group.to_string(),
repetitions: sg4_reps,
});
if !uns_segments.is_empty() {
all_groups.sort_by_key(|g| sg_num(&g.group_id));
let tx_num = sg_num(transaction_group);
let uns_pos = all_groups
.iter()
.rposition(|g| sg_num(&g.group_id) <= tx_num)
.map(|i| i + 1)
.unwrap_or(all_groups.len());
inter_group.insert(uns_pos, uns_segments);
}
} else {
if !uns_segments.is_empty() {
inter_group.insert(all_groups.len(), uns_segments);
}
all_groups.push(AssembledGroup {
group_id: transaction_group.to_string(),
repetitions: sg4_reps,
});
}
} else if !uns_segments.is_empty() {
if transaction_group.is_empty() {
all_groups.sort_by_key(|g| sg_num(&g.group_id));
if uns_is_summary {
inter_group.insert(all_groups.len(), uns_segments);
} else {
inter_group.insert(0, uns_segments);
}
} else {
all_groups.sort_by_key(|g| sg_num(&g.group_id));
let tx_num = sg_num(transaction_group);
let uns_pos = all_groups
.iter()
.rposition(|g| sg_num(&g.group_id) <= tx_num)
.map(|i| i + 1)
.unwrap_or(all_groups.len());
inter_group.insert(uns_pos, uns_segments);
}
}
for (k, segs) in &mapped.inter_group_segments {
if segs.is_empty() {
continue;
}
let existing_tags: std::collections::HashSet<String> = inter_group
.get(k)
.map(|v| v.iter().map(|s| s.tag.clone()).collect())
.unwrap_or_default();
for seg in segs {
if existing_tags.contains(&seg.tag) {
continue;
}
inter_group.entry(*k).or_default().push(seg.clone());
}
}
let mut tree = AssembledTree {
segments: root_segments,
groups: all_groups,
post_group_start: pre_group_count,
inter_group_segments: inter_group,
};
if let Some(mig) = filtered_mig {
mig_assembly::repetition_order::sort_repetitions_by_mig_variant(
&mut tree,
mig,
(!transaction_group.is_empty()).then_some(transaction_group),
);
}
tree
}
pub fn build_group_from_bo4e(
&self,
bo4e_value: &serde_json::Value,
def: &MappingDefinition,
) -> AssembledGroup {
let instance = self.map_reverse(bo4e_value, def);
let leaf_group = def
.meta
.source_group
.rsplit('.')
.next()
.unwrap_or(&def.meta.source_group);
AssembledGroup {
group_id: leaf_group.to_string(),
repetitions: vec![instance],
}
}
pub fn map_interchange_typed<M, T>(
msg_engine: &MappingEngine,
tx_engine: &MappingEngine,
tree: &AssembledTree,
tx_group: &str,
enrich_codes: bool,
nachrichtendaten: crate::model::Nachrichtendaten,
interchangedaten: crate::model::Interchangedaten,
) -> Result<crate::model::Interchange<M, T>, serde_json::Error>
where
M: serde::de::DeserializeOwned,
T: serde::de::DeserializeOwned,
{
let mapped = Self::map_interchange(msg_engine, tx_engine, tree, tx_group, enrich_codes);
let nachricht = mapped.into_dynamic_nachricht(nachrichtendaten);
let dynamic = crate::model::DynamicInterchange {
interchangedaten,
nachrichten: vec![nachricht],
};
let value = serde_json::to_value(&dynamic)?;
serde_json::from_value(value)
}
pub fn map_interchange_reverse_typed<M, T>(
msg_engine: &MappingEngine,
tx_engine: &MappingEngine,
nachricht: &crate::model::Nachricht<M, T>,
tx_group: &str,
) -> Result<AssembledTree, serde_json::Error>
where
M: serde::Serialize,
T: serde::Serialize,
{
let mut stammdaten = serde_json::to_value(&nachricht.stammdaten)?;
crate::model::restore_message_metadata(&mut stammdaten, &nachricht.nachrichtendaten);
let transaktionen: Vec<crate::model::MappedTransaktion> = nachricht
.transaktionen
.iter()
.map(|t| {
Ok(crate::model::MappedTransaktion {
stammdaten: serde_json::to_value(t)?,
transaktionsdaten: serde_json::Value::Null,
nesting_info: Default::default(),
})
})
.collect::<Result<Vec<_>, serde_json::Error>>()?;
let mapped = crate::model::MappedMessage {
stammdaten,
nachricht_meta: serde_json::Value::Null,
transaktionen,
nesting_info: Default::default(),
inter_group_segments: Default::default(),
};
Ok(Self::map_interchange_reverse(
msg_engine, tx_engine, &mapped, tx_group, None,
))
}
}
fn parse_source_path_part(part: &str) -> (&str, Option<&str>) {
if let Some(pos) = part.find('_') {
let group = &part[..pos];
let qualifier = &part[pos + 1..];
if !qualifier.is_empty() {
return (group, Some(qualifier));
}
}
(part, None)
}
fn build_reverse_mig_group_order(mig: &MigSchema, tx_group_id: &str) -> HashMap<String, usize> {
let mut order = HashMap::new();
if let Some(tg) = mig.segment_groups.iter().find(|g| g.id == tx_group_id) {
for (i, nested) in tg.nested_groups.iter().enumerate() {
if let Some(ref vc) = nested.variant_code {
let variant_key = format!("{}_{}", nested.id, vc.to_uppercase());
order.insert(variant_key, i);
}
order.entry(nested.id.clone()).or_insert(i);
}
}
order
}
fn variant_mig_position(
def: &MappingDefinition,
base_group_id: &str,
mig_order: &HashMap<String, usize>,
) -> usize {
if let Some(ref sp) = def.meta.source_path {
let base_lower = base_group_id.to_lowercase();
for part in sp.split('.') {
if part.starts_with(&base_lower)
|| part.starts_with(base_group_id.to_lowercase().as_str())
{
if let Some(underscore_pos) = part.find('_') {
let qualifier = &part[underscore_pos + 1..];
let variant_key = format!("{}_{}", base_group_id, qualifier.to_uppercase());
if let Some(&pos) = mig_order.get(&variant_key) {
return pos;
}
}
}
}
}
mig_order.get(base_group_id).copied().unwrap_or(usize::MAX)
}
fn find_rep_by_entry_qualifier<'a>(
reps: &'a [AssembledGroupInstance],
qualifier: &str,
) -> Option<&'a AssembledGroupInstance> {
let parts: Vec<&str> = qualifier.split('_').collect();
reps.iter().find(|inst| {
inst.segments.first().is_some_and(|seg| {
seg.elements
.first()
.and_then(|e| e.first())
.is_some_and(|v| parts.iter().any(|part| v.eq_ignore_ascii_case(part)))
})
})
}
fn find_all_reps_by_entry_qualifier<'a>(
reps: &'a [AssembledGroupInstance],
qualifier: &str,
) -> Vec<&'a AssembledGroupInstance> {
let parts: Vec<&str> = qualifier.split('_').collect();
reps.iter()
.filter(|inst| {
inst.segments.first().is_some_and(|seg| {
seg.elements
.first()
.and_then(|e| e.first())
.is_some_and(|v| parts.iter().any(|part| v.eq_ignore_ascii_case(part)))
})
})
.collect()
}
fn has_source_path_qualifiers(source_path: &str) -> bool {
source_path.split('.').any(|part| {
if let Some(pos) = part.find('_') {
pos < part.len() - 1
} else {
false
}
})
}
fn parse_group_spec(part: &str) -> (&str, Option<usize>) {
if let Some(colon_pos) = part.find(':') {
let id = &part[..colon_pos];
let rep = part[colon_pos + 1..].parse::<usize>().ok();
(id, rep)
} else {
(part, None)
}
}
fn strip_tx_group_prefix(source_group: &str, tx_group: &str) -> String {
if source_group == tx_group || source_group.is_empty() {
String::new()
} else if let Some(rest) = source_group.strip_prefix(tx_group) {
rest.strip_prefix('.').unwrap_or(rest).to_string()
} else {
source_group.to_string()
}
}
fn place_in_groups(
groups: &mut Vec<AssembledGroup>,
relative_path: &str,
instance: AssembledGroupInstance,
) -> usize {
let parts: Vec<&str> = relative_path.split('.').collect();
if parts.len() == 1 {
let (id, rep) = parse_group_spec(parts[0]);
let group = if let Some(g) = groups.iter_mut().find(|g| g.group_id == id) {
g
} else {
groups.push(AssembledGroup {
group_id: id.to_string(),
repetitions: vec![],
});
groups.last_mut().unwrap()
};
if let Some(rep_idx) = rep {
while group.repetitions.len() <= rep_idx {
group.repetitions.push(AssembledGroupInstance {
segments: vec![],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: Vec::new(),
skipped_positions: Vec::new(),
});
}
group.repetitions[rep_idx]
.segments
.extend(instance.segments);
group.repetitions[rep_idx]
.child_groups
.extend(instance.child_groups);
rep_idx
} else {
let pos = group.repetitions.len();
group.repetitions.push(instance);
pos
}
} else {
let (parent_id, parent_rep) = parse_group_spec(parts[0]);
let rep_idx = parent_rep.unwrap_or(0);
let parent_group = if let Some(g) = groups.iter_mut().find(|g| g.group_id == parent_id) {
g
} else {
groups.push(AssembledGroup {
group_id: parent_id.to_string(),
repetitions: vec![],
});
groups.last_mut().unwrap()
};
while parent_group.repetitions.len() <= rep_idx {
parent_group.repetitions.push(AssembledGroupInstance {
segments: vec![],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: Vec::new(),
skipped_positions: Vec::new(),
});
}
let remaining = parts[1..].join(".");
place_in_groups(
&mut parent_group.repetitions[rep_idx].child_groups,
&remaining,
instance,
);
rep_idx
}
}
fn resolve_child_relative(
relative: &str,
source_path: Option<&str>,
source_path_to_rep: &std::collections::HashMap<String, Vec<usize>>,
item_idx: usize,
) -> String {
let parts: Vec<&str> = relative.split('.').collect();
if parts.is_empty() {
return relative.to_string();
}
let (parent_id, parent_rep) = parse_group_spec(parts[0]);
if parent_rep.is_some() {
return relative.to_string();
}
if let Some(sp) = source_path {
if let Some((parent_path, _child)) = sp.rsplit_once('.') {
if let Some(rep_indices) = source_path_to_rep.get(parent_path) {
let rep_idx = rep_indices
.get(item_idx)
.or_else(|| rep_indices.last())
.copied()
.unwrap_or(0);
let rest = parts[1..].join(".");
return format!("{}:{}.{}", parent_id, rep_idx, rest);
}
let prefix = format!("{}_", parent_path);
let mut unioned: Vec<usize> = source_path_to_rep
.iter()
.filter(|(k, _)| k.starts_with(&prefix))
.flat_map(|(_, v)| v.iter().copied())
.collect();
if !unioned.is_empty() {
unioned.sort_unstable();
unioned.dedup();
let rep_idx = unioned
.get(item_idx)
.or_else(|| unioned.last())
.copied()
.unwrap_or(0);
let rest = parts[1..].join(".");
return format!("{}:{}.{}", parent_id, rep_idx, rest);
}
}
}
relative.to_string()
}
struct DiscriminatorMatcher<'a> {
tag: &'a str,
element_idx: usize,
component_idx: usize,
expected_values: Vec<&'a str>,
occurrence: Option<usize>,
}
impl<'a> DiscriminatorMatcher<'a> {
fn parse(disc: &'a str) -> Option<Self> {
let (spec, expected) = disc.split_once('=')?;
let parts: Vec<&str> = spec.split('.').collect();
if parts.len() != 3 {
return None;
}
let (expected_raw, occurrence) = parse_discriminator_occurrence(expected);
Some(Self {
tag: parts[0],
element_idx: parts[1].parse().ok()?,
component_idx: parts[2].parse().ok()?,
expected_values: expected_raw.split('|').collect(),
occurrence,
})
}
fn matches(&self, instance: &AssembledGroupInstance) -> bool {
instance.segments.iter().any(|s| {
s.tag.eq_ignore_ascii_case(self.tag)
&& s.elements
.get(self.element_idx)
.and_then(|e| e.get(self.component_idx))
.map(|v| self.expected_values.iter().any(|ev| v == ev))
.unwrap_or(false)
})
}
fn filter_instances<'b>(
&self,
instances: Vec<&'b AssembledGroupInstance>,
) -> Vec<&'b AssembledGroupInstance> {
let matching: Vec<_> = instances
.into_iter()
.filter(|inst| self.matches(inst))
.collect();
if let Some(occ) = self.occurrence {
matching.into_iter().nth(occ).into_iter().collect()
} else {
matching
}
}
}
fn parse_discriminator_occurrence(expected: &str) -> (&str, Option<usize>) {
if let Some(hash_pos) = expected.rfind('#') {
if let Ok(occ) = expected[hash_pos + 1..].parse::<usize>() {
return (&expected[..hash_pos], Some(occ));
}
}
(expected, None)
}
fn strip_rep_index(relative: &str) -> String {
let (id, _) = parse_group_spec(relative);
id.to_string()
}
pub(crate) fn strip_all_rep_indices(relative: &str) -> String {
relative
.split('.')
.map(|part| {
let (id, _) = parse_group_spec(part);
id
})
.collect::<Vec<_>>()
.join(".")
}
pub fn is_nested_child_of(child: &MappingDefinition, parent: &MappingDefinition) -> bool {
if child.meta.parent_field.is_none() || child.meta.entity != parent.meta.entity {
return false;
}
let child_sg = strip_all_rep_indices(&child.meta.source_group);
let parent_sg = strip_all_rep_indices(&parent.meta.source_group);
match child_sg.rsplit_once('.') {
Some((head, _)) if head.eq_ignore_ascii_case(&parent_sg) => {}
_ => return false,
}
let (Some(child_sp), Some(parent_sp)) = (
child.meta.source_path.as_deref(),
parent.meta.source_path.as_deref(),
) else {
return true;
};
let Some((child_parent_sp, _)) = child_sp.rsplit_once('.') else {
return false;
};
let child_parts: Vec<&str> = child_parent_sp.split('.').collect();
let parent_parts: Vec<&str> = parent_sp.split('.').collect();
child_parts.len() == parent_parts.len()
&& child_parts.iter().zip(&parent_parts).all(|(c, p)| {
let (c_id, c_q) = parse_source_path_part(c);
let (p_id, p_q) = parse_source_path_part(p);
c_id.eq_ignore_ascii_case(p_id)
&& match (c_q, p_q) {
(Some(cq), Some(pq)) => cq.eq_ignore_ascii_case(pq),
_ => true,
}
})
}
pub fn is_bound_child_of(child: &MappingDefinition, parent: &MappingDefinition) -> bool {
if std::ptr::eq(child, parent)
|| child.meta.parent_field.is_some()
|| parent.meta.parent_field.is_some()
|| child.meta.target_list.is_some()
|| parent.meta.target_list.is_some()
|| child.meta.entity != parent.meta.entity
{
return false;
}
let (Some(child_sp), Some(parent_sp)) = (
child.meta.source_path.as_deref(),
parent.meta.source_path.as_deref(),
) else {
return false;
};
parent_sp.contains('.')
&& child_sp
.rsplit_once('.')
.is_some_and(|(head, _)| head.eq_ignore_ascii_case(parent_sp))
}
pub fn is_bound_child(definitions: &[MappingDefinition], def: &MappingDefinition) -> bool {
definitions.iter().any(|p| is_bound_child_of(def, p))
}
fn nested_parent_qualifier(child: &MappingDefinition) -> Option<&str> {
let (parent_path, _) = child.meta.source_path.as_deref()?.rsplit_once('.')?;
let last = parent_path.rsplit('.').next()?;
parse_source_path_part(last).1
}
fn nested_child_leaf(child: &MappingDefinition) -> (String, Option<&str>) {
let leaf_group = strip_all_rep_indices(
child
.meta
.source_group
.rsplit('.')
.next()
.unwrap_or(&child.meta.source_group),
);
let leaf_qualifier = child
.meta
.source_path
.as_deref()
.and_then(|sp| sp.rsplit('.').next())
.and_then(|part| parse_source_path_part(part).1);
(leaf_group, leaf_qualifier)
}
fn bound_child_reps<'i>(
instance: &'i AssembledGroupInstance,
child: &MappingDefinition,
) -> Vec<&'i AssembledGroupInstance> {
let (leaf_id, leaf_qualifier) = nested_child_leaf(child);
let Some(group) = instance
.child_groups
.iter()
.find(|g| g.group_id.eq_ignore_ascii_case(&leaf_id))
else {
return Vec::new();
};
let reps: Vec<&AssembledGroupInstance> = match leaf_qualifier {
Some(q) => find_all_reps_by_entry_qualifier(&group.repetitions, q),
None => group.repetitions.iter().collect(),
};
match child
.meta
.discriminator
.as_deref()
.and_then(DiscriminatorMatcher::parse)
{
Some(matcher) => matcher.filter_instances(reps),
None => reps,
}
}
fn push_child_reps(
instance: &mut AssembledGroupInstance,
leaf_id: String,
reps: Vec<AssembledGroupInstance>,
) {
match instance
.child_groups
.iter_mut()
.find(|g| g.group_id.eq_ignore_ascii_case(&leaf_id))
{
Some(group) => group.repetitions.extend(reps),
None => instance.child_groups.push(AssembledGroup {
group_id: leaf_id,
repetitions: reps,
}),
}
}
fn entry_qualifier_matches(instance: &AssembledGroupInstance, qualifier: &str) -> bool {
segment_qualifier_matches(instance.segments.first(), qualifier)
}
fn rebuilt_entry_qualifier_matches(
instance: &AssembledGroupInstance,
def: &MappingDefinition,
qualifier: &str,
) -> bool {
let entry_tag = def
.meta
.discriminator
.as_deref()
.and_then(|d| d.split('.').next())
.filter(|tag| !tag.is_empty());
let entry = match entry_tag {
Some(tag) => instance
.segments
.iter()
.find(|s| s.tag.eq_ignore_ascii_case(tag)),
None => instance.segments.first(),
};
segment_qualifier_matches(entry, qualifier)
}
fn segment_qualifier_matches(segment: Option<&AssembledSegment>, qualifier: &str) -> bool {
segment
.and_then(|seg| seg.elements.first())
.and_then(|e| e.first())
.is_some_and(|v| qualifier.split('_').any(|q| v.eq_ignore_ascii_case(q)))
}
fn field_is_filled(bo4e_value: &serde_json::Value, field: &str) -> bool {
let mut current = bo4e_value;
for part in field.split('.') {
match current.get(part) {
Some(v) => current = v,
None => return false,
}
}
match current {
serde_json::Value::String(s) => !s.is_empty(),
serde_json::Value::Array(a) => !a.is_empty(),
serde_json::Value::Object(o) => !o.is_empty(),
serde_json::Value::Number(_) | serde_json::Value::Bool(_) => true,
serde_json::Value::Null => false,
}
}
pub(crate) fn list_target(target: &str) -> Option<(&str, &str)> {
let (list, sub) = target.split_once("[].")?;
(!list.is_empty() && !sub.is_empty()).then_some((list, sub))
}
pub(crate) fn parse_tag_qualifier(tag_part: &str) -> (String, Option<&str>, usize) {
if let Some(bracket_start) = tag_part.find('[') {
let tag = tag_part[..bracket_start].to_uppercase();
let inner = tag_part[bracket_start + 1..].trim_end_matches(']');
if let Some(comma_pos) = inner.find(',') {
let qualifier = &inner[..comma_pos];
let index = inner[comma_pos + 1..].parse::<usize>().unwrap_or(0);
if qualifier == "*" {
(tag, None, index)
} else {
(tag, Some(qualifier), index)
}
} else {
(tag, Some(inner), 0)
}
} else {
(tag_part.to_uppercase(), None, 0)
}
}
pub fn deep_merge_insert(
result: &mut serde_json::Map<String, serde_json::Value>,
entity: &str,
bo4e: serde_json::Value,
) {
merge_entity(result, entity, bo4e, false);
}
fn merge_entity(
result: &mut serde_json::Map<String, serde_json::Value>,
entity: &str,
bo4e: serde_json::Value,
keep_both: bool,
) {
if let Some(existing) = result.get_mut(entity) {
if let (Some(existing_arr), Some(new_arr)) =
(existing.as_array().map(|a| a.len()), bo4e.as_array())
{
if existing_arr == new_arr.len() {
let existing_arr = existing.as_array_mut().unwrap();
for (existing_elem, new_elem) in existing_arr.iter_mut().zip(new_arr) {
if let (Some(existing_map), Some(new_map)) =
(existing_elem.as_object_mut(), new_elem.as_object())
{
for (k, v) in new_map {
if let Some(existing_v) = existing_map.get_mut(k) {
if let (Some(existing_inner), Some(new_inner)) =
(existing_v.as_object_mut(), v.as_object())
{
for (ik, iv) in new_inner {
existing_inner
.entry(ik.clone())
.or_insert_with(|| iv.clone());
}
}
} else {
existing_map.insert(k.clone(), v.clone());
}
}
}
}
return;
}
}
if let (Some(existing_map), serde_json::Value::Object(new_map)) =
(existing.as_object_mut(), &bo4e)
{
for (k, v) in new_map {
if let Some(existing_v) = existing_map.get_mut(k) {
if let (Some(existing_inner), Some(new_inner)) =
(existing_v.as_object_mut(), v.as_object())
{
for (ik, iv) in new_inner {
existing_inner
.entry(ik.clone())
.or_insert_with(|| iv.clone());
}
}
} else {
existing_map.insert(k.clone(), v.clone());
}
}
return;
}
if !keep_both {
result.insert(entity.to_string(), bo4e);
return;
}
let existing_items = match std::mem::take(existing) {
serde_json::Value::Array(items) => items,
other => vec![other],
};
let new_items = match bo4e {
serde_json::Value::Array(items) => items,
other => vec![other],
};
*existing = serde_json::Value::Array(existing_items.into_iter().chain(new_items).collect());
return;
}
result.insert(entity.to_string(), bo4e);
}
fn append_to_list_field(
result: &mut serde_json::Map<String, serde_json::Value>,
entity: &str,
list_field: &str,
bo4e: serde_json::Value,
) {
let mut items = match bo4e {
serde_json::Value::Array(a) => a,
other => vec![other],
};
items.retain(|v| !v.as_object().is_some_and(|o| o.is_empty()));
if items.is_empty() {
return;
}
let entry = result
.entry(entity.to_string())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
let Some(obj) = entry.as_object_mut() else {
return;
};
match obj.get_mut(list_field).and_then(|v| v.as_array_mut()) {
Some(existing) => existing.extend(items),
None => {
obj.insert(list_field.to_string(), serde_json::Value::Array(items));
}
}
}
fn is_map_keyed_object(value: &serde_json::Value) -> bool {
let Some(obj) = value.as_object() else {
return false;
};
if obj.is_empty() {
return false;
}
obj.iter().all(|(k, v)| {
k.len() <= 5
&& k.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
&& v.is_object()
})
}
fn find_qualifier_companion_field(
definitions: &[crate::definition::MappingDefinition],
entity: &str,
) -> Option<String> {
for def in definitions {
if def.meta.entity != *entity || def.meta.parent_field.is_some() {
continue;
}
let disc = def.meta.discriminator.as_deref()?;
let (disc_path, _) = disc.split_once('=')?;
let disc_path_lower = disc_path.to_lowercase();
for (path, mapping) in &def.fields {
let cf_path = path.to_lowercase();
let matches = cf_path == disc_path_lower || format!("{}.0", cf_path) == disc_path_lower;
if matches {
let target = match mapping {
FieldMapping::Simple(t) => t.as_str(),
FieldMapping::Structured(s) => s.target.as_str(),
FieldMapping::Nested(_) => continue,
};
if !target.is_empty() {
return Some(target.to_string());
}
}
}
}
None
}
fn extract_child_from_parent(
entities: &serde_json::Value,
definitions: &[MappingDefinition],
child_def: &MappingDefinition,
) -> Option<serde_json::Value> {
extract_child_from_parent_with_indices(entities, definitions, child_def).map(|(v, _)| v)
}
fn extract_child_from_parent_with_indices(
entities: &serde_json::Value,
definitions: &[MappingDefinition],
child_def: &MappingDefinition,
) -> Option<(serde_json::Value, Vec<usize>)> {
let parts: Vec<&str> = child_def.meta.source_group.split('.').collect();
if parts.len() < 2 {
return None;
}
let parent_group = parts[0];
let parent_def = definitions
.iter()
.find(|d| d.meta.source_group == parent_group && d.meta.entity != child_def.meta.entity)?;
let parent_key = to_camel_case(&parent_def.meta.entity);
let child_key = to_camel_case(&child_def.meta.entity);
let parent_value = entities.get(&parent_key)?;
if let Some(parent_map) = parent_value.as_object() {
if is_map_keyed_value(parent_map) {
let mut children: Vec<serde_json::Value> = Vec::new();
let mut indices: Vec<usize> = Vec::new();
for (i, (_key, inner)) in parent_map.iter().enumerate() {
if let Some(child) = inner.get(&child_key) {
if !child.is_null() {
children.push(child.clone());
indices.push(i);
}
}
}
return match children.len() {
0 => None,
1 => Some((children.into_iter().next().unwrap(), indices)),
_ => Some((serde_json::Value::Array(children), indices)),
};
}
}
if let Some(parent_arr) = parent_value.as_array() {
let mut children: Vec<serde_json::Value> = Vec::new();
let mut indices: Vec<usize> = Vec::new();
for (i, item) in parent_arr.iter().enumerate() {
if let Some(child) = item.get(&child_key) {
if !child.is_null() {
children.push(child.clone());
indices.push(i);
}
}
}
return match children.len() {
0 => None,
1 => Some((children.into_iter().next().unwrap(), indices)),
_ => Some((serde_json::Value::Array(children), indices)),
};
}
let child = parent_value.get(&child_key)?;
if child.is_null() {
return None;
}
Some((child.clone(), vec![0]))
}
fn nest_child_entities_in_result(
result: &mut serde_json::Map<String, serde_json::Value>,
definitions: &[MappingDefinition],
nesting_info: &std::collections::HashMap<String, Vec<usize>>,
transaction_group: Option<&str>,
) {
let nesting_pairs = child_entity_nesting_pairs(definitions, transaction_group);
for (_parent_group, parent_entity, child_entity, child_source_path) in nesting_pairs {
let parent_key = to_camel_case(&parent_entity);
let child_key = to_camel_case(&child_entity);
let child_value = match result.remove(&child_key) {
Some(v) => v,
None => continue,
};
let Some(parent_value) = result.get_mut(&parent_key) else {
result.insert(child_key, child_value);
continue;
};
if parent_value.is_array() {
result.insert(child_key, child_value);
continue;
}
let distribution = child_source_path
.as_deref()
.and_then(|sp| nesting_info.get(sp));
let child_items: Vec<(usize, &serde_json::Value)> = match &child_value {
serde_json::Value::Array(arr) => arr.iter().enumerate().collect(),
other => vec![(0, other)],
};
let insert_or_append = |obj: &mut serde_json::Map<String, serde_json::Value>,
key: &str,
val: &serde_json::Value| {
match obj.get_mut(key) {
Some(existing) => {
if !existing.is_array() {
let prev = existing.take();
*existing = serde_json::Value::Array(vec![prev]);
}
if let Some(arr) = existing.as_array_mut() {
arr.push(val.clone());
}
}
None => {
obj.insert(key.to_string(), val.clone());
}
}
};
if let Some(parent_map) = parent_value.as_object_mut() {
if is_map_keyed_value(parent_map) {
let keys: Vec<String> = parent_map.keys().cloned().collect();
for (i, child_item) in &child_items {
let target_idx = distribution
.and_then(|dist| dist.get(*i))
.copied()
.unwrap_or(0);
if let Some(key) = keys.get(target_idx) {
if let Some(inner) = parent_map.get_mut(key).and_then(|v| v.as_object_mut())
{
insert_or_append(inner, &child_key, child_item);
}
}
}
continue;
}
}
if let Some(parent_arr) = parent_value.as_array_mut() {
for (i, child_item) in &child_items {
let target_idx = distribution
.and_then(|dist| dist.get(*i))
.copied()
.unwrap_or(0);
if let Some(parent_obj) = parent_arr
.get_mut(target_idx)
.and_then(|v| v.as_object_mut())
{
insert_or_append(parent_obj, &child_key, child_item);
}
}
continue;
}
if let Some(parent_obj) = parent_value.as_object_mut() {
for (_i, child_item) in &child_items {
insert_or_append(parent_obj, &child_key, child_item);
}
continue;
}
result.insert(child_key, child_value);
}
}
pub(crate) fn child_entity_nesting_pairs(
definitions: &[MappingDefinition],
transaction_group: Option<&str>,
) -> Vec<(String, String, String, Option<String>)> {
let mut nesting_pairs: Vec<(String, String, String, Option<String>)> = Vec::new();
for def in definitions {
let parts: Vec<&str> = def.meta.source_group.split('.').collect();
if parts.len() < 2 || def.meta.parent_field.is_some() {
continue;
}
let parent_group = parts[0];
if transaction_group.is_some_and(|tx| tx == parent_group) {
continue;
}
let child_entity = def.meta.entity.clone();
let child_has_parent_level_def = definitions
.iter()
.any(|d| d.meta.source_group == parent_group && d.meta.entity == child_entity);
if child_has_parent_level_def {
continue;
}
let parent_entity = definitions
.iter()
.find(|d| d.meta.source_group == parent_group && d.meta.entity != child_entity)
.map(|d| d.meta.entity.clone());
if let Some(ref parent_entity) = parent_entity {
let child_key_lc = to_camel_case(&child_entity);
let parent_defs: Vec<_> = definitions
.iter()
.filter(|d| d.meta.entity == *parent_entity)
.collect();
let has_conflicting_field = parent_defs.iter().any(|pd| {
pd.fields.values().any(|fm| {
let target = match fm {
crate::definition::FieldMapping::Simple(t) => t.as_str(),
crate::definition::FieldMapping::Structured(s) => s.target.as_str(),
crate::definition::FieldMapping::Nested(_) => "",
};
target.starts_with(&child_key_lc)
&& target.get(child_key_lc.len()..child_key_lc.len() + 1) == Some(".")
})
});
if has_conflicting_field {
continue;
}
if nesting_pairs
.iter()
.any(|(_, pe, ce, _)| *pe == *parent_entity && *ce == child_entity)
{
continue;
}
nesting_pairs.push((
parent_group.to_string(),
parent_entity.clone(),
child_entity,
def.meta.source_path.clone(),
));
}
}
nesting_pairs
}
fn is_map_keyed_value(map: &serde_json::Map<String, serde_json::Value>) -> bool {
if map.is_empty() {
return false;
}
map.values().all(|v| v.is_object())
&& map.keys().all(|k| {
k.len() <= 5
|| k.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
})
}
#[derive(Clone)]
struct CodeSite<'a> {
target: &'a str,
parent_field: Option<&'a str>,
source_path: &'a str,
seg_tag: String,
path_qualifier: Option<String>,
disc_qualifier: Option<String>,
element_idx: usize,
component_idx: usize,
enum_map: Option<&'a std::collections::BTreeMap<String, String>>,
also_target: Option<&'a str>,
also_enum_map: Option<&'a std::collections::BTreeMap<String, String>>,
}
pub(crate) fn to_camel_case(name: &str) -> String {
let mut chars = name.chars();
match chars.next() {
Some(c) => c.to_lowercase().to_string() + chars.as_str(),
None => String::new(),
}
}
fn set_nested_value(map: &mut serde_json::Map<String, serde_json::Value>, path: &str, val: String) {
set_nested_value_json(map, path, serde_json::Value::String(val));
}
fn set_nested_value_json(
map: &mut serde_json::Map<String, serde_json::Value>,
path: &str,
val: serde_json::Value,
) {
if let Some((prefix, leaf)) = path.rsplit_once('.') {
let mut current = map;
for part in prefix.split('.') {
let entry = current
.entry(part.to_string())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
current = entry.as_object_mut().expect("expected object in path");
}
current.insert(leaf.to_string(), val);
} else {
map.insert(path.to_string(), val);
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct VariantCache {
pub message_defs: Vec<MappingDefinition>,
pub transaction_defs: BTreeMap<String, Vec<MappingDefinition>>,
pub combined_defs: BTreeMap<String, Vec<MappingDefinition>>,
#[serde(default)]
pub code_lookups: BTreeMap<String, crate::code_lookup::CodeLookup>,
#[serde(default)]
pub mig_schema: Option<mig_types::schema::mig::MigSchema>,
#[serde(default)]
pub segment_structure: Option<crate::segment_structure::SegmentStructure>,
#[serde(skip)]
pub code_lists: std::sync::Arc<crate::code_lists::CodeLists>,
#[serde(default)]
pub pid_segment_numbers: BTreeMap<String, Vec<String>>,
#[serde(default)]
pub pid_requirements: BTreeMap<String, crate::pid_requirements::PidRequirements>,
#[serde(default)]
pub pid_ahb_workflows: BTreeMap<String, ahb_types::AhbWorkflow>,
#[serde(default)]
pub tx_groups: BTreeMap<String, String>,
}
impl VariantCache {
pub fn save(&self, path: &Path) -> Result<(), MappingError> {
let encoded = serde_json::to_vec(self).map_err(|e| MappingError::CacheWrite {
path: path.display().to_string(),
message: e.to_string(),
})?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, encoded)?;
Ok(())
}
pub fn load(path: &Path) -> Result<Self, MappingError> {
let bytes = std::fs::read(path)?;
let mut cache: Self =
serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
path: path.display().to_string(),
message: e.to_string(),
})?;
cache.code_lists = crate::code_lists::CodeLists::discover(path);
Ok(cache)
}
pub fn tx_group(&self, pid: &str) -> Option<&str> {
self.tx_groups
.get(&format!("pid_{pid}"))
.map(|s| s.as_str())
}
pub fn msg_engine(&self, pid: &str) -> MappingEngine {
let mut eng = MappingEngine::from_definitions_with_code_lists(
std::sync::Arc::clone(&self.code_lists),
self.message_defs.clone(),
)
.with_pid(pid);
if let Some(cl) = self.code_lookups.get(&format!("pid_{pid}")) {
eng = eng.with_code_lookup(cl.clone());
}
eng
}
pub fn tx_engine(&self, pid: &str) -> Option<MappingEngine> {
self.transaction_defs
.get(&format!("pid_{pid}"))
.map(|defs| {
let mut eng = MappingEngine::from_definitions_with_code_lists(
std::sync::Arc::clone(&self.code_lists),
defs.clone(),
)
.with_pid(pid);
if let Some(cl) = self.code_lookups.get(&format!("pid_{pid}")) {
eng = eng.with_code_lookup(cl.clone());
}
eng
})
}
pub fn filtered_mig(&self, pid: &str) -> Option<mig_types::schema::mig::MigSchema> {
let mig = self.mig_schema.as_ref()?;
let numbers = self
.pid_segment_numbers
.get(&format!("pid_{pid}"))
.or_else(|| self.pid_segment_numbers.get("pid_"))?;
let number_set: std::collections::HashSet<String> = numbers.iter().cloned().collect();
Some(mig_assembly::pid_filter::filter_mig_for_pid(
mig,
&number_set,
))
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct DataBundle {
pub format_version: String,
pub bundle_version: u32,
pub variants: BTreeMap<String, VariantCache>,
#[serde(default)]
pub bo4e_catalog: crate::bo4e_catalog::Bo4eCatalog,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub built_by: Option<String>,
#[serde(default)]
pub code_lists: crate::code_lists::CodeLists,
}
impl DataBundle {
pub const CURRENT_VERSION: u32 = 2;
pub const PRODUCING_VERSION: &'static str = env!("CARGO_PKG_VERSION");
pub fn variant(&self, name: &str) -> Option<&VariantCache> {
self.variants.get(name)
}
pub fn write_to<W: std::io::Write>(&self, writer: &mut W) -> Result<(), MappingError> {
let encoded = serde_json::to_vec(self).map_err(|e| MappingError::CacheWrite {
path: "<stream>".to_string(),
message: e.to_string(),
})?;
writer.write_all(&encoded).map_err(MappingError::Io)
}
pub fn read_from<R: std::io::Read>(reader: &mut R) -> Result<Self, MappingError> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).map_err(MappingError::Io)?;
serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
path: "<stream>".to_string(),
message: e.to_string(),
})
}
pub fn read_from_checked<R: std::io::Read>(reader: &mut R) -> Result<Self, MappingError> {
let mut bundle = Self::read_from(reader)?;
let shared = std::sync::Arc::new(std::mem::take(&mut bundle.code_lists));
for variant in bundle.variants.values_mut() {
variant.code_lists = std::sync::Arc::clone(&shared);
}
bundle.code_lists = (*shared).clone();
if bundle.bundle_version != Self::CURRENT_VERSION {
return Err(MappingError::CacheRead {
path: "<stream>".to_string(),
message: format!(
"Incompatible bundle version {}, expected version {}. \
Run `edifact-data update` to fetch compatible bundles.",
bundle.bundle_version,
Self::CURRENT_VERSION
),
});
}
Ok(bundle)
}
pub fn save(&self, path: &Path) -> Result<(), MappingError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut file = std::fs::File::create(path).map_err(MappingError::Io)?;
self.write_to(&mut file)
}
pub fn load(path: &Path) -> Result<Self, MappingError> {
let mut file = std::fs::File::open(path).map_err(MappingError::Io)?;
Self::read_from_checked(&mut file)
}
}
#[cfg(test)]
mod variant_cache_helper_tests {
use super::*;
fn make_test_cache() -> VariantCache {
let mut tx_groups = BTreeMap::new();
tx_groups.insert("pid_55001".to_string(), "SG4".to_string());
tx_groups.insert("pid_21007".to_string(), "SG14".to_string());
let mut transaction_defs = BTreeMap::new();
transaction_defs.insert("pid_55001".to_string(), vec![]);
transaction_defs.insert("pid_21007".to_string(), vec![]);
VariantCache {
code_lists: Default::default(),
message_defs: vec![],
transaction_defs,
combined_defs: BTreeMap::new(),
code_lookups: BTreeMap::new(),
mig_schema: None,
segment_structure: None,
pid_segment_numbers: BTreeMap::new(),
pid_requirements: BTreeMap::new(),
pid_ahb_workflows: BTreeMap::new(),
tx_groups,
}
}
#[test]
fn test_tx_group_returns_correct_group() {
let vc = make_test_cache();
assert_eq!(vc.tx_group("55001").unwrap(), "SG4");
assert_eq!(vc.tx_group("21007").unwrap(), "SG14");
}
#[test]
fn test_tx_group_unknown_pid_returns_none() {
let vc = make_test_cache();
assert!(vc.tx_group("99999").is_none());
}
#[test]
fn test_msg_engine_returns_engine() {
let vc = make_test_cache();
let engine = vc.msg_engine("55001");
assert_eq!(engine.definitions().len(), 0);
}
#[test]
fn test_tx_engine_returns_engine_for_known_pid() {
let vc = make_test_cache();
assert!(vc.tx_engine("55001").is_some());
}
#[test]
fn test_tx_engine_returns_none_for_unknown_pid() {
let vc = make_test_cache();
assert!(vc.tx_engine("99999").is_none());
}
fn make_populated_cache() -> VariantCache {
let pids: Vec<String> = (0..40).map(|i| format!("pid_{}", 55000 + i * 7)).collect();
let schema: serde_json::Value = serde_json::from_str(include_str!(
"../../mig-types/src/generated/fv2504/utilmd/pids/pid_55001_schema.json"
))
.unwrap();
let code_lookup = crate::code_lookup::CodeLookup::from_schema_value(&schema);
let element_counts: serde_json::Map<String, serde_json::Value> = (0..40)
.map(|i| (format!("T{i:02}"), serde_json::json!(i)))
.collect();
let segment_structure: SegmentStructure =
serde_json::from_value(serde_json::json!({ "element_counts": element_counts }))
.unwrap();
let ubs: serde_json::Map<String, serde_json::Value> = (0..40)
.map(|i| (format!("UB{i}"), serde_json::json!({ "Ref": i })))
.collect();
let workflow: ahb_types::AhbWorkflow = serde_json::from_value(serde_json::json!({
"pruefidentifikator": "55001",
"description": "",
"communication_direction": null,
"fields": [],
"ub_definitions": ubs,
}))
.unwrap();
let mut vc = make_test_cache();
vc.segment_structure = Some(segment_structure);
for pid in &pids {
vc.transaction_defs.insert(pid.clone(), vec![]);
vc.combined_defs.insert(pid.clone(), vec![]);
vc.code_lookups.insert(pid.clone(), code_lookup.clone());
vc.pid_segment_numbers
.insert(pid.clone(), vec!["00001".to_string()]);
vc.pid_ahb_workflows.insert(pid.clone(), workflow.clone());
vc.tx_groups.insert(pid.clone(), "SG4".to_string());
}
vc
}
#[test]
fn test_enrichment_uses_codes_of_the_path_qualifier_variant() {
let comp = |sub: u64, id: &str, codes: Option<serde_json::Value>| match codes {
Some(c) => serde_json::json!({"sub_index": sub, "id": id, "type": "code", "codes": c}),
None => serde_json::json!({"sub_index": sub, "id": id, "type": "data"}),
};
let code = |v: &str, n: &str| serde_json::json!([{"value": v, "name": n}]);
let seg = |tag: &str, composite: &str, comps: Vec<serde_json::Value>| serde_json::json!({"id": tag, "elements": [{"index": 0, "composite": composite, "components": comps}]});
let schema = serde_json::json!({"fields": {"sg15": {"segments": [
seg("RFF", "C506", vec![comp(0, "1153", Some(code("Z13", "PID"))), comp(1, "1154", Some(code("21037", "RD / NB-Bewertung")))]),
seg("RFF", "C506", vec![comp(0, "1153", Some(code("ACW", "Referenz"))), comp(1, "1154", None)]),
seg("CAV", "C889", vec![comp(0, "7111", Some(code("Z91", "Z91"))), comp(1, "7110", Some(code("A", "Alpha")))]),
seg("CAV", "C889", vec![comp(0, "7111", Some(code("ZF0", "ZF0"))), comp(1, "7110", Some(code("C", "Gamma")))]),
]}}});
let engine = MappingEngine::new_empty()
.with_code_lookup(crate::code_lookup::CodeLookup::from_schema_value(&schema));
let def = MappingDefinition::from_toml_str(
r#"
[meta]
entity = "Status"
bo4e_type = "Status"
source_group = "SG15"
source_path = "sg15"
discriminator = "RFF.0.0=Z13"
[fields]
"rff.0.1" = "pruefidentifikator"
"rff[ACW].0.1" = "referenz"
"cav[Z91].0.1" = "z91Wert"
"cav[ZF0].0.1" = "zf0Wert"
"#,
)
.unwrap();
let segment = |tag: &str, elements: &[&[&str]]| OwnedSegment {
id: tag.to_string(),
elements: elements
.iter()
.map(|e| e.iter().map(|c| c.to_string()).collect())
.collect(),
segment_number: 1,
};
let json = engine.map_forward_from_segments(
&[
segment("RFF", &[&["Z13", "21037"]]),
segment("RFF", &[&["ACW", "REF-1"]]),
segment("CAV", &[&["Z91", "C"]]),
segment("CAV", &[&["ZF0", "C"]]),
],
&def,
);
assert_eq!(
json["referenz"],
serde_json::json!("REF-1"),
"RFF+ACW d1154 is data; RFF+Z13's codes must not apply: {json}"
);
assert_eq!(json["pruefidentifikator"]["meaning"], "RD / NB-Bewertung");
assert_eq!(
json["z91Wert"]["meaning"],
serde_json::Value::Null,
"'C' is a CAV+ZF0 code, unknown to CAV+Z91: {json}"
);
assert_eq!(json["zf0Wert"]["meaning"], "Gamma");
}
#[test]
fn list_target_reads_and_writes_every_repetition_in_order() {
let engine = MappingEngine::new_empty();
let def = MappingDefinition::from_toml_str(
r#"
[meta]
entity = "Zuordnung"
bo4e_type = "Zuordnung"
source_group = "SG10"
source_path = "sg10"
[fields]
"cci.2.0" = "merkmal.code"
"cav[*,*].0.0" = "werte[].code"
"cav[*,*].0.3" = "werte[].text"
"cav[Z30,*].0.3" = "geraetenummern[].nummer"
"#,
)
.unwrap();
let segment = |tag: &str, elements: &[&[&str]]| OwnedSegment {
id: tag.to_string(),
elements: elements
.iter()
.map(|e| e.iter().map(|c| c.to_string()).collect())
.collect(),
segment_number: 1,
};
let json = engine.map_forward_from_segments(
&[
segment("CCI", &[&[""], &[""], &["ZB3"]]),
segment("CAV", &[&["Z90", "", "", "UENB"]]),
segment("CAV", &[&["Z91", "", "", "MSB"]]),
segment("CAV", &[&["Z30", "", "", "W1"]]),
segment("CAV", &[&["Z30", "", "", "W2"]]),
],
&def,
);
assert_eq!(
json["werte"],
serde_json::json!([
{"code": "Z90", "text": "UENB"},
{"code": "Z91", "text": "MSB"},
{"code": "Z30", "text": "W1"},
{"code": "Z30", "text": "W2"},
]),
"{json}"
);
assert_eq!(
json["geraetenummern"],
serde_json::json!([{"nummer": "W1"}, {"nummer": "W2"}])
);
let only_werte = serde_json::json!({
"merkmal": {"code": "ZB3"},
"werte": [{"text": "UENB", "code": "Z90"}, {"code": "Z91", "text": "MSB"}],
});
let instance = engine.map_reverse(&only_werte, &def);
let cavs: Vec<Vec<String>> = instance
.segments
.iter()
.filter(|s| s.tag == "CAV")
.map(|s| s.elements[0].clone())
.collect();
assert_eq!(
cavs,
vec![
vec![
"Z90".to_string(),
String::new(),
String::new(),
"UENB".to_string()
],
vec![
"Z91".to_string(),
String::new(),
String::new(),
"MSB".to_string()
],
]
);
}
#[test]
fn test_variant_cache_serialization_is_deterministic() {
let reference = serde_json::to_vec(&make_populated_cache()).unwrap();
for _ in 0..5 {
let again = serde_json::to_vec(&make_populated_cache()).unwrap();
assert!(
reference == again,
"VariantCache serialization must not depend on HashMap iteration order"
);
}
}
#[test]
fn test_variant_cache_serializes_map_keys_sorted() {
use indexmap::IndexMap;
use serde::de::IgnoredAny;
#[derive(serde::Deserialize)]
struct ProbeWorkflow {
ub_definitions: IndexMap<String, IgnoredAny>,
}
#[derive(serde::Deserialize)]
struct ProbeStructure {
element_counts: IndexMap<String, usize>,
}
#[derive(serde::Deserialize)]
struct Probe {
transaction_defs: IndexMap<String, IgnoredAny>,
combined_defs: IndexMap<String, IgnoredAny>,
code_lookups: IndexMap<String, IndexMap<String, IgnoredAny>>,
segment_structure: ProbeStructure,
pid_segment_numbers: IndexMap<String, IgnoredAny>,
pid_requirements: IndexMap<String, IgnoredAny>,
pid_ahb_workflows: IndexMap<String, ProbeWorkflow>,
tx_groups: IndexMap<String, String>,
}
fn assert_sorted<'a>(what: &str, keys: impl Iterator<Item = &'a String>) {
let keys: Vec<&String> = keys.collect();
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(keys, sorted, "{what} keys must serialize in sorted order");
}
let json = serde_json::to_string(&make_populated_cache()).unwrap();
let probe: Probe = serde_json::from_str(&json).unwrap();
assert_sorted("transaction_defs", probe.transaction_defs.keys());
assert_sorted("combined_defs", probe.combined_defs.keys());
assert_sorted("code_lookups", probe.code_lookups.keys());
let lookup = probe.code_lookups.values().next().unwrap();
assert!(lookup.len() > 10, "fixture lookup should have many entries");
assert_sorted("code_lookup entries", lookup.keys());
assert_sorted(
"segment_structure",
probe.segment_structure.element_counts.keys(),
);
assert_sorted("pid_segment_numbers", probe.pid_segment_numbers.keys());
assert_sorted("pid_requirements", probe.pid_requirements.keys());
assert_sorted("pid_ahb_workflows", probe.pid_ahb_workflows.keys());
let wf = probe.pid_ahb_workflows.values().next().unwrap();
assert_sorted("ub_definitions", wf.ub_definitions.keys());
assert_sorted("tx_groups", probe.tx_groups.keys());
}
#[test]
fn test_data_bundle_serializes_variants_sorted() {
use indexmap::IndexMap;
use serde::de::IgnoredAny;
#[derive(serde::Deserialize)]
struct Probe {
variants: IndexMap<String, IgnoredAny>,
}
let variants: BTreeMap<String, VariantCache> = (0..20)
.map(|i| (format!("VARIANT_{i:02}"), make_test_cache()))
.collect();
let bundle = DataBundle {
format_version: "FV2504".to_string(),
bundle_version: DataBundle::CURRENT_VERSION,
built_by: Some(DataBundle::PRODUCING_VERSION.to_string()),
variants,
bo4e_catalog: Default::default(),
code_lists: Default::default(),
};
let mut bytes = Vec::new();
bundle.write_to(&mut bytes).unwrap();
let probe: Probe = serde_json::from_slice(&bytes).unwrap();
let keys: Vec<&String> = probe.variants.keys().collect();
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(keys, sorted);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::definition::{MappingDefinition, MappingMeta, StructuredFieldMapping};
use indexmap::IndexMap;
fn make_def(fields: IndexMap<String, FieldMapping>) -> MappingDefinition {
MappingDefinition {
meta: MappingMeta {
entity: "Test".to_string(),
bo4e_type: "Test".to_string(),
source_group: "SG4".to_string(),
source_path: None,
discriminator: None,
repeat_on_tag: None,
parent_field: None,
target_list: None,
order: None,
},
fields,
complex_handlers: None,
}
}
#[test]
fn test_map_interchange_single_transaction_backward_compat() {
use mig_assembly::assembler::*;
let tree = AssembledTree {
segments: vec![
AssembledSegment {
tag: "UNH".to_string(),
elements: vec![vec!["001".to_string()]],
mig_number: None,
segment_number: None,
},
AssembledSegment {
tag: "BGM".to_string(),
elements: vec![vec!["E01".to_string()], vec!["DOC001".to_string()]],
mig_number: None,
segment_number: None,
},
],
groups: vec![
AssembledGroup {
group_id: "SG2".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "NAD".to_string(),
elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
mig_number: None,
segment_number: None,
}],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}],
},
AssembledGroup {
group_id: "SG4".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "IDE".to_string(),
elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
mig_number: None,
segment_number: None,
}],
child_groups: vec![AssembledGroup {
group_id: "SG5".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "LOC".to_string(),
elements: vec![
vec!["Z16".to_string()],
vec!["DE000111222333".to_string()],
],
mig_number: None,
segment_number: None,
}],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}],
}],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}],
},
],
post_group_start: 2,
inter_group_segments: std::collections::BTreeMap::new(),
};
let msg_engine = MappingEngine::from_definitions(vec![]);
let mut tx_fields: IndexMap<String, FieldMapping> = IndexMap::new();
tx_fields.insert(
"ide.1".to_string(),
FieldMapping::Simple("vorgangId".to_string()),
);
let mut malo_fields: IndexMap<String, FieldMapping> = IndexMap::new();
malo_fields.insert(
"loc.1".to_string(),
FieldMapping::Simple("marktlokationsId".to_string()),
);
let tx_engine = MappingEngine::from_definitions(vec![
MappingDefinition {
meta: MappingMeta {
entity: "Prozessdaten".to_string(),
bo4e_type: "Prozessdaten".to_string(),
source_group: "SG4".to_string(),
source_path: None,
discriminator: None,
repeat_on_tag: None,
parent_field: None,
target_list: None,
order: None,
},
fields: tx_fields,
complex_handlers: None,
},
MappingDefinition {
meta: MappingMeta {
entity: "Marktlokation".to_string(),
bo4e_type: "Marktlokation".to_string(),
source_group: "SG4.SG5".to_string(),
source_path: None,
discriminator: None,
repeat_on_tag: None,
parent_field: None,
target_list: None,
order: None,
},
fields: malo_fields,
complex_handlers: None,
},
]);
let result = MappingEngine::map_interchange(&msg_engine, &tx_engine, &tree, "SG4", true);
assert_eq!(result.transaktionen.len(), 1);
assert_eq!(
result.transaktionen[0].transaktionsdaten["vorgangId"]
.as_str()
.unwrap(),
"TX001"
);
assert_eq!(
result.transaktionen[0].stammdaten["marktlokation"]["marktlokationsId"]
.as_str()
.unwrap(),
"DE000111222333"
);
}
#[test]
fn test_map_reverse_pads_intermediate_empty_elements() {
let mut fields = IndexMap::new();
fields.insert(
"nad.0".to_string(),
FieldMapping::Structured(StructuredFieldMapping {
target: String::new(),
transform: None,
when: None,
default: Some("Z09".to_string()),
enum_map: None,
code_list: None,
also_code_list: None,
when_filled: None,
also_target: None,
also_enum_map: None,
}),
);
fields.insert(
"nad.3.0".to_string(),
FieldMapping::Simple("name".to_string()),
);
fields.insert(
"nad.3.1".to_string(),
FieldMapping::Simple("vorname".to_string()),
);
let def = make_def(fields);
let engine = MappingEngine::from_definitions(vec![]);
let bo4e = serde_json::json!({
"name": "Muster",
"vorname": "Max"
});
let instance = engine.map_reverse(&bo4e, &def);
assert_eq!(instance.segments.len(), 1);
let nad = &instance.segments[0];
assert_eq!(nad.tag, "NAD");
assert_eq!(nad.elements.len(), 4);
assert_eq!(nad.elements[0], vec!["Z09"]);
assert_eq!(nad.elements[1], vec![""]);
assert_eq!(nad.elements[2], vec![""]);
assert_eq!(nad.elements[3][0], "Muster");
assert_eq!(nad.elements[3][1], "Max");
}
#[test]
fn test_map_reverse_no_padding_when_contiguous() {
let mut fields = IndexMap::new();
fields.insert(
"dtm.0.0".to_string(),
FieldMapping::Structured(StructuredFieldMapping {
target: String::new(),
transform: None,
when: None,
default: Some("92".to_string()),
enum_map: None,
code_list: None,
also_code_list: None,
when_filled: None,
also_target: None,
also_enum_map: None,
}),
);
fields.insert(
"dtm.0.1".to_string(),
FieldMapping::Simple("value".to_string()),
);
fields.insert(
"dtm.0.2".to_string(),
FieldMapping::Structured(StructuredFieldMapping {
target: String::new(),
transform: None,
when: None,
default: Some("303".to_string()),
enum_map: None,
code_list: None,
also_code_list: None,
when_filled: None,
also_target: None,
also_enum_map: None,
}),
);
let def = make_def(fields);
let engine = MappingEngine::from_definitions(vec![]);
let bo4e = serde_json::json!({ "value": "20250531" });
let instance = engine.map_reverse(&bo4e, &def);
let dtm = &instance.segments[0];
assert_eq!(dtm.elements.len(), 1);
assert_eq!(dtm.elements[0], vec!["92", "20250531", "303"]);
}
#[test]
fn test_map_message_level_extracts_sg2_only() {
use mig_assembly::assembler::*;
let tree = AssembledTree {
segments: vec![
AssembledSegment {
tag: "UNH".to_string(),
elements: vec![vec!["001".to_string()]],
mig_number: None,
segment_number: None,
},
AssembledSegment {
tag: "BGM".to_string(),
elements: vec![vec!["E01".to_string()]],
mig_number: None,
segment_number: None,
},
],
groups: vec![
AssembledGroup {
group_id: "SG2".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "NAD".to_string(),
elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
mig_number: None,
segment_number: None,
}],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}],
},
AssembledGroup {
group_id: "SG4".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "IDE".to_string(),
elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
mig_number: None,
segment_number: None,
}],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}],
},
],
post_group_start: 2,
inter_group_segments: std::collections::BTreeMap::new(),
};
let mut msg_fields: IndexMap<String, FieldMapping> = IndexMap::new();
msg_fields.insert(
"nad.0".to_string(),
FieldMapping::Simple("marktrolle".to_string()),
);
msg_fields.insert(
"nad.1".to_string(),
FieldMapping::Simple("rollencodenummer".to_string()),
);
let msg_def = MappingDefinition {
meta: MappingMeta {
entity: "Marktteilnehmer".to_string(),
bo4e_type: "Marktteilnehmer".to_string(),
source_group: "SG2".to_string(),
source_path: None,
discriminator: None,
repeat_on_tag: None,
parent_field: None,
target_list: None,
order: None,
},
fields: msg_fields,
complex_handlers: None,
};
let engine = MappingEngine::from_definitions(vec![msg_def.clone()]);
let result = engine.map_all_forward(&tree);
assert!(result.get("marktteilnehmer").is_some());
let mt = &result["marktteilnehmer"];
assert_eq!(mt["marktrolle"].as_str().unwrap(), "MS");
assert_eq!(mt["rollencodenummer"].as_str().unwrap(), "9900123");
}
#[test]
fn test_map_transaction_scoped_to_sg4_instance() {
use mig_assembly::assembler::*;
let tree = AssembledTree {
segments: vec![
AssembledSegment {
tag: "UNH".to_string(),
elements: vec![vec!["001".to_string()]],
mig_number: None,
segment_number: None,
},
AssembledSegment {
tag: "BGM".to_string(),
elements: vec![vec!["E01".to_string()]],
mig_number: None,
segment_number: None,
},
],
groups: vec![AssembledGroup {
group_id: "SG4".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "IDE".to_string(),
elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
mig_number: None,
segment_number: None,
}],
child_groups: vec![AssembledGroup {
group_id: "SG5".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "LOC".to_string(),
elements: vec![
vec!["Z16".to_string()],
vec!["DE000111222333".to_string()],
],
mig_number: None,
segment_number: None,
}],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}],
}],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}],
}],
post_group_start: 2,
inter_group_segments: std::collections::BTreeMap::new(),
};
let mut proz_fields: IndexMap<String, FieldMapping> = IndexMap::new();
proz_fields.insert(
"ide.1".to_string(),
FieldMapping::Simple("vorgangId".to_string()),
);
let proz_def = MappingDefinition {
meta: MappingMeta {
entity: "Prozessdaten".to_string(),
bo4e_type: "Prozessdaten".to_string(),
source_group: "".to_string(), source_path: None,
discriminator: None,
repeat_on_tag: None,
parent_field: None,
target_list: None,
order: None,
},
fields: proz_fields,
complex_handlers: None,
};
let mut malo_fields: IndexMap<String, FieldMapping> = IndexMap::new();
malo_fields.insert(
"loc.1".to_string(),
FieldMapping::Simple("marktlokationsId".to_string()),
);
let malo_def = MappingDefinition {
meta: MappingMeta {
entity: "Marktlokation".to_string(),
bo4e_type: "Marktlokation".to_string(),
source_group: "SG5".to_string(), source_path: None,
discriminator: None,
repeat_on_tag: None,
parent_field: None,
target_list: None,
order: None,
},
fields: malo_fields,
complex_handlers: None,
};
let tx_engine = MappingEngine::from_definitions(vec![proz_def, malo_def]);
let sg4 = &tree.groups[0]; let sg4_instance = &sg4.repetitions[0];
let sub_tree = sg4_instance.as_assembled_tree();
let result = tx_engine.map_all_forward(&sub_tree);
assert_eq!(
result["prozessdaten"]["vorgangId"].as_str().unwrap(),
"TX001"
);
assert_eq!(
result["marktlokation"]["marktlokationsId"]
.as_str()
.unwrap(),
"DE000111222333"
);
}
#[test]
fn test_map_interchange_produces_full_hierarchy() {
use mig_assembly::assembler::*;
let tree = AssembledTree {
segments: vec![
AssembledSegment {
tag: "UNH".to_string(),
elements: vec![vec!["001".to_string()]],
mig_number: None,
segment_number: None,
},
AssembledSegment {
tag: "BGM".to_string(),
elements: vec![vec!["E01".to_string()]],
mig_number: None,
segment_number: None,
},
],
groups: vec![
AssembledGroup {
group_id: "SG2".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "NAD".to_string(),
elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
mig_number: None,
segment_number: None,
}],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}],
},
AssembledGroup {
group_id: "SG4".to_string(),
repetitions: vec![
AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "IDE".to_string(),
elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
mig_number: None,
segment_number: None,
}],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
},
AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "IDE".to_string(),
elements: vec![vec!["24".to_string()], vec!["TX002".to_string()]],
mig_number: None,
segment_number: None,
}],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
},
],
},
],
post_group_start: 2,
inter_group_segments: std::collections::BTreeMap::new(),
};
let mut msg_fields: IndexMap<String, FieldMapping> = IndexMap::new();
msg_fields.insert(
"nad.0".to_string(),
FieldMapping::Simple("marktrolle".to_string()),
);
let msg_defs = vec![MappingDefinition {
meta: MappingMeta {
entity: "Marktteilnehmer".to_string(),
bo4e_type: "Marktteilnehmer".to_string(),
source_group: "SG2".to_string(),
source_path: None,
discriminator: None,
repeat_on_tag: None,
parent_field: None,
target_list: None,
order: None,
},
fields: msg_fields,
complex_handlers: None,
}];
let mut tx_fields: IndexMap<String, FieldMapping> = IndexMap::new();
tx_fields.insert(
"ide.1".to_string(),
FieldMapping::Simple("vorgangId".to_string()),
);
let tx_defs = vec![MappingDefinition {
meta: MappingMeta {
entity: "Prozessdaten".to_string(),
bo4e_type: "Prozessdaten".to_string(),
source_group: "SG4".to_string(),
source_path: None,
discriminator: None,
repeat_on_tag: None,
parent_field: None,
target_list: None,
order: None,
},
fields: tx_fields,
complex_handlers: None,
}];
let msg_engine = MappingEngine::from_definitions(msg_defs);
let tx_engine = MappingEngine::from_definitions(tx_defs);
let result = MappingEngine::map_interchange(&msg_engine, &tx_engine, &tree, "SG4", true);
assert!(result.stammdaten["marktteilnehmer"].is_object());
assert_eq!(
result.stammdaten["marktteilnehmer"]["marktrolle"]
.as_str()
.unwrap(),
"MS"
);
assert_eq!(result.transaktionen.len(), 2);
assert_eq!(
result.transaktionen[0].transaktionsdaten["vorgangId"]
.as_str()
.unwrap(),
"TX001"
);
assert_eq!(
result.transaktionen[1].transaktionsdaten["vorgangId"]
.as_str()
.unwrap(),
"TX002"
);
}
#[test]
fn test_map_reverse_with_segment_structure_pads_trailing() {
let mut fields = IndexMap::new();
fields.insert(
"sts.0".to_string(),
FieldMapping::Structured(StructuredFieldMapping {
target: String::new(),
transform: None,
when: None,
default: Some("7".to_string()),
enum_map: None,
code_list: None,
also_code_list: None,
when_filled: None,
also_target: None,
also_enum_map: None,
}),
);
fields.insert(
"sts.2".to_string(),
FieldMapping::Simple("grund".to_string()),
);
let def = make_def(fields);
let mut counts = std::collections::BTreeMap::new();
counts.insert("STS".to_string(), 5usize);
let ss = SegmentStructure {
element_counts: counts,
};
let engine = MappingEngine::from_definitions(vec![]).with_segment_structure(ss);
let bo4e = serde_json::json!({ "grund": "E01" });
let instance = engine.map_reverse(&bo4e, &def);
let sts = &instance.segments[0];
assert_eq!(sts.elements.len(), 5);
assert_eq!(sts.elements[0], vec!["7"]);
assert_eq!(sts.elements[1], vec![""]);
assert_eq!(sts.elements[2], vec!["E01"]);
assert_eq!(sts.elements[3], vec![""]);
assert_eq!(sts.elements[4], vec![""]);
}
#[test]
fn test_resolve_child_relative_with_source_path() {
let mut map: std::collections::HashMap<String, Vec<usize>> =
std::collections::HashMap::new();
map.insert("sg4.sg8_ze1".to_string(), vec![6]);
map.insert("sg4.sg8_z98".to_string(), vec![0]);
assert_eq!(
resolve_child_relative("SG8.SG10", Some("sg4.sg8_ze1.sg10"), &map, 0),
"SG8:6.SG10"
);
assert_eq!(
resolve_child_relative("SG8:3.SG10", Some("sg4.sg8_ze1.sg10"), &map, 0),
"SG8:3.SG10"
);
assert_eq!(
resolve_child_relative("SG8.SG10", Some("sg4.sg8_unknown.sg10"), &map, 0),
"SG8.SG10"
);
assert_eq!(
resolve_child_relative("SG8.SG10", None, &map, 0),
"SG8.SG10"
);
assert_eq!(
resolve_child_relative("SG8.SG9", Some("sg4.sg8_z98.sg9"), &map, 0),
"SG8:0.SG9"
);
map.insert("sg4.sg8_zf3".to_string(), vec![3, 4]);
assert_eq!(
resolve_child_relative("SG8.SG10", Some("sg4.sg8_zf3.sg10"), &map, 0),
"SG8:3.SG10"
);
assert_eq!(
resolve_child_relative("SG8.SG10", Some("sg4.sg8_zf3.sg10"), &map, 1),
"SG8:4.SG10"
);
}
#[test]
fn test_place_in_groups_returns_rep_index() {
let mut groups: Vec<AssembledGroup> = Vec::new();
let instance = AssembledGroupInstance {
segments: vec![],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
};
assert_eq!(place_in_groups(&mut groups, "SG8", instance), 0);
let instance = AssembledGroupInstance {
segments: vec![],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
};
assert_eq!(place_in_groups(&mut groups, "SG8", instance), 1);
let instance = AssembledGroupInstance {
segments: vec![],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
};
assert_eq!(place_in_groups(&mut groups, "SG8:5", instance), 5);
}
#[test]
fn test_resolve_by_source_path() {
use mig_assembly::assembler::*;
let tree = AssembledTree {
segments: vec![],
groups: vec![AssembledGroup {
group_id: "SG4".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![],
child_groups: vec![AssembledGroup {
group_id: "SG8".to_string(),
repetitions: vec![
AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "SEQ".to_string(),
elements: vec![vec!["Z98".to_string()]],
mig_number: None,
segment_number: None,
}],
child_groups: vec![AssembledGroup {
group_id: "SG10".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "CCI".to_string(),
elements: vec![vec![], vec![], vec!["ZB3".to_string()]],
mig_number: None,
segment_number: None,
}],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}],
}],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
},
AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "SEQ".to_string(),
elements: vec![vec!["ZD7".to_string()]],
mig_number: None,
segment_number: None,
}],
child_groups: vec![AssembledGroup {
group_id: "SG10".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "CCI".to_string(),
elements: vec![vec![], vec![], vec!["ZE6".to_string()]],
mig_number: None,
segment_number: None,
}],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}],
}],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
},
],
}],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}],
}],
post_group_start: 0,
inter_group_segments: std::collections::BTreeMap::new(),
};
let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_z98.sg10");
assert!(inst.is_some());
assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZB3");
let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_zd7.sg10");
assert!(inst.is_some());
assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZE6");
let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_zzz.sg10");
assert!(inst.is_none());
let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8.sg10");
assert!(inst.is_some());
assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZB3");
}
#[test]
fn test_parse_source_path_part() {
assert_eq!(parse_source_path_part("sg4"), ("sg4", None));
assert_eq!(parse_source_path_part("sg8_z98"), ("sg8", Some("z98")));
assert_eq!(parse_source_path_part("sg10"), ("sg10", None));
assert_eq!(parse_source_path_part("sg12_z04"), ("sg12", Some("z04")));
}
#[test]
fn test_has_source_path_qualifiers() {
assert!(has_source_path_qualifiers("sg4.sg8_z98.sg10"));
assert!(has_source_path_qualifiers("sg4.sg8_ze1.sg9"));
assert!(!has_source_path_qualifiers("sg4.sg6"));
assert!(!has_source_path_qualifiers("sg4.sg8.sg10"));
}
#[test]
fn test_extract_all_from_instance_collects_all_qualifier_matches() {
use mig_assembly::assembler::*;
let instance = AssembledGroupInstance {
segments: vec![
AssembledSegment {
tag: "SEQ".to_string(),
elements: vec![vec!["ZD6".to_string()]],
mig_number: None,
segment_number: None,
},
AssembledSegment {
tag: "RFF".to_string(),
elements: vec![vec!["Z34".to_string(), "REF_A".to_string()]],
mig_number: None,
segment_number: None,
},
AssembledSegment {
tag: "RFF".to_string(),
elements: vec![vec!["Z34".to_string(), "REF_B".to_string()]],
mig_number: None,
segment_number: None,
},
AssembledSegment {
tag: "RFF".to_string(),
elements: vec![vec!["Z34".to_string(), "REF_C".to_string()]],
mig_number: None,
segment_number: None,
},
AssembledSegment {
tag: "RFF".to_string(),
elements: vec![vec!["Z35".to_string(), "OTHER".to_string()]],
mig_number: None,
segment_number: None,
},
],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
};
let all = MappingEngine::extract_all_from_instance(&instance, "rff[Z34,*].0.1");
assert_eq!(all, vec!["REF_A", "REF_B", "REF_C"]);
let single = MappingEngine::extract_from_instance(&instance, "rff[Z34].0.1");
assert_eq!(single, Some("REF_A".to_string()));
let second = MappingEngine::extract_from_instance(&instance, "rff[Z34,1].0.1");
assert_eq!(second, Some("REF_B".to_string()));
}
}