Skip to main content

edifact_mapper/
element_scopes.rs

1//! Which group instance each BO4E entity element becomes.
2//!
3//! Condition-aware BO4E validation evaluates AHB conditions against the tree
4//! the JSON reverse-maps to. "In dieser SG8" needs the SG8 an element *is*,
5//! and the JSON does not say: the reverse merges definitions and orders
6//! repetitions by MIG variant, so array index and repetition index differ.
7
8use std::collections::HashMap;
9
10use mig_assembly::assembler::{AssembledGroup, AssembledGroupInstance, AssembledTree};
11use mig_bo4e::MappingEngine;
12use serde_json::Value;
13
14/// An element of the BO4E JSON: its entity key, and its index when the
15/// entity is an array (`None` for a single object).
16pub type ElementKey = (String, Option<usize>);
17
18/// A group instance, by its repetition at every level from the root.
19pub type InstancePath = Vec<(String, usize)>;
20
21/// The group instance each entity element of `json` becomes in `tree` (the
22/// reverse mapping of `json` by `engine`).
23///
24/// Found by removal: the element is taken out of the JSON, the rest reverse-
25/// mapped again, and the repetition that is no longer there is the element's.
26/// Only an element whose removal takes away exactly one repetition (with the
27/// groups nested in it) gets an entry. One that spans several — or none, when
28/// it only contributes fields to a repetition other elements also fill — gets
29/// none, and its conditions are evaluated message-wide, as without a scope.
30pub fn entity_element_scopes(
31    engine: &MappingEngine,
32    json: &Value,
33    tree: &AssembledTree,
34) -> HashMap<ElementKey, InstancePath> {
35    let mut scopes = HashMap::new();
36    let Some(entities) = json.as_object() else {
37        return scopes;
38    };
39    for (key, value) in entities {
40        let indices: Vec<Option<usize>> = match value.as_array() {
41            Some(elements) => (0..elements.len()).map(Some).collect(),
42            None => vec![None],
43        };
44        for index in indices {
45            let mut without = json.clone();
46            match index {
47                Some(i) => {
48                    without[key.as_str()]
49                        .as_array_mut()
50                        .expect("array")
51                        .remove(i);
52                }
53                None => {
54                    without.as_object_mut().expect("object").remove(key);
55                }
56            }
57            let reduced = engine.map_all_reverse(&without, None);
58            if let Some(path) = element_instance(&tree.groups, &reduced.groups) {
59                scopes.insert((key.clone(), index), path);
60            }
61        }
62    }
63    scopes
64}
65
66/// A nested-list element: its entity element, and its position in the lists
67/// below it (`[("zuordnungen", 3)]`).
68pub type NestedKey = (ElementKey, Vec<(String, usize)>);
69
70/// The group instance each element of a nested list becomes: a
71/// `zuordnungen` element its SG10, a `mengen` element its SG9 — so "im selben
72/// CCI" ([438]) reads that element's CCI, not any in the message.
73///
74/// Found by removal, as for [`entity_element_scopes`], for every array of
75/// objects in an entity element and, recursively, in its elements. An element
76/// whose removal takes away no repetition (`werte[]`: the CAVs of one SG10)
77/// or several gets no entry.
78pub fn nested_element_scopes(
79    engine: &MappingEngine,
80    json: &Value,
81    tree: &AssembledTree,
82) -> HashMap<NestedKey, InstancePath> {
83    let mut scopes = HashMap::new();
84    let Some(entities) = json.as_object() else {
85        return scopes;
86    };
87    for (key, value) in entities {
88        let elements: Vec<(Option<usize>, &Value)> = match value.as_array() {
89            Some(items) => items
90                .iter()
91                .enumerate()
92                .map(|(i, v)| (Some(i), v))
93                .collect(),
94            None => vec![(None, value)],
95        };
96        for (index, element) in elements {
97            let mut pointer = format!("/{}", escape(key));
98            if let Some(i) = index {
99                pointer.push_str(&format!("/{i}"));
100            }
101            let element_key: ElementKey = (key.clone(), index);
102            collect_nested(
103                engine,
104                json,
105                tree,
106                element,
107                &pointer,
108                &mut Vec::new(),
109                &element_key,
110                &mut scopes,
111            );
112        }
113    }
114    scopes
115}
116
117/// The scopes of the list elements in `value` (at JSON `pointer`), recursively.
118#[allow(clippy::too_many_arguments)]
119fn collect_nested(
120    engine: &MappingEngine,
121    json: &Value,
122    tree: &AssembledTree,
123    value: &Value,
124    pointer: &str,
125    position: &mut Vec<(String, usize)>,
126    element_key: &ElementKey,
127    scopes: &mut HashMap<NestedKey, InstancePath>,
128) {
129    let Some(fields) = value.as_object() else {
130        return;
131    };
132    for (field, list) in fields {
133        let Some(items) = list.as_array() else {
134            continue;
135        };
136        if !items.iter().any(Value::is_object) {
137            continue;
138        }
139        let list_pointer = format!("{pointer}/{}", escape(field));
140        for (j, item) in items.iter().enumerate() {
141            let mut without = json.clone();
142            if let Some(Value::Array(target)) = without.pointer_mut(&list_pointer) {
143                target.remove(j);
144            }
145            let reduced = engine.map_all_reverse(&without, None);
146            let mut removed = Vec::new();
147            removed_instances(&tree.groups, &reduced.groups, &mut Vec::new(), &mut removed);
148            position.push((field.clone(), j));
149            if let [path] = &removed[..] {
150                scopes.insert((element_key.clone(), position.clone()), path.clone());
151            }
152            collect_nested(
153                engine,
154                json,
155                tree,
156                item,
157                &format!("{list_pointer}/{j}"),
158                position,
159                element_key,
160                scopes,
161            );
162            position.pop();
163        }
164    }
165}
166
167/// A key as a JSON pointer token.
168fn escape(key: &str) -> String {
169    key.replace('~', "~0").replace('/', "~1")
170}
171
172/// The instance an element is, from the tree with it (`full`) and without it
173/// (`reduced`): the one repetition its removal takes away — or, when it fills
174/// segments of an instance others fill too (Prozessdaten's IDE and STS beside
175/// the Freitext's FTX in one SG4), the one instance that lost segments, so
176/// long as every repetition that went lies inside it (Prozessdaten's SG6).
177fn element_instance(full: &[AssembledGroup], reduced: &[AssembledGroup]) -> Option<InstancePath> {
178    let mut removed = Vec::new();
179    removed_instances(full, reduced, &mut Vec::new(), &mut removed);
180    let mut changed = Vec::new();
181    changed_instances(full, reduced, &mut Vec::new(), &mut changed);
182    match (&removed[..], &changed[..]) {
183        ([path], []) => Some(path.clone()),
184        (_, [path]) if removed.iter().all(|r| r.starts_with(path)) => Some(path.clone()),
185        _ => None,
186    }
187}
188
189/// The instances present in both trees whose own segments differ — where an
190/// element that takes no repetition away had its data. Instances whose
191/// repetitions do not line up are left to [`removed_instances`].
192fn changed_instances(
193    full: &[AssembledGroup],
194    reduced: &[AssembledGroup],
195    prefix: &mut InstancePath,
196    out: &mut Vec<InstancePath>,
197) {
198    for group in full {
199        let Some(remaining) = reduced.iter().find(|g| g.group_id == group.group_id) else {
200            continue;
201        };
202        if remaining.repetitions.len() != group.repetitions.len() {
203            continue;
204        }
205        for (i, (a, b)) in group
206            .repetitions
207            .iter()
208            .zip(&remaining.repetitions)
209            .enumerate()
210        {
211            prefix.push((group.group_id.clone(), i));
212            if a.segments != b.segments {
213                out.push(prefix.clone());
214            }
215            changed_instances(&a.child_groups, &b.child_groups, prefix, out);
216            prefix.pop();
217        }
218    }
219}
220
221/// The repetitions of `full` that `reduced` no longer has, by path.
222///
223/// Where a group has as many repetitions in both, they are paired in order and
224/// the pairs that differ are searched for a removal further down. Where
225/// `reduced` has fewer, they are aligned in order: a repetition of `full` that
226/// matches the next one of `reduced` is kept, the others are removed. Groups
227/// nested in a removed repetition are not listed separately.
228fn removed_instances(
229    full: &[AssembledGroup],
230    reduced: &[AssembledGroup],
231    prefix: &mut InstancePath,
232    out: &mut Vec<InstancePath>,
233) {
234    for group in full {
235        let remaining: &[AssembledGroupInstance] = reduced
236            .iter()
237            .find(|g| g.group_id == group.group_id)
238            .map_or(&[], |g| &g.repetitions);
239        let repetitions = &group.repetitions;
240        if repetitions.len() == remaining.len() {
241            for (i, (a, b)) in repetitions.iter().zip(remaining).enumerate() {
242                if a != b {
243                    prefix.push((group.group_id.clone(), i));
244                    removed_instances(&a.child_groups, &b.child_groups, prefix, out);
245                    prefix.pop();
246                }
247            }
248            continue;
249        }
250        // If the rest does not line up (a repetition changed as well), more
251        // than `full - reduced` repetitions go unmatched — at least two — so a
252        // misalignment never passes for a single removal.
253        let mut next = 0;
254        for (i, repetition) in repetitions.iter().enumerate() {
255            if remaining.get(next) == Some(repetition) {
256                next += 1;
257            } else {
258                let mut path = prefix.clone();
259                path.push((group.group_id.clone(), i));
260                out.push(path);
261            }
262        }
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use mig_assembly::assembler::AssembledSegment;
270
271    fn rep(seq: &str) -> AssembledGroupInstance {
272        AssembledGroupInstance {
273            segments: vec![AssembledSegment {
274                tag: "SEQ".to_string(),
275                elements: vec![vec![seq.to_string()]],
276                mig_number: None,
277                segment_number: None,
278            }],
279            child_groups: vec![],
280            entry_mig_number: None,
281            variant_mig_numbers: vec![],
282            skipped_segments: vec![],
283            skipped_positions: Vec::new(),
284        }
285    }
286
287    fn sg8(seqs: &[&str]) -> Vec<AssembledGroup> {
288        vec![AssembledGroup {
289            group_id: "SG8".to_string(),
290            repetitions: seqs.iter().map(|s| rep(s)).collect(),
291        }]
292    }
293
294    fn removed(full: &[&str], reduced: &[&str]) -> Vec<InstancePath> {
295        let mut out = Vec::new();
296        removed_instances(&sg8(full), &sg8(reduced), &mut Vec::new(), &mut out);
297        out
298    }
299
300    #[test]
301    fn the_missing_repetition_is_found_by_order() {
302        assert_eq!(
303            removed(&["Z01", "Z03", "Z20"], &["Z01", "Z20"]),
304            [vec![("SG8".to_string(), 1)]]
305        );
306    }
307
308    /// An element that fills segments of an instance other elements fill
309    /// too (55003's FTX+ACB `Freitext` in the SG4 that also holds IDE and
310    /// STS) takes no repetition away; the instance that lost segments is its.
311    #[test]
312    fn an_element_of_the_instances_own_segments_is_found_by_them() {
313        let mut full = sg8(&["Z01", "Z03"]);
314        full[0].repetitions[1].segments.push(AssembledSegment {
315            tag: "FTX".to_string(),
316            elements: vec![vec!["ACB".to_string()]],
317            mig_number: None,
318            segment_number: None,
319        });
320        let reduced = sg8(&["Z01", "Z03"]);
321        let mut out = Vec::new();
322        removed_instances(&full, &reduced, &mut Vec::new(), &mut out);
323        assert!(out.is_empty(), "{out:?}");
324        let mut changed = Vec::new();
325        changed_instances(&full, &reduced, &mut Vec::new(), &mut changed);
326        assert_eq!(changed, [vec![("SG8".to_string(), 1)]]);
327    }
328
329    /// Prozessdaten fills the SG4's own segments and an SG6 inside it: the
330    /// SG4 is its instance.
331    #[test]
332    fn an_element_of_an_instance_and_a_group_inside_it_is_that_instance() {
333        let with_child = |seqs: &[&str]| {
334            let mut groups = sg8(&["Z01"]);
335            groups[0].repetitions[0].child_groups = sg8(seqs);
336            groups[0].repetitions[0].child_groups[0].group_id = "SG10".to_string();
337            groups
338        };
339        let mut full = with_child(&["A", "B"]);
340        full[0].repetitions[0].segments.push(AssembledSegment {
341            tag: "FTX".to_string(),
342            elements: vec![],
343            mig_number: None,
344            segment_number: None,
345        });
346        assert_eq!(
347            element_instance(&full, &with_child(&["A"])),
348            Some(vec![("SG8".to_string(), 0)])
349        );
350        // A repetition that went without its parent losing anything is still
351        // the repetition.
352        assert_eq!(
353            element_instance(&with_child(&["A", "B"]), &with_child(&["A"])),
354            Some(vec![("SG8".to_string(), 0), ("SG10".to_string(), 1)])
355        );
356    }
357
358    #[test]
359    fn a_reduced_tree_that_does_not_line_up_names_no_single_instance() {
360        let out = removed(&["Z01", "Z03", "Z20"], &["Z01", "Z99"]);
361        assert_ne!(out.len(), 1, "{out:?}");
362    }
363}