1use mig_types::schema::mig::{MigSchema, MigSegment, MigSegmentGroup};
10
11use crate::definition::{FieldMapping, MappingDefinition};
12
13pub struct Bo4eFieldIndex {
15 entries: Vec<IndexEntry>,
16}
17
18struct IndexEntry {
19 edifact_prefix: String,
21 entity: String,
23 location: FieldLocation,
25 companion_type: Option<String>,
27 fields: Vec<FieldEntry>,
29}
30
31#[derive(Clone, Copy)]
32enum FieldLocation {
33 Stammdaten,
34}
35
36struct FieldEntry {
37 edifact_path: String,
39 bo4e_field: String,
41 is_companion: bool,
43}
44
45impl Bo4eFieldIndex {
46 pub fn build(definitions: &[MappingDefinition], mig: &MigSchema) -> Self {
52 let mut entries = Vec::new();
53
54 for def in definitions {
55 let group_path = source_group_to_slash(&def.meta.source_group);
56 let location = classify_entity(&def.meta.entity);
57 let companion_type = def.meta.companion_type.clone();
58
59 let mut fields = Vec::new();
60
61 Self::collect_fields(&def.fields, &group_path, mig, false, &mut fields);
63
64 if let Some(ref companion) = def.companion_fields {
66 Self::collect_fields(companion, &group_path, mig, true, &mut fields);
67 }
68
69 if !fields.is_empty() {
70 entries.push(IndexEntry {
71 edifact_prefix: group_path.clone(),
72 entity: def.meta.entity.clone(),
73 location,
74 companion_type,
75 fields,
76 });
77 }
78 }
79
80 Self { entries }
81 }
82
83 pub fn resolve(&self, edifact_field_path: &str) -> Option<String> {
85 for entry in &self.entries {
87 for field in &entry.fields {
88 if field.edifact_path == edifact_field_path {
89 return Some(self.build_bo4e_path(entry, field));
90 }
91 }
92 }
93 let mut best: Option<&IndexEntry> = None;
95 for entry in &self.entries {
96 if !entry.edifact_prefix.is_empty()
97 && edifact_field_path.starts_with(&entry.edifact_prefix)
98 && best
99 .map(|b| entry.edifact_prefix.len() > b.edifact_prefix.len())
100 .unwrap_or(true)
101 {
102 best = Some(entry);
103 }
104 }
105 best.map(|entry| self.build_entity_path(entry))
106 }
107
108 fn collect_fields(
109 field_map: &indexmap::IndexMap<String, FieldMapping>,
110 group_path: &str,
111 mig: &MigSchema,
112 is_companion: bool,
113 out: &mut Vec<FieldEntry>,
114 ) {
115 for (toml_path, mapping) in field_map {
116 let target = match mapping {
117 FieldMapping::Simple(s) => s.as_str(),
118 FieldMapping::Structured(s) => s.target.as_str(),
119 FieldMapping::Nested(_) => continue,
120 };
121
122 if target.is_empty() {
124 continue;
125 }
126
127 let parsed = match parse_toml_path(toml_path) {
130 Some(p) => p,
131 None => continue,
132 };
133
134 if let Some(edifact_path) = resolve_edifact_path(group_path, &parsed, mig) {
136 out.push(FieldEntry {
137 edifact_path,
138 bo4e_field: target.to_string(),
139 is_companion,
140 });
141 }
142 }
143 }
144
145 fn build_bo4e_path(&self, entry: &IndexEntry, field: &FieldEntry) -> String {
146 let location = match entry.location {
147 FieldLocation::Stammdaten => "stammdaten",
148 };
149 if field.is_companion {
150 if let Some(ref ct) = entry.companion_type {
151 format!(
152 "{}.{}.{}.{}",
153 location,
154 entry.entity,
155 to_camel_first_lower(ct),
156 field.bo4e_field
157 )
158 } else {
159 format!("{}.{}.{}", location, entry.entity, field.bo4e_field)
160 }
161 } else {
162 format!("{}.{}.{}", location, entry.entity, field.bo4e_field)
163 }
164 }
165
166 fn build_entity_path(&self, entry: &IndexEntry) -> String {
167 let location = match entry.location {
168 FieldLocation::Stammdaten => "stammdaten",
169 };
170 format!("{}.{}", location, entry.entity)
171 }
172}
173
174struct ParsedTomlPath {
176 segment_tag: String,
178 element_idx: usize,
180 component_idx: Option<usize>,
182}
183
184fn parse_toml_path(path: &str) -> Option<ParsedTomlPath> {
186 let parts: Vec<&str> = path.split('.').collect();
187 if parts.len() < 2 {
188 return None;
189 }
190
191 let raw_tag = parts[0];
193 let tag = if let Some(bracket) = raw_tag.find('[') {
194 &raw_tag[..bracket]
195 } else {
196 raw_tag
197 };
198
199 let element_idx: usize = parts[1].parse().ok()?;
200 let component_idx = if parts.len() > 2 {
201 Some(parts[2].parse::<usize>().ok()?)
202 } else {
203 None
204 };
205
206 Some(ParsedTomlPath {
207 segment_tag: tag.to_uppercase(),
208 element_idx,
209 component_idx,
210 })
211}
212
213fn source_group_to_slash(source_group: &str) -> String {
216 source_group
217 .split('.')
218 .map(|part| {
219 if let Some(colon) = part.find(':') {
220 &part[..colon]
221 } else {
222 part
223 }
224 })
225 .collect::<Vec<_>>()
226 .join("/")
227}
228
229fn classify_entity(_entity: &str) -> FieldLocation {
232 FieldLocation::Stammdaten
233}
234
235fn resolve_edifact_path(
237 group_path: &str,
238 parsed: &ParsedTomlPath,
239 mig: &MigSchema,
240) -> Option<String> {
241 let segment = find_segment_in_mig(mig, group_path, &parsed.segment_tag)?;
243
244 let resolved = resolve_element_at_position(segment, parsed.element_idx, parsed.component_idx)?;
246
247 let prefix = if group_path.is_empty() {
248 parsed.segment_tag.clone()
249 } else {
250 format!("{}/{}", group_path, parsed.segment_tag)
251 };
252
253 match resolved {
254 ResolvedElement::DataElement(id) => Some(format!("{}/{}", prefix, id)),
255 ResolvedElement::CompositeElement(composite_id, element_id) => {
256 Some(format!("{}/{}/{}", prefix, composite_id, element_id))
257 }
258 }
259}
260
261enum ResolvedElement {
262 DataElement(String),
264 CompositeElement(String, String),
266}
267
268fn find_segment_in_mig<'a>(
270 mig: &'a MigSchema,
271 group_path: &str,
272 segment_tag: &str,
273) -> Option<&'a MigSegment> {
274 if group_path.is_empty() {
275 return mig
277 .segments
278 .iter()
279 .find(|s| s.id.eq_ignore_ascii_case(segment_tag));
280 }
281
282 let parts: Vec<&str> = group_path.split('/').collect();
283
284 let mut current_group = mig
286 .segment_groups
287 .iter()
288 .find(|g| g.id.eq_ignore_ascii_case(parts[0]))?;
289
290 for &part in &parts[1..] {
292 current_group = current_group
293 .nested_groups
294 .iter()
295 .find(|g| g.id.eq_ignore_ascii_case(part))?;
296 }
297
298 find_segment_in_group(current_group, segment_tag)
299}
300
301fn find_segment_in_group<'a>(
303 group: &'a MigSegmentGroup,
304 segment_tag: &str,
305) -> Option<&'a MigSegment> {
306 group
307 .segments
308 .iter()
309 .find(|s| s.id.eq_ignore_ascii_case(segment_tag))
310}
311
312fn resolve_element_at_position(
318 segment: &MigSegment,
319 element_idx: usize,
320 component_idx: Option<usize>,
321) -> Option<ResolvedElement> {
322 if let Some(composite) = segment
324 .composites
325 .iter()
326 .find(|c| c.position == element_idx)
327 {
328 let comp_idx = component_idx.unwrap_or(0);
329 let mut sub_elements: Vec<_> = composite.data_elements.iter().collect();
331 sub_elements.sort_by_key(|de| de.position);
332 let de = sub_elements.get(comp_idx)?;
333 return Some(ResolvedElement::CompositeElement(
334 composite.id.clone(),
335 de.id.clone(),
336 ));
337 }
338
339 if let Some(de) = segment
341 .data_elements
342 .iter()
343 .find(|d| d.position == element_idx)
344 {
345 return Some(ResolvedElement::DataElement(de.id.clone()));
346 }
347
348 None
349}
350
351fn to_camel_first_lower(s: &str) -> String {
353 let mut chars = s.chars();
354 match chars.next() {
355 None => String::new(),
356 Some(c) => c.to_lowercase().to_string() + chars.as_str(),
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[test]
365 fn test_source_group_to_slash() {
366 assert_eq!(source_group_to_slash("SG4.SG5"), "SG4/SG5");
367 assert_eq!(source_group_to_slash("SG4"), "SG4");
368 assert_eq!(source_group_to_slash("SG8:1.SG10"), "SG8/SG10");
369 assert_eq!(source_group_to_slash(""), "");
370 }
371
372 #[test]
373 fn test_parse_toml_path() {
374 let p = parse_toml_path("loc.1.0").unwrap();
375 assert_eq!(p.segment_tag, "LOC");
376 assert_eq!(p.element_idx, 1);
377 assert_eq!(p.component_idx, Some(0));
378
379 let p = parse_toml_path("ide.1").unwrap();
380 assert_eq!(p.segment_tag, "IDE");
381 assert_eq!(p.element_idx, 1);
382 assert_eq!(p.component_idx, None);
383
384 let p = parse_toml_path("dtm[92].0.1").unwrap();
385 assert_eq!(p.segment_tag, "DTM");
386 assert_eq!(p.element_idx, 0);
387 assert_eq!(p.component_idx, Some(1));
388
389 assert!(parse_toml_path("loc").is_none());
390 }
391
392 #[test]
393 fn test_classify_entity() {
394 assert!(matches!(
396 classify_entity("Prozessdaten"),
397 FieldLocation::Stammdaten
398 ));
399 assert!(matches!(
400 classify_entity("Nachricht"),
401 FieldLocation::Stammdaten
402 ));
403 assert!(matches!(
404 classify_entity("Marktlokation"),
405 FieldLocation::Stammdaten
406 ));
407 assert!(matches!(
408 classify_entity("Marktteilnehmer"),
409 FieldLocation::Stammdaten
410 ));
411 }
412
413 #[test]
414 fn test_to_camel_first_lower() {
415 assert_eq!(
416 to_camel_first_lower("MarktlokationEdifact"),
417 "marktlokationEdifact"
418 );
419 assert_eq!(to_camel_first_lower("Foo"), "foo");
420 assert_eq!(to_camel_first_lower(""), "");
421 }
422
423 #[test]
424 fn test_resolve_returns_none_for_unknown_path() {
425 let index = Bo4eFieldIndex { entries: vec![] };
426 assert!(index.resolve("SG99/UNKNOWN/9999").is_none());
427 }
428}