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/// The repetitions of `full` that `reduced` no longer has, by path.
69///
70/// Where a group has as many repetitions in both, they are paired in order and
71/// the pairs that differ are searched for a removal further down. Where
72/// `reduced` has fewer, they are aligned in order: a repetition of `full` that
73/// matches the next one of `reduced` is kept, the others are removed. Groups
74/// nested in a removed repetition are not listed separately.
75fn removed_instances(
76    full: &[AssembledGroup],
77    reduced: &[AssembledGroup],
78    prefix: &mut InstancePath,
79    out: &mut Vec<InstancePath>,
80) {
81    for group in full {
82        let remaining: &[AssembledGroupInstance] = reduced
83            .iter()
84            .find(|g| g.group_id == group.group_id)
85            .map_or(&[], |g| &g.repetitions);
86        let repetitions = &group.repetitions;
87        if repetitions.len() == remaining.len() {
88            for (i, (a, b)) in repetitions.iter().zip(remaining).enumerate() {
89                if a != b {
90                    prefix.push((group.group_id.clone(), i));
91                    removed_instances(&a.child_groups, &b.child_groups, prefix, out);
92                    prefix.pop();
93                }
94            }
95            continue;
96        }
97        // If the rest does not line up (a repetition changed as well), more
98        // than `full - reduced` repetitions go unmatched — at least two — so a
99        // misalignment never passes for a single removal.
100        let mut next = 0;
101        for (i, repetition) in repetitions.iter().enumerate() {
102            if remaining.get(next) == Some(repetition) {
103                next += 1;
104            } else {
105                let mut path = prefix.clone();
106                path.push((group.group_id.clone(), i));
107                out.push(path);
108            }
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use mig_assembly::assembler::AssembledSegment;
117
118    fn rep(seq: &str) -> AssembledGroupInstance {
119        AssembledGroupInstance {
120            segments: vec![AssembledSegment {
121                tag: "SEQ".to_string(),
122                elements: vec![vec![seq.to_string()]],
123                mig_number: None,
124                segment_number: None,
125            }],
126            child_groups: vec![],
127            entry_mig_number: None,
128            variant_mig_numbers: vec![],
129            skipped_segments: vec![],
130            skipped_positions: Vec::new(),
131        }
132    }
133
134    fn sg8(seqs: &[&str]) -> Vec<AssembledGroup> {
135        vec![AssembledGroup {
136            group_id: "SG8".to_string(),
137            repetitions: seqs.iter().map(|s| rep(s)).collect(),
138        }]
139    }
140
141    fn removed(full: &[&str], reduced: &[&str]) -> Vec<InstancePath> {
142        let mut out = Vec::new();
143        removed_instances(&sg8(full), &sg8(reduced), &mut Vec::new(), &mut out);
144        out
145    }
146
147    #[test]
148    fn the_missing_repetition_is_found_by_order() {
149        assert_eq!(
150            removed(&["Z01", "Z03", "Z20"], &["Z01", "Z20"]),
151            [vec![("SG8".to_string(), 1)]]
152        );
153    }
154
155    #[test]
156    fn a_reduced_tree_that_does_not_line_up_names_no_single_instance() {
157        let out = removed(&["Z01", "Z03", "Z20"], &["Z01", "Z99"]);
158        assert_ne!(out.len(), 1, "{out:?}");
159    }
160}