1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
//! TOML mapping definition types.
//!
//! These types are deserialized from TOML mapping files
//! in the `mappings/{format_version}/{message_type}_{variant}/` directory.
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::path_resolver::PathResolver;
/// Root mapping definition — one per TOML file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MappingDefinition {
pub meta: MappingMeta,
/// Field mappings — uses IndexMap to preserve TOML file insertion order,
/// which determines reverse-mapping segment ordering (e.g., DTM+Z05 before DTM+Z01).
pub fields: IndexMap<String, FieldMapping>,
pub complex_handlers: Option<Vec<ComplexHandlerRef>>,
}
/// Metadata about the entity being mapped.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MappingMeta {
pub entity: String,
pub bo4e_type: String,
pub source_group: String,
/// PID struct field path (e.g., "sg2", "sg4.sg8_z79").
/// When present, the mapping engine can use PID-direct navigation
/// instead of AssembledTree group resolution.
#[serde(default)]
pub source_path: Option<String>,
pub discriminator: Option<String>,
/// When set, the engine iterates over all segments matching this tag within the
/// group instance and produces one array element per segment. Used for repeating
/// segments like FTX that aren't in their own subgroup.
pub repeat_on_tag: Option<String>,
}
/// A field mapping — either a simple string target or a structured mapping.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FieldMapping {
/// Simple: "source_path" = "target_field"
Simple(String),
/// Structured: with optional transform, condition, etc.
Structured(StructuredFieldMapping),
/// Nested group mappings
Nested(IndexMap<String, FieldMapping>),
}
/// A structured field mapping with optional transform and condition.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StructuredFieldMapping {
pub target: String,
pub transform: Option<String>,
pub when: Option<String>,
pub default: Option<String>,
/// Bidirectional enum translation map (EDIFACT value → BO4E value).
/// Forward: looks up extracted EDIFACT value to produce BO4E value.
/// Reverse: reverse-looks up BO4E value to produce EDIFACT value.
/// Uses BTreeMap for deterministic reverse lookup (first key alphabetically wins).
pub enum_map: Option<BTreeMap<String, String>>,
/// Conditional injection: only inject the default on reverse when ANY of these
/// target field names has a value in the BO4E JSON (checked in both core and
/// companion objects). Used for transport metadata (qualifiers, codelist codes)
/// that should only appear when associated domain data exists.
pub when_filled: Option<Vec<String>>,
/// Second target for dual decomposition: one EDIFACT value → two companion fields.
/// Used when a code encodes two concepts (e.g., NAD qualifier = partnerrolle + datenqualitaet).
/// Forward: map raw value via `also_enum_map`, store as `also_target`.
/// Reverse: joint lookup — find code where both `enum_map` and `also_enum_map` match.
pub also_target: Option<String>,
/// Enum map for the `also_target` field. Same code keys as `enum_map`.
pub also_enum_map: Option<BTreeMap<String, String>>,
}
/// Reference to a complex handler function.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplexHandlerRef {
pub name: String,
pub description: Option<String>,
}
impl MappingDefinition {
/// Normalize all EDIFACT ID paths to numeric indices using the given resolver.
///
/// Resolves named paths in field keys and discriminators.
/// Already-numeric paths pass through unchanged.
pub fn normalize_paths(&mut self, resolver: &PathResolver) {
// Normalize discriminator
if let Some(ref disc) = self.meta.discriminator {
self.meta.discriminator = Some(resolver.resolve_discriminator(disc));
}
// Normalize field keys
self.fields = self
.fields
.iter()
.map(|(k, v)| (resolver.resolve_path(k), v.clone()))
.collect();
}
/// Validate the definition for common misconfigurations.
/// Returns a list of warning messages (empty if valid).
pub fn validate(&self) -> Vec<String> {
Vec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_when_filled_deserialization() {
let toml = r#"
[meta]
entity = "Test"
bo4e_type = "Test"
source_group = "SG4.SG8.SG10"
[fields]
"cci.d7059" = { target = "", default = "Z83", when_filled = ["merkmal.code"] }
"cav.c889.d7111" = "merkmal.code"
"#;
let def: MappingDefinition = toml::from_str(toml).unwrap();
let cci = &def.fields["cci.d7059"];
match cci {
FieldMapping::Structured(s) => {
assert_eq!(s.target, "");
assert_eq!(s.default.as_deref(), Some("Z83"));
let wf = s.when_filled.as_ref().unwrap();
assert_eq!(wf, &vec!["merkmal.code".to_string()]);
}
_ => panic!("expected Structured variant"),
}
}
#[test]
fn test_validate_no_warnings_for_clean_def() {
let toml = r#"
[meta]
entity = "Test"
bo4e_type = "Test"
source_group = "SG4"
[fields]
"loc.1.0" = "marktlokationsId"
"loc.0.0" = { target = "", default = "Z16", when_filled = ["marktlokationsId"] }
"#;
let def: MappingDefinition = toml::from_str(toml).unwrap();
let warnings = def.validate();
assert!(warnings.is_empty());
}
#[test]
fn test_when_filled_absent_is_none() {
let toml = r#"
[meta]
entity = "Test"
bo4e_type = "Test"
source_group = "SG4"
[fields]
"loc.d3227" = { target = "", default = "Z16" }
"#;
let def: MappingDefinition = toml::from_str(toml).unwrap();
let loc = &def.fields["loc.d3227"];
match loc {
FieldMapping::Structured(s) => {
assert!(s.when_filled.is_none());
}
_ => panic!("expected Structured variant"),
}
}
}