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            let mut removed = Vec::new();
59            removed_instances(&tree.groups, &reduced.groups, &mut Vec::new(), &mut removed);
60            if let [path] = &removed[..] {
61                scopes.insert((key.clone(), index), path.clone());
62            }
63        }
64    }
65    scopes
66}
67
68/// A nested-list element: its entity element, and its position in the lists
69/// below it (`[("zuordnungen", 3)]`).
70pub type NestedKey = (ElementKey, Vec<(String, usize)>);
71
72/// The group instance each element of a nested list becomes: a
73/// `zuordnungen` element its SG10, a `mengen` element its SG9 — so "im selben
74/// CCI" ([438]) reads that element's CCI, not any in the message.
75///
76/// Found by removal, as for [`entity_element_scopes`], for every array of
77/// objects in an entity element and, recursively, in its elements. An element
78/// whose removal takes away no repetition (`werte[]`: the CAVs of one SG10)
79/// or several gets no entry.
80pub fn nested_element_scopes(
81    engine: &MappingEngine,
82    json: &Value,
83    tree: &AssembledTree,
84) -> HashMap<NestedKey, InstancePath> {
85    let mut scopes = HashMap::new();
86    let Some(entities) = json.as_object() else {
87        return scopes;
88    };
89    for (key, value) in entities {
90        let elements: Vec<(Option<usize>, &Value)> = match value.as_array() {
91            Some(items) => items
92                .iter()
93                .enumerate()
94                .map(|(i, v)| (Some(i), v))
95                .collect(),
96            None => vec![(None, value)],
97        };
98        for (index, element) in elements {
99            let mut pointer = format!("/{}", escape(key));
100            if let Some(i) = index {
101                pointer.push_str(&format!("/{i}"));
102            }
103            let element_key: ElementKey = (key.clone(), index);
104            collect_nested(
105                engine,
106                json,
107                tree,
108                element,
109                &pointer,
110                &mut Vec::new(),
111                &element_key,
112                &mut scopes,
113            );
114        }
115    }
116    scopes
117}
118
119/// The scopes of the list elements in `value` (at JSON `pointer`), recursively.
120#[allow(clippy::too_many_arguments)]
121fn collect_nested(
122    engine: &MappingEngine,
123    json: &Value,
124    tree: &AssembledTree,
125    value: &Value,
126    pointer: &str,
127    position: &mut Vec<(String, usize)>,
128    element_key: &ElementKey,
129    scopes: &mut HashMap<NestedKey, InstancePath>,
130) {
131    let Some(fields) = value.as_object() else {
132        return;
133    };
134    for (field, list) in fields {
135        let Some(items) = list.as_array() else {
136            continue;
137        };
138        if !items.iter().any(Value::is_object) {
139            continue;
140        }
141        let list_pointer = format!("{pointer}/{}", escape(field));
142        for (j, item) in items.iter().enumerate() {
143            let mut without = json.clone();
144            if let Some(Value::Array(target)) = without.pointer_mut(&list_pointer) {
145                target.remove(j);
146            }
147            let reduced = engine.map_all_reverse(&without, None);
148            let mut removed = Vec::new();
149            removed_instances(&tree.groups, &reduced.groups, &mut Vec::new(), &mut removed);
150            position.push((field.clone(), j));
151            if let [path] = &removed[..] {
152                scopes.insert((element_key.clone(), position.clone()), path.clone());
153            }
154            collect_nested(
155                engine,
156                json,
157                tree,
158                item,
159                &format!("{list_pointer}/{j}"),
160                position,
161                element_key,
162                scopes,
163            );
164            position.pop();
165        }
166    }
167}
168
169/// A key as a JSON pointer token.
170fn escape(key: &str) -> String {
171    key.replace('~', "~0").replace('/', "~1")
172}
173
174/// The repetitions of `full` that `reduced` no longer has, by path.
175///
176/// Where a group has as many repetitions in both, they are paired in order and
177/// the pairs that differ are searched for a removal further down. Where
178/// `reduced` has fewer, they are aligned in order: a repetition of `full` that
179/// matches the next one of `reduced` is kept, the others are removed. Groups
180/// nested in a removed repetition are not listed separately.
181fn removed_instances(
182    full: &[AssembledGroup],
183    reduced: &[AssembledGroup],
184    prefix: &mut InstancePath,
185    out: &mut Vec<InstancePath>,
186) {
187    for group in full {
188        let remaining: &[AssembledGroupInstance] = reduced
189            .iter()
190            .find(|g| g.group_id == group.group_id)
191            .map_or(&[], |g| &g.repetitions);
192        let repetitions = &group.repetitions;
193        if repetitions.len() == remaining.len() {
194            for (i, (a, b)) in repetitions.iter().zip(remaining).enumerate() {
195                if a != b {
196                    prefix.push((group.group_id.clone(), i));
197                    removed_instances(&a.child_groups, &b.child_groups, prefix, out);
198                    prefix.pop();
199                }
200            }
201            continue;
202        }
203        // If the rest does not line up (a repetition changed as well), more
204        // than `full - reduced` repetitions go unmatched — at least two — so a
205        // misalignment never passes for a single removal.
206        let mut next = 0;
207        for (i, repetition) in repetitions.iter().enumerate() {
208            if remaining.get(next) == Some(repetition) {
209                next += 1;
210            } else {
211                let mut path = prefix.clone();
212                path.push((group.group_id.clone(), i));
213                out.push(path);
214            }
215        }
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use mig_assembly::assembler::AssembledSegment;
223
224    fn rep(seq: &str) -> AssembledGroupInstance {
225        AssembledGroupInstance {
226            segments: vec![AssembledSegment {
227                tag: "SEQ".to_string(),
228                elements: vec![vec![seq.to_string()]],
229                mig_number: None,
230                segment_number: None,
231            }],
232            child_groups: vec![],
233            entry_mig_number: None,
234            variant_mig_numbers: vec![],
235            skipped_segments: vec![],
236            skipped_positions: Vec::new(),
237        }
238    }
239
240    fn sg8(seqs: &[&str]) -> Vec<AssembledGroup> {
241        vec![AssembledGroup {
242            group_id: "SG8".to_string(),
243            repetitions: seqs.iter().map(|s| rep(s)).collect(),
244        }]
245    }
246
247    fn removed(full: &[&str], reduced: &[&str]) -> Vec<InstancePath> {
248        let mut out = Vec::new();
249        removed_instances(&sg8(full), &sg8(reduced), &mut Vec::new(), &mut out);
250        out
251    }
252
253    #[test]
254    fn the_missing_repetition_is_found_by_order() {
255        assert_eq!(
256            removed(&["Z01", "Z03", "Z20"], &["Z01", "Z20"]),
257            [vec![("SG8".to_string(), 1)]]
258        );
259    }
260
261    #[test]
262    fn a_reduced_tree_that_does_not_line_up_names_no_single_instance() {
263        let out = removed(&["Z01", "Z03", "Z20"], &["Z01", "Z99"]);
264        assert_ne!(out.len(), 1, "{out:?}");
265    }
266}