1use indexmap::IndexMap;
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10use crate::path_resolver::PathResolver;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct MappingDefinition {
15 pub meta: MappingMeta,
16 pub fields: IndexMap<String, FieldMapping>,
19 pub companion_fields: Option<IndexMap<String, FieldMapping>>,
20 pub complex_handlers: Option<Vec<ComplexHandlerRef>>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct MappingMeta {
26 pub entity: String,
27 pub bo4e_type: String,
28 pub companion_type: Option<String>,
29 pub source_group: String,
30 #[serde(default)]
34 pub source_path: Option<String>,
35 pub discriminator: Option<String>,
36 pub repeat_on_tag: Option<String>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(untagged)]
45pub enum FieldMapping {
46 Simple(String),
48 Structured(StructuredFieldMapping),
50 Nested(IndexMap<String, FieldMapping>),
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct StructuredFieldMapping {
57 pub target: String,
58 pub transform: Option<String>,
59 pub when: Option<String>,
60 pub default: Option<String>,
61 pub enum_map: Option<BTreeMap<String, String>>,
66 pub when_filled: Option<Vec<String>>,
71 pub also_target: Option<String>,
76 pub also_enum_map: Option<BTreeMap<String, String>>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ComplexHandlerRef {
83 pub name: String,
84 pub description: Option<String>,
85}
86
87impl MappingDefinition {
88 pub fn normalize_paths(&mut self, resolver: &PathResolver) {
93 if let Some(ref disc) = self.meta.discriminator {
95 self.meta.discriminator = Some(resolver.resolve_discriminator(disc));
96 }
97
98 self.fields = self
100 .fields
101 .iter()
102 .map(|(k, v)| (resolver.resolve_path(k), v.clone()))
103 .collect();
104
105 if let Some(ref cf) = self.companion_fields {
107 self.companion_fields = Some(
108 cf.iter()
109 .map(|(k, v)| (resolver.resolve_path(k), v.clone()))
110 .collect(),
111 );
112 }
113 }
114
115 pub fn validate(&self) -> Vec<String> {
118 let mut warnings = Vec::new();
119
120 let mut field_targets: Vec<&str> = Vec::new();
122 for fm in self.fields.values() {
123 if let Some(t) = extract_target_ref(fm) {
124 if !t.is_empty() {
125 field_targets.push(t);
126 }
127 }
128 }
129
130 if let Some(ref cf) = self.companion_fields {
132 for fm in cf.values() {
133 if let Some(t) = extract_target_ref(fm) {
134 if !t.is_empty() && field_targets.contains(&t) {
135 warnings.push(format!(
136 "field '{}' appears in both [fields] and [companion_fields] in entity '{}'",
137 t, self.meta.entity
138 ));
139 }
140 }
141 }
142 }
143
144 warnings
145 }
146}
147
148fn extract_target_ref(fm: &FieldMapping) -> Option<&str> {
150 match fm {
151 FieldMapping::Simple(t) => Some(t.as_str()),
152 FieldMapping::Structured(s) => Some(s.target.as_str()),
153 FieldMapping::Nested(_) => None,
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_when_filled_deserialization() {
163 let toml = r#"
164[meta]
165entity = "Test"
166bo4e_type = "Test"
167source_group = "SG4.SG8.SG10"
168
169[fields]
170
171[companion_fields]
172"cci.d7059" = { target = "", default = "Z83", when_filled = ["merkmal.code"] }
173"cav.c889.d7111" = "merkmal.code"
174"#;
175 let def: MappingDefinition = toml::from_str(toml).unwrap();
176 let cf = def.companion_fields.unwrap();
177 let cci = &cf["cci.d7059"];
178 match cci {
179 FieldMapping::Structured(s) => {
180 assert_eq!(s.target, "");
181 assert_eq!(s.default.as_deref(), Some("Z83"));
182 let wf = s.when_filled.as_ref().unwrap();
183 assert_eq!(wf, &vec!["merkmal.code".to_string()]);
184 }
185 _ => panic!("expected Structured variant"),
186 }
187 }
188
189 #[test]
190 fn test_validate_duplicate_target_warning() {
191 let toml = r#"
192[meta]
193entity = "Test"
194bo4e_type = "Test"
195companion_type = "TestEdifact"
196source_group = "SG4"
197
198[fields]
199"loc.1.0" = "someField"
200
201[companion_fields]
202"loc.0.0" = "someField"
203"#;
204 let def: MappingDefinition = toml::from_str(toml).unwrap();
205 let warnings = def.validate();
206 assert!(!warnings.is_empty());
207 assert!(warnings[0].contains("someField"));
208 }
209
210 #[test]
211 fn test_validate_no_warnings_for_clean_def() {
212 let toml = r#"
213[meta]
214entity = "Test"
215bo4e_type = "Test"
216source_group = "SG4"
217
218[fields]
219"loc.1.0" = "marktlokationsId"
220
221[companion_fields]
222"loc.0.0" = { target = "", default = "Z16", when_filled = ["marktlokationsId"] }
223"#;
224 let def: MappingDefinition = toml::from_str(toml).unwrap();
225 let warnings = def.validate();
226 assert!(warnings.is_empty());
227 }
228
229 #[test]
230 fn test_when_filled_absent_is_none() {
231 let toml = r#"
232[meta]
233entity = "Test"
234bo4e_type = "Test"
235source_group = "SG4"
236
237[fields]
238"loc.d3227" = { target = "", default = "Z16" }
239"#;
240 let def: MappingDefinition = toml::from_str(toml).unwrap();
241 let loc = &def.fields["loc.d3227"];
242 match loc {
243 FieldMapping::Structured(s) => {
244 assert!(s.when_filled.is_none());
245 }
246 _ => panic!("expected Structured variant"),
247 }
248 }
249}