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 if let Some(path) = element_instance(&tree.groups, &reduced.groups) {
59 scopes.insert((key.clone(), index), path);
60 }
61 }
62 }
63 scopes
64}
65
66pub type NestedKey = (ElementKey, Vec<(String, usize)>);
69
70pub 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#[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
167fn escape(key: &str) -> String {
169 key.replace('~', "~0").replace('/', "~1")
170}
171
172fn 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
189fn 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
221fn 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 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 #[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 #[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 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}