use std::collections::HashMap;
use mig_assembly::assembler::{AssembledGroup, AssembledGroupInstance, AssembledTree};
use mig_bo4e::MappingEngine;
use serde_json::Value;
pub type ElementKey = (String, Option<usize>);
pub type InstancePath = Vec<(String, usize)>;
pub fn entity_element_scopes(
engine: &MappingEngine,
json: &Value,
tree: &AssembledTree,
) -> HashMap<ElementKey, InstancePath> {
let mut scopes = HashMap::new();
let Some(entities) = json.as_object() else {
return scopes;
};
for (key, value) in entities {
let indices: Vec<Option<usize>> = match value.as_array() {
Some(elements) => (0..elements.len()).map(Some).collect(),
None => vec![None],
};
for index in indices {
let mut without = json.clone();
match index {
Some(i) => {
without[key.as_str()]
.as_array_mut()
.expect("array")
.remove(i);
}
None => {
without.as_object_mut().expect("object").remove(key);
}
}
let reduced = engine.map_all_reverse(&without, None);
let mut removed = Vec::new();
removed_instances(&tree.groups, &reduced.groups, &mut Vec::new(), &mut removed);
if let [path] = &removed[..] {
scopes.insert((key.clone(), index), path.clone());
}
}
}
scopes
}
pub type NestedKey = (ElementKey, Vec<(String, usize)>);
pub fn nested_element_scopes(
engine: &MappingEngine,
json: &Value,
tree: &AssembledTree,
) -> HashMap<NestedKey, InstancePath> {
let mut scopes = HashMap::new();
let Some(entities) = json.as_object() else {
return scopes;
};
for (key, value) in entities {
let elements: Vec<(Option<usize>, &Value)> = match value.as_array() {
Some(items) => items
.iter()
.enumerate()
.map(|(i, v)| (Some(i), v))
.collect(),
None => vec![(None, value)],
};
for (index, element) in elements {
let mut pointer = format!("/{}", escape(key));
if let Some(i) = index {
pointer.push_str(&format!("/{i}"));
}
let element_key: ElementKey = (key.clone(), index);
collect_nested(
engine,
json,
tree,
element,
&pointer,
&mut Vec::new(),
&element_key,
&mut scopes,
);
}
}
scopes
}
#[allow(clippy::too_many_arguments)]
fn collect_nested(
engine: &MappingEngine,
json: &Value,
tree: &AssembledTree,
value: &Value,
pointer: &str,
position: &mut Vec<(String, usize)>,
element_key: &ElementKey,
scopes: &mut HashMap<NestedKey, InstancePath>,
) {
let Some(fields) = value.as_object() else {
return;
};
for (field, list) in fields {
let Some(items) = list.as_array() else {
continue;
};
if !items.iter().any(Value::is_object) {
continue;
}
let list_pointer = format!("{pointer}/{}", escape(field));
for (j, item) in items.iter().enumerate() {
let mut without = json.clone();
if let Some(Value::Array(target)) = without.pointer_mut(&list_pointer) {
target.remove(j);
}
let reduced = engine.map_all_reverse(&without, None);
let mut removed = Vec::new();
removed_instances(&tree.groups, &reduced.groups, &mut Vec::new(), &mut removed);
position.push((field.clone(), j));
if let [path] = &removed[..] {
scopes.insert((element_key.clone(), position.clone()), path.clone());
}
collect_nested(
engine,
json,
tree,
item,
&format!("{list_pointer}/{j}"),
position,
element_key,
scopes,
);
position.pop();
}
}
}
fn escape(key: &str) -> String {
key.replace('~', "~0").replace('/', "~1")
}
fn removed_instances(
full: &[AssembledGroup],
reduced: &[AssembledGroup],
prefix: &mut InstancePath,
out: &mut Vec<InstancePath>,
) {
for group in full {
let remaining: &[AssembledGroupInstance] = reduced
.iter()
.find(|g| g.group_id == group.group_id)
.map_or(&[], |g| &g.repetitions);
let repetitions = &group.repetitions;
if repetitions.len() == remaining.len() {
for (i, (a, b)) in repetitions.iter().zip(remaining).enumerate() {
if a != b {
prefix.push((group.group_id.clone(), i));
removed_instances(&a.child_groups, &b.child_groups, prefix, out);
prefix.pop();
}
}
continue;
}
let mut next = 0;
for (i, repetition) in repetitions.iter().enumerate() {
if remaining.get(next) == Some(repetition) {
next += 1;
} else {
let mut path = prefix.clone();
path.push((group.group_id.clone(), i));
out.push(path);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use mig_assembly::assembler::AssembledSegment;
fn rep(seq: &str) -> AssembledGroupInstance {
AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "SEQ".to_string(),
elements: vec![vec![seq.to_string()]],
mig_number: None,
segment_number: None,
}],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: Vec::new(),
}
}
fn sg8(seqs: &[&str]) -> Vec<AssembledGroup> {
vec![AssembledGroup {
group_id: "SG8".to_string(),
repetitions: seqs.iter().map(|s| rep(s)).collect(),
}]
}
fn removed(full: &[&str], reduced: &[&str]) -> Vec<InstancePath> {
let mut out = Vec::new();
removed_instances(&sg8(full), &sg8(reduced), &mut Vec::new(), &mut out);
out
}
#[test]
fn the_missing_repetition_is_found_by_order() {
assert_eq!(
removed(&["Z01", "Z03", "Z20"], &["Z01", "Z20"]),
[vec![("SG8".to_string(), 1)]]
);
}
#[test]
fn a_reduced_tree_that_does_not_line_up_names_no_single_instance() {
let out = removed(&["Z01", "Z03", "Z20"], &["Z01", "Z99"]);
assert_ne!(out.len(), 1, "{out:?}");
}
}