1use std::collections::HashMap;
9
10use mig_assembly::assembler::{AssembledGroup, AssembledGroupInstance, AssembledTree};
11use mig_bo4e::MappingEngine;
12use serde_json::Value;
13
14pub type ElementKey = (String, Option<usize>);
17
18pub type InstancePath = Vec<(String, usize)>;
20
21pub 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
68pub type NestedKey = (ElementKey, Vec<(String, usize)>);
71
72pub 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#[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
169fn escape(key: &str) -> String {
171 key.replace('~', "~0").replace('/', "~1")
172}
173
174fn 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 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}