use std::collections::{BTreeMap, BTreeSet};
use mig_types::schema::mig::{MigSchema, MigSegmentGroup};
use crate::code_lists::CodeLists;
use crate::code_lookup::CodeLookup;
use crate::definition::{FieldMapping, MappingDefinition};
use crate::engine::{
child_entity_nesting_pairs, is_nested_child_of, parse_tag_qualifier, strip_all_rep_indices,
to_camel_case, MappingEngine, VariantCache,
};
#[derive(Debug, Clone, Default, PartialEq)]
pub struct OutputShape {
pub message: ScopeShape,
pub transaction: ScopeShape,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ScopeShape {
pub entities: BTreeMap<String, EntityShape>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct EntityShape {
pub entity: String,
pub object: ObjectShape,
pub repeats: bool,
pub children: BTreeSet<String>,
pub key: Option<KeyField>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ObjectShape {
pub fields: BTreeMap<String, FieldShape>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FieldShape {
Leaf(LeafShape),
Object(ObjectShape),
List {
item_type: String,
item: ObjectShape,
},
Mixed,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct LeafShape {
pub values: Option<BTreeMap<String, WrittenValue>>,
pub enriched: bool,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct WrittenValue {
pub raw_codes: BTreeSet<String>,
pub meaning: String,
pub enum_key: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct KeyField {
pub field: String,
pub also_field: Option<String>,
pub codes: BTreeMap<String, (String, Option<String>)>,
pub source_paths: BTreeSet<String>,
}
impl OutputShape {
pub fn for_pid(vc: &VariantCache, pid: &str, schema: Option<&serde_json::Value>) -> Self {
let key = format!("pid_{pid}");
let tx_group = vc.tx_group(pid).unwrap_or("");
let mig = vc.filtered_mig(pid);
let lookup = vc.code_lookups.get(&key);
let tx_defs = vc
.transaction_defs
.get(&key)
.map(Vec::as_slice)
.unwrap_or_default();
let sources = Sources {
lookup,
mig: mig.as_ref(),
schema,
code_lists: Some(&vc.code_lists),
};
Self::from_parts(&vc.message_defs, tx_defs, tx_group, &sources)
}
pub fn from_parts(
message_defs: &[MappingDefinition],
transaction_defs: &[MappingDefinition],
tx_group: &str,
sources: &Sources<'_>,
) -> Self {
let message = ScopeShape::build(message_defs, sources, None);
let transaction = if tx_group.is_empty() {
ScopeShape::default()
} else {
ScopeShape::build(transaction_defs, sources, Some(tx_group))
};
Self {
message,
transaction,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Sources<'a> {
pub lookup: Option<&'a CodeLookup>,
pub mig: Option<&'a MigSchema>,
pub schema: Option<&'a serde_json::Value>,
pub code_lists: Option<&'a CodeLists>,
}
impl ScopeShape {
fn build(defs: &[MappingDefinition], sources: &Sources<'_>, tx_group: Option<&str>) -> Self {
let Sources {
lookup,
mig,
code_lists,
..
} = *sources;
let mut entities: BTreeMap<String, EntityShape> = BTreeMap::new();
for def in defs.iter().filter(|d| d.meta.parent_field.is_none()) {
let key = to_camel_case(&def.meta.entity);
let object = object_shape(def, defs, lookup, code_lists);
let repeats = def_repeats(def, sources, tx_group);
let entry = entities.entry(key).or_insert_with(|| EntityShape {
entity: def.meta.entity.clone(),
..Default::default()
});
entry.object.merge(object);
entry.repeats |= repeats;
}
for (_, parent, child, _) in child_entity_nesting_pairs(defs, tx_group) {
let child_key = to_camel_case(&child);
if !entities.contains_key(&child_key) {
continue;
}
if let Some(p) = entities.get_mut(&to_camel_case(&parent)) {
p.children.insert(child_key);
}
}
for (key, entity) in entities.iter_mut() {
entity.key = key_field(key, defs, lookup, mig, code_lists);
}
Self { entities }
}
}
fn to_pascal(name: &str) -> String {
let mut chars = name.chars();
match chars.next() {
Some(first) => first.to_uppercase().chain(chars).collect(),
None => String::new(),
}
}
impl ObjectShape {
pub fn merge(&mut self, other: ObjectShape) {
for (name, field) in other.fields {
match self.fields.remove(&name) {
None => {
self.fields.insert(name, field);
}
Some(existing) => {
self.fields.insert(name, existing.merged(field));
}
}
}
}
fn insert_path(&mut self, path: &str, leaf: LeafShape) {
match path.split_once('.') {
None => {
let merged = match self.fields.remove(path) {
None => FieldShape::Leaf(leaf),
Some(existing) => existing.merged(FieldShape::Leaf(leaf)),
};
self.fields.insert(path.to_string(), merged);
}
Some((head, rest)) if head.ends_with("[]") => {
let name = head.trim_end_matches("[]");
let field =
self.fields
.entry(name.to_string())
.or_insert_with(|| FieldShape::List {
item_type: to_pascal(name),
item: ObjectShape::default(),
});
match field {
FieldShape::List { item, .. } => item.insert_path(rest, leaf),
other => *other = FieldShape::Mixed,
}
}
Some((head, rest)) => {
let field = self
.fields
.entry(head.to_string())
.or_insert_with(|| FieldShape::Object(ObjectShape::default()));
match field {
FieldShape::Object(obj) => obj.insert_path(rest, leaf),
other => *other = FieldShape::Mixed,
}
}
}
}
}
impl FieldShape {
fn merged(self, other: FieldShape) -> FieldShape {
match (self, other) {
(FieldShape::Leaf(mut a), FieldShape::Leaf(b)) => {
a.merge(b);
FieldShape::Leaf(a)
}
(FieldShape::Object(mut a), FieldShape::Object(b)) => {
a.merge(b);
FieldShape::Object(a)
}
(
FieldShape::List {
item_type,
item: mut a,
},
FieldShape::List { item: b, .. },
) => {
a.merge(b);
FieldShape::List { item_type, item: a }
}
_ => FieldShape::Mixed,
}
}
}
impl LeafShape {
fn merge(&mut self, other: LeafShape) {
self.enriched |= other.enriched;
self.values = match (self.values.take(), other.values) {
(Some(mut a), Some(b)) => {
for (value, w) in b {
let entry = a.entry(value).or_default();
if entry.meaning.is_empty() {
entry.meaning = w.meaning;
}
if entry.enum_key.is_none() {
entry.enum_key = w.enum_key;
}
entry.raw_codes.extend(w.raw_codes);
}
Some(a)
}
_ => None,
};
}
pub fn may_be(&self, value: &str) -> bool {
self.values.as_ref().map_or(true, |v| v.contains_key(value))
}
}
fn path_position(path: &str) -> (String, Option<String>, (usize, usize)) {
let parts: Vec<&str> = path.split('.').collect();
let (tag, qualifier, _) = parse_tag_qualifier(parts[0]);
let position = MappingEngine::parse_element_component(&parts[1..]);
(tag, qualifier.map(String::from), position)
}
fn object_shape(
def: &MappingDefinition,
defs: &[MappingDefinition],
lookup: Option<&CodeLookup>,
code_lists: Option<&CodeLists>,
) -> ObjectShape {
let mut obj = ObjectShape::default();
for (path, mapping) in &def.fields {
let (target, enum_map, also) = match mapping {
FieldMapping::Simple(t) => (t.as_str(), None, None),
FieldMapping::Structured(s) => (
s.target.as_str(),
table(code_lists, s.enum_map.as_ref(), s.code_list.as_deref()),
s.also_target.as_deref().zip(table(
code_lists,
s.also_enum_map.as_ref(),
s.also_code_list.as_deref(),
)),
),
FieldMapping::Nested(_) => continue,
};
if target.is_empty() {
continue;
}
let (tag, path_qualifier, (element, component)) = path_position(path);
let (codes, enriched) = match (lookup, def.meta.source_path.as_deref()) {
(Some(lookup), Some(sp)) => {
let disc_qualifier = MappingEngine::discriminator_qualifier_for_tag(def, &tag);
let (pq, dq) = (path_qualifier.as_deref(), disc_qualifier.as_deref());
let enriched = lookup
.enrichment_codes(sp, &tag, pq, dq, element, component)
.is_some();
(
lookup.field_codes(sp, &tag, pq, dq, element, component),
enriched,
)
}
_ => (None, false),
};
let values = codes.map(|codes| {
let mut values: BTreeMap<String, WrittenValue> = BTreeMap::new();
let raw = codes
.iter()
.map(|(code, e)| (code.clone(), e.meaning.clone(), e.enum_key.clone()))
.chain(
enum_map
.into_iter()
.flatten()
.filter(|(code, _)| !codes.contains_key(*code))
.map(|(code, _)| (code.clone(), String::new(), None)),
);
for (code, meaning, enum_key) in raw {
let written = enum_map
.and_then(|m| m.get(&code))
.cloned()
.unwrap_or_else(|| code.clone());
let entry = values.entry(written).or_default();
if entry.raw_codes.is_empty() {
entry.meaning = meaning;
entry.enum_key = enum_key;
}
entry.raw_codes.insert(code);
}
values
});
obj.insert_path(target, LeafShape { values, enriched });
if let Some((also_target, also_map)) = also {
let values = also_map
.values()
.map(|v| (v.clone(), WrittenValue::default()))
.collect();
obj.insert_path(
also_target,
LeafShape {
values: Some(values),
enriched: false,
},
);
}
}
for child in defs.iter().filter(|c| is_nested_child_of(c, def)) {
let field = child.meta.parent_field.clone().unwrap_or_default();
let list = FieldShape::List {
item_type: child.meta.bo4e_type.clone(),
item: object_shape(child, defs, lookup, code_lists),
};
let merged = match obj.fields.remove(&field) {
None => list,
Some(existing) => existing.merged(list),
};
obj.fields.insert(field, merged);
}
obj
}
fn table<'a>(
code_lists: Option<&'a CodeLists>,
inline: Option<&'a BTreeMap<String, String>>,
named: Option<&str>,
) -> Option<&'a BTreeMap<String, String>> {
match code_lists {
Some(lists) => lists.resolve(inline, named),
None => inline,
}
}
fn group_chain<'a>(mig: &'a MigSchema, source_group: &str) -> Option<Vec<&'a MigSegmentGroup>> {
let mut chain = Vec::new();
let mut groups = &mig.segment_groups;
for part in strip_all_rep_indices(source_group).split('.') {
let group = groups.iter().find(|g| g.id.eq_ignore_ascii_case(part))?;
chain.push(group);
groups = &group.nested_groups;
}
Some(chain)
}
fn def_repeats(def: &MappingDefinition, sources: &Sources<'_>, tx_group: Option<&str>) -> bool {
if def.meta.source_group.is_empty() {
return false;
}
if def.meta.repeat_on_tag.is_some() {
return true;
}
let disc = MappingEngine::discriminator_qualifier(def);
let mig_repeats = || {
let Some(chain) = sources
.mig
.and_then(|m| group_chain(m, &def.meta.source_group))
else {
return true;
};
chain.iter().enumerate().any(|(i, g)| {
let is_tx_root = i == 0 && tx_group.is_some_and(|tx| g.id.eq_ignore_ascii_case(tx));
!is_tx_root && g.max_rep_std.max(g.max_rep_spec) > 1
})
};
let (Some(schema), Some(sp)) = (sources.schema, def.meta.source_path.as_deref()) else {
return mig_repeats();
};
match schema_path_repeats(schema, sp, tx_group, disc.as_deref()) {
None => false,
Some(true) => true,
Some(false) => disc.is_none() && mig_repeats(),
}
}
fn schema_path_repeats(
schema: &serde_json::Value,
source_path: &str,
tx_group: Option<&str>,
discriminator: Option<&str>,
) -> Option<bool> {
let mut level: Vec<&serde_json::Value> = vec![schema.get("fields")?];
let mut repeats = false;
let parts: Vec<&str> = source_path.split('.').collect();
for (i, part) in parts.iter().copied().enumerate() {
let mut next: Vec<&serde_json::Value> = map_names(&level, part)
.into_iter()
.map(|(_, node)| node)
.collect();
if next.is_empty() {
return None;
}
if i == parts.len() - 1 && !part.contains('_') {
if let Some(q) = discriminator.map(|q| format!("_{}", q.to_lowercase())) {
let selected: Vec<&serde_json::Value> = map_names(&level, part)
.into_iter()
.filter(|(name, _)| name.to_lowercase().ends_with(&q))
.map(|(_, node)| node)
.collect();
if !selected.is_empty() {
next = selected;
}
}
}
let is_tx_root = i == 0 && tx_group.is_some_and(|tx| part.eq_ignore_ascii_case(tx));
if !is_tx_root {
repeats |= next.len() > 1;
let last_discriminated = i == parts.len() - 1 && discriminator.is_some();
repeats |= next.iter().any(|node| {
node.get("max_reps")
.and_then(|v| v.as_u64())
.map_or(true, |max| max > 1)
|| (!last_discriminated && entry_segment_count(node) > 1)
});
}
level = next
.iter()
.filter_map(|node| node.get("children"))
.collect();
}
Some(repeats)
}
fn entry_segment_count(node: &serde_json::Value) -> usize {
let Some(segments) = node.get("segments").and_then(|v| v.as_array()) else {
return 0;
};
let entry = segments.first().and_then(|s| s.get("id"));
segments.iter().filter(|s| s.get("id") == entry).count()
}
fn map_names<'a>(
containers: &[&'a serde_json::Value],
part: &str,
) -> Vec<(&'a String, &'a serde_json::Value)> {
let prefix = format!("{part}_");
containers
.iter()
.filter_map(|c| c.as_object())
.flatten()
.filter(|(name, _)| {
name.eq_ignore_ascii_case(part) || (!part.contains('_') && name.starts_with(&prefix))
})
.collect()
}
fn key_field(
entity_key: &str,
defs: &[MappingDefinition],
lookup: Option<&CodeLookup>,
mig: Option<&MigSchema>,
code_lists: Option<&CodeLists>,
) -> Option<KeyField> {
let mig = mig?;
let entity_defs: Vec<&MappingDefinition> = defs
.iter()
.filter(|d| d.meta.parent_field.is_none() && to_camel_case(&d.meta.entity) == entity_key)
.collect();
let depth = |d: &MappingDefinition| d.meta.source_group.split('.').count();
let top = entity_defs.iter().map(|d| depth(d)).min()?;
let mut key: Option<KeyField> = None;
for def in entity_defs.iter().filter(|d| depth(d) == top) {
if def.meta.source_group.is_empty() || def.meta.discriminator.is_some() {
return None;
}
let chain = group_chain(mig, &def.meta.source_group)?;
let entry_tag = chain.last()?.segments.first()?.id.to_uppercase();
let (field, enum_map, also) = def.fields.iter().find_map(|(path, mapping)| {
let (tag, qualifier, position) = path_position(path);
let numeric = path.split('.').skip(1).all(|p| p.parse::<usize>().is_ok());
if tag != entry_tag || qualifier.is_some() || !numeric || position != (0, 0) {
return None;
}
match mapping {
FieldMapping::Simple(t) if !t.is_empty() => Some((t.clone(), None, None)),
FieldMapping::Structured(s) if !s.target.is_empty() => Some((
s.target.clone(),
table(code_lists, s.enum_map.as_ref(), s.code_list.as_deref()),
s.also_target.clone().zip(table(
code_lists,
s.also_enum_map.as_ref(),
s.also_code_list.as_deref(),
)),
)),
_ => None,
}
})?;
let also_field = also.as_ref().map(|(f, _)| f.clone());
let entry = key.get_or_insert_with(|| KeyField {
field: field.clone(),
also_field: also_field.clone(),
..Default::default()
});
if entry.field != field || entry.also_field != also_field {
return None;
}
let mut raw: BTreeSet<String> = enum_map
.into_iter()
.flatten()
.map(|(c, _)| c.clone())
.collect();
if let (Some(lookup), Some(sp)) = (lookup, def.meta.source_path.as_deref()) {
if let Some(codes) = lookup.codes_q(sp, &entry_tag, None, 0, 0) {
raw.extend(codes.keys().cloned());
}
entry.source_paths.insert(sp.to_string());
}
for code in raw {
let written = enum_map
.and_then(|m| m.get(&code))
.cloned()
.unwrap_or_else(|| code.clone());
let also_value = also.as_ref().and_then(|(_, m)| m.get(&code).cloned());
let value = (written, also_value);
match entry.codes.get(&code) {
Some(existing) if *existing != value => return None,
_ => {
entry.codes.insert(code, value);
}
}
}
}
key.filter(|k| !k.codes.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn def(toml: &str) -> MappingDefinition {
MappingDefinition::from_toml_str(toml).unwrap()
}
fn nad_node(code: &str, max_reps: u64) -> serde_json::Value {
json!({
"max_reps": max_reps,
"discriminator": {"segment": "NAD", "element": "3035", "values": [code]},
"segments": [{"id": "NAD", "elements": [
{"index": 0, "id": "3035", "type": "code",
"codes": [{"value": code, "name": format!("Rolle {code}")}]}
]}]
})
}
fn schema(sg12_max_reps: u64) -> serde_json::Value {
json!({"fields": {
"sg2": {"max_reps": 2, "segments": [], "children": {}},
"sg4": {"max_reps": 99999, "segments": [], "children": {
"sg5_z16": {"max_reps": 999999, "segments": []},
"sg6": {"max_reps": 1, "segments": []},
"sg12_z07": nad_node("Z07", sg12_max_reps),
"sg12_z39": nad_node("Z39", sg12_max_reps),
"sg12_z40": nad_node("Z40", sg12_max_reps),
}}
}})
}
fn mig() -> MigSchema {
let segment = |id: &str| {
json!({"id": id, "name": id, "description": null, "counter": null, "level": 1,
"number": null, "max_rep_std": 1, "max_rep_spec": 1, "status_std": null,
"status_spec": null, "example": null, "data_elements": [], "composites": []})
};
let group = |id: &str, entry: &str, nested: Vec<serde_json::Value>| {
json!({"id": id, "name": id, "description": null, "counter": null, "level": 1,
"max_rep_std": 99, "max_rep_spec": 99, "status_std": null, "status_spec": null,
"segments": [segment(entry)], "nested_groups": nested})
};
serde_json::from_value(json!({
"message_type": "UTILMD", "variant": null, "version": "", "publication_date": "",
"author": "", "format_version": "FV2604", "source_file": "", "segments": [],
"segment_groups": [group("SG4", "IDE", vec![
group("SG12", "NAD", vec![group("SG13", "CTA", vec![])]),
])]
}))
.unwrap()
}
const GESCHAEFTSPARTNER: &str = r#"
[meta]
entity = "Geschaeftspartner"
bo4e_type = "Geschaeftspartner"
source_group = "SG4.SG12"
source_path = "sg4.sg12"
[fields]
"nad.3.0" = "name1"
"nad.0" = { target = "partnerrolle", enum_map = { "Z07" = "kundeMsb", "Z39" = "kundeLf", "Z40" = "kundeLf" }, also_target = "datenqualitaet", also_enum_map = { "Z39" = "base", "Z40" = "informative" } }
"#;
const KONTAKTWEG: &str = r#"
[meta]
entity = "Geschaeftspartner"
bo4e_type = "Kontaktweg"
source_group = "SG4.SG12.SG13"
source_path = "sg4.sg12.sg13"
parent_field = "kontaktwege"
[fields]
"com.0.0" = "kontaktwert"
"#;
fn shape(sg12_max_reps: u64) -> OutputShape {
let schema = schema(sg12_max_reps);
let lookup = CodeLookup::from_schema_value(&schema);
let mig = mig();
let defs = vec![def(GESCHAEFTSPARTNER), def(KONTAKTWEG)];
OutputShape::from_parts(
&[],
&defs,
"SG4",
&Sources {
lookup: Some(&lookup),
mig: Some(&mig),
schema: Some(&schema),
code_lists: None,
},
)
}
#[test]
fn code_field_values_are_written_values_with_raw_code_aliases() {
let shape = shape(1);
let gp = &shape.transaction.entities["geschaeftspartner"];
let Some(FieldShape::Leaf(rolle)) = gp.object.fields.get("partnerrolle") else {
panic!("partnerrolle leaf: {gp:#?}");
};
assert!(rolle.enriched, "a schema code field can be enriched");
let values = rolle.values.as_ref().unwrap();
let raw = |v: &str| values[v].raw_codes.iter().cloned().collect::<Vec<_>>();
assert_eq!(values.keys().collect::<Vec<_>>(), ["kundeLf", "kundeMsb"]);
assert_eq!(raw("kundeLf"), ["Z39", "Z40"]);
assert_eq!(raw("kundeMsb"), ["Z07"]);
assert_eq!(values["kundeMsb"].meaning, "Rolle Z07");
let Some(FieldShape::Leaf(dq)) = gp.object.fields.get("datenqualitaet") else {
panic!("datenqualitaet leaf");
};
assert!(!dq.enriched, "also_target values are never enriched");
let dq_values: Vec<&String> = dq.values.as_ref().unwrap().keys().collect();
assert_eq!(dq_values, ["base", "informative"]);
assert!(
matches!(gp.object.fields.get("name1"), Some(FieldShape::Leaf(l)) if l.values.is_none())
);
assert!(matches!(
gp.object.fields.get("kontaktwege"),
Some(FieldShape::List { item_type, item }) if item_type == "Kontaktweg"
&& item.fields.contains_key("kontaktwert")
));
}
#[test]
fn key_field_records_joint_qualifier_decomposition() {
let shape = shape(1);
let key = shape.transaction.entities["geschaeftspartner"]
.key
.clone()
.expect("key field");
assert_eq!(key.field, "partnerrolle");
assert_eq!(key.also_field.as_deref(), Some("datenqualitaet"));
let codes: Vec<(&str, &str, Option<&str>)> = key
.codes
.iter()
.map(|(c, (w, a))| (c.as_str(), w.as_str(), a.as_deref()))
.collect();
assert_eq!(
codes,
[
("Z07", "kundeMsb", None),
("Z39", "kundeLf", Some("base")),
("Z40", "kundeLf", Some("informative")),
]
);
}
fn status_schema() -> serde_json::Value {
let rff = |qual: &str, id: serde_json::Value| {
json!({"id": "RFF", "elements": [{"index": 0, "composite": "C506", "components": [
{"sub_index": 0, "id": "1153", "type": "code", "codes": [{"value": qual, "name": qual}]},
id,
]}]})
};
let data = json!({"sub_index": 1, "id": "1154", "type": "data"});
let cav = |qual: &str, codes: serde_json::Value| {
json!({"id": "CAV", "elements": [{"index": 0, "composite": "C889", "components": [
{"sub_index": 0, "id": "7111", "type": "code", "codes": [{"value": qual, "name": qual}]},
{"sub_index": 1, "id": "7110", "type": "code", "codes": codes},
]}]})
};
json!({"fields": {"sg14": {"max_reps": 1, "segments": [], "children": {"sg15": {"max_reps": 1, "segments": [
rff("Z13", json!({"sub_index": 1, "id": "1154", "type": "code",
"codes": [{"value": "21037", "name": "RD / NB-Bewertung"}]})),
rff("ACW", data.clone()),
rff("ACE", data),
cav("Z91", json!([{"value": "A", "name": "Alpha"}, {"value": "B", "name": "Beta"}])),
cav("ZF0", json!([{"value": "C", "name": "Gamma"}])),
]}}}}})
}
#[test]
fn qualified_field_paths_take_codes_of_their_own_segment_variant() {
let schema = status_schema();
let lookup = CodeLookup::from_schema_value(&schema);
let status = def(r#"
[meta]
entity = "Status"
bo4e_type = "Status"
source_group = "SG14.SG15"
source_path = "sg14.sg15"
[fields]
"rff[Z13].0.1" = "pruefidentifikator"
"rff[ACW].0.0" = "acwQualifier"
"rff[ACW].0.1" = "referenz"
"rff[ACE].0.1" = "gegenvorschlagReferenz"
"cav[Z91].0.1" = "z91Wert"
"cav.0.1" = "wert"
"#);
let obj = object_shape(&status, &[], Some(&lookup), None);
let leaf = |name: &str| match obj.fields.get(name) {
Some(FieldShape::Leaf(l)) => l.clone(),
other => panic!("{name}: {other:?}"),
};
let values = |name: &str| {
leaf(name)
.values
.map(|v| v.keys().cloned().collect::<Vec<_>>())
};
for free_reference in ["referenz", "gegenvorschlagReferenz"] {
assert_eq!(
values(free_reference),
None,
"{free_reference} is RFF+ACW/ACE data, not RFF+Z13's PID code"
);
assert!(!leaf(free_reference).enriched);
}
assert_eq!(
values("pruefidentifikator"),
Some(vec!["21037".to_string()])
);
assert_eq!(values("acwQualifier"), Some(vec!["ACW".to_string()]));
assert_eq!(
values("z91Wert"),
Some(vec!["A".to_string(), "B".to_string()])
);
assert_eq!(
values("wert"),
Some(vec!["A".to_string(), "B".to_string(), "C".to_string()]),
"an unqualified path reads any CAV variant"
);
}
#[test]
fn a_named_code_list_shapes_like_the_inline_table() {
let schema = status_schema();
let lookup = CodeLookup::from_schema_value(&schema);
let rule = |mapping: &str| {
def(&format!(
r#"
[meta]
entity = "Status"
bo4e_type = "Status"
source_group = "SG14.SG15"
source_path = "sg14.sg15"
[fields]
"cav[Z91].0.1" = {mapping}
"#
))
};
let inline = rule(r#"{ target = "z91Wert", enum_map = { "A" = "alpha", "B" = "beta" } }"#);
let named = rule(r#"{ target = "z91Wert", code_list = "z91" }"#);
let lists = crate::code_lists::CodeLists::from_toml_str(
"[z91]\n\"A\" = \"alpha\"\n\"B\" = \"beta\"\n",
)
.unwrap();
let values = |obj: &ObjectShape| match obj.fields.get("z91Wert") {
Some(FieldShape::Leaf(l)) => l
.values
.as_ref()
.map(|v| v.keys().cloned().collect::<Vec<_>>()),
other => panic!("{other:?}"),
};
let expected = values(&object_shape(&inline, &[], Some(&lookup), None));
assert_eq!(
expected,
Some(vec!["alpha".to_string(), "beta".to_string()])
);
assert_eq!(
values(&object_shape(&named, &[], Some(&lookup), Some(&lists))),
expected
);
}
#[test]
fn several_variants_of_an_unqualified_group_repeat() {
assert!(shape(1).transaction.entities["geschaeftspartner"].repeats);
}
#[test]
fn repetition_follows_the_pid_schema() {
let schema = schema(1);
let mig = mig();
let sources = Sources {
schema: Some(&schema),
mig: Some(&mig),
lookup: None,
code_lists: None,
};
let repeats = |toml: &str, tx: Option<&str>| def_repeats(&def(toml), &sources, tx);
let entity = |sg: &str, sp: &str, extra: &str| {
format!(
"[meta]\nentity = \"E\"\nbo4e_type = \"E\"\nsource_group = \"{sg}\"\n\
source_path = \"{sp}\"\n{extra}\n[fields]\n\"x.0\" = \"x\"\n"
)
};
assert!(repeats(&entity("SG4.SG5", "sg4.sg5_z16", ""), Some("SG4")));
assert!(
!repeats(
&entity("SG4.SG6", "sg4.sg6", "discriminator = \"RFF.0.0=Z13\""),
Some("SG4")
),
"a discriminated variant repeats as the schema says"
);
assert!(
repeats(&entity("SG4.SG6", "sg4.sg6", ""), Some("SG4")),
"without a discriminator the MIG's repetition counts (unknown here)"
);
assert!(!repeats(&entity("SG4", "sg4", ""), Some("SG4")), "tx root");
assert!(repeats(&entity("SG2", "sg2", ""), None));
assert!(
!repeats(
&entity("SG4.SG12", "sg4.sg12", "discriminator = \"NAD.0.0=Z07\""),
Some("SG4")
),
"a discriminator selects one variant"
);
assert!(
!repeats(&entity("SG4.SG9", "sg4.sg9", ""), Some("SG4")),
"absent group"
);
assert!(repeats(
&entity("SG4.SG6", "sg4.sg6", "repeat_on_tag = \"FTX\""),
Some("SG4")
));
}
}