use std::collections::HashMap;
use panproto_gat::Name;
use panproto_schema::{Edge, Schema};
use rustc_hash::FxHashSet;
use crate::error::InvertError;
use crate::migration::Migration;
pub fn invert(
migration: &Migration,
_src: &Schema,
tgt: &Schema,
) -> Result<Migration, InvertError> {
validate_bijectivity(migration, tgt)?;
let inv_vertex_map: HashMap<Name, Name> = migration
.vertex_map
.iter()
.map(|(s, t)| (t.clone(), s.clone()))
.collect();
let inv_edge_map: HashMap<Edge, Edge> = migration
.edge_map
.iter()
.map(|(s, t)| (t.clone(), s.clone()))
.collect();
let inv_hyper_edge_map: HashMap<Name, Name> = migration
.hyper_edge_map
.iter()
.map(|(s, t)| (t.clone(), s.clone()))
.collect();
let mut inv_label_map = HashMap::new();
for ((he_id, old_label), new_label) in &migration.label_map {
if let Some(inv_he_id) = migration.hyper_edge_map.get(he_id) {
inv_label_map.insert((inv_he_id.clone(), new_label.clone()), old_label.clone());
}
}
let mut inv_resolver = HashMap::new();
for ((src, tgt), edge) in &migration.resolver {
let Some(inv_src) = inv_vertex_map.get(src).cloned() else {
return Err(InvertError::NotBijective {
detail: format!("resolver key vertex {src} not in inverse vertex map"),
});
};
let Some(inv_tgt) = inv_vertex_map.get(tgt).cloned() else {
return Err(InvertError::NotBijective {
detail: format!("resolver key vertex {tgt} not in inverse vertex map"),
});
};
let Some(inv_edge) = inv_edge_map.get(edge).cloned() else {
return Err(InvertError::EdgeNotBijective {
detail: format!(
"resolver edge {} -> {} ({}) not in inverse edge map",
edge.src, edge.tgt, edge.kind
),
});
};
inv_resolver.insert((inv_src, inv_tgt), inv_edge);
}
let mut inv_hyper_resolver = HashMap::new();
for ((he_id, labels), (tgt_he_id, label_remap)) in &migration.hyper_resolver {
let inv_he_id = inv_hyper_edge_map.get(tgt_he_id).cloned().ok_or_else(|| {
InvertError::HyperEdgeNotBijective {
detail: format!("hyper-resolver target {tgt_he_id} not in inverse hyper-edge map"),
}
})?;
let inv_label_remap: HashMap<Name, Name> = label_remap
.iter()
.map(|(k, v)| (v.clone(), k.clone()))
.collect();
let mut inv_labels = Vec::with_capacity(labels.len());
for l in labels {
let mapped =
migration
.vertex_map
.get(l)
.cloned()
.ok_or_else(|| InvertError::NotBijective {
detail: format!("hyper-resolver label vertex {l} not in vertex map"),
})?;
inv_labels.push(mapped);
}
inv_hyper_resolver.insert((inv_he_id, inv_labels), (he_id.clone(), inv_label_remap));
}
Ok(Migration {
vertex_map: inv_vertex_map,
edge_map: inv_edge_map,
hyper_edge_map: inv_hyper_edge_map,
label_map: inv_label_map,
resolver: inv_resolver,
hyper_resolver: inv_hyper_resolver,
expr_resolvers: HashMap::new(),
})
}
fn validate_bijectivity(migration: &Migration, tgt: &Schema) -> Result<(), InvertError> {
let mut seen_targets: FxHashSet<Name> = FxHashSet::default();
for tgt_v in migration.vertex_map.values() {
if !seen_targets.insert(tgt_v.clone()) {
return Err(InvertError::NotBijective {
detail: format!("target vertex {tgt_v} has multiple preimages"),
});
}
}
let dropped: Vec<String> = tgt
.vertices
.keys()
.filter(|v| !seen_targets.contains(*v))
.map(std::string::ToString::to_string)
.collect();
if !dropped.is_empty() {
return Err(InvertError::DroppedVertices { dropped });
}
let mut seen_edge_targets: FxHashSet<Edge> = FxHashSet::default();
for tgt_e in migration.edge_map.values() {
if !seen_edge_targets.insert(tgt_e.clone()) {
return Err(InvertError::EdgeNotBijective {
detail: format!(
"target edge {} -> {} ({}) has multiple preimages",
tgt_e.src, tgt_e.tgt, tgt_e.kind
),
});
}
}
if tgt.edges.keys().any(|e| !seen_edge_targets.contains(e)) {
return Err(InvertError::DroppedEdges);
}
let mut seen_hyper_targets: FxHashSet<Name> = FxHashSet::default();
for tgt_he in migration.hyper_edge_map.values() {
if !seen_hyper_targets.insert(tgt_he.clone()) {
return Err(InvertError::HyperEdgeNotBijective {
detail: format!("target hyper-edge {tgt_he} has multiple preimages"),
});
}
}
let dropped_hyper: Vec<String> = tgt
.hyper_edges
.keys()
.filter(|he| !seen_hyper_targets.contains(*he))
.map(std::string::ToString::to_string)
.collect();
if !dropped_hyper.is_empty() {
return Err(InvertError::DroppedHyperEdges {
dropped: dropped_hyper,
});
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use panproto_schema::Vertex;
fn test_schema(vertices: &[(&str, &str)], edges: &[Edge]) -> Schema {
let mut vert_map = HashMap::new();
let mut edge_map = HashMap::new();
let mut outgoing: HashMap<Name, smallvec::SmallVec<Edge, 4>> = HashMap::new();
let mut incoming: HashMap<Name, smallvec::SmallVec<Edge, 4>> = HashMap::new();
let mut between: HashMap<(Name, Name), smallvec::SmallVec<Edge, 2>> = HashMap::new();
for (id, kind) in vertices {
vert_map.insert(
Name::from(*id),
Vertex {
id: Name::from(*id),
kind: Name::from(*kind),
nsid: None,
},
);
}
for edge in edges {
edge_map.insert(edge.clone(), edge.kind.clone());
outgoing
.entry(edge.src.clone())
.or_default()
.push(edge.clone());
incoming
.entry(edge.tgt.clone())
.or_default()
.push(edge.clone());
between
.entry((edge.src.clone(), edge.tgt.clone()))
.or_default()
.push(edge.clone());
}
Schema {
protocol: "test".into(),
vertices: vert_map,
edges: edge_map,
hyper_edges: HashMap::new(),
constraints: HashMap::new(),
required: HashMap::new(),
nsids: HashMap::new(),
entries: Vec::new(),
variants: HashMap::new(),
orderings: HashMap::new(),
recursion_points: HashMap::new(),
spans: HashMap::new(),
usage_modes: HashMap::new(),
nominal: HashMap::new(),
coercions: HashMap::new(),
mergers: HashMap::new(),
defaults: HashMap::new(),
policies: HashMap::new(),
outgoing,
incoming,
between,
}
}
#[test]
fn bijective_migration_inverts() {
let edge = Edge {
src: "a".into(),
tgt: "b".into(),
kind: "prop".into(),
name: Some("x".into()),
};
let inv_edge = Edge {
src: "A".into(),
tgt: "B".into(),
kind: "prop".into(),
name: Some("x".into()),
};
let src = test_schema(
&[("a", "object"), ("b", "string")],
std::slice::from_ref(&edge),
);
let tgt = test_schema(
&[("A", "object"), ("B", "string")],
std::slice::from_ref(&inv_edge),
);
let mig = Migration {
vertex_map: HashMap::from([
(Name::from("a"), Name::from("A")),
(Name::from("b"), Name::from("B")),
]),
edge_map: HashMap::from([(edge.clone(), inv_edge.clone())]),
hyper_edge_map: HashMap::new(),
label_map: HashMap::new(),
resolver: HashMap::new(),
hyper_resolver: HashMap::new(),
expr_resolvers: HashMap::new(),
};
let result = invert(&mig, &src, &tgt);
assert!(result.is_ok(), "bijective migration should invert");
let inv = result.unwrap_or_else(|_| panic!("invert should succeed"));
assert_eq!(inv.vertex_map.get("A"), Some(&Name::from("a")));
assert_eq!(inv.vertex_map.get("B"), Some(&Name::from("b")));
assert_eq!(inv.edge_map.get(&inv_edge), Some(&edge));
}
#[test]
fn non_bijective_does_not_invert() {
let edge = Edge {
src: "a".into(),
tgt: "b".into(),
kind: "prop".into(),
name: Some("x".into()),
};
let src = test_schema(
&[("a", "object"), ("b", "string"), ("c", "string")],
std::slice::from_ref(&edge),
);
let mig_non_inj = Migration {
vertex_map: HashMap::from([
(Name::from("a"), Name::from("a")),
(Name::from("b"), Name::from("a")),
]),
edge_map: HashMap::new(),
hyper_edge_map: HashMap::new(),
label_map: HashMap::new(),
resolver: HashMap::new(),
hyper_resolver: HashMap::new(),
expr_resolvers: HashMap::new(),
};
let tgt2 = test_schema(&[("a", "object")], &[]);
let result = invert(&mig_non_inj, &src, &tgt2);
assert!(result.is_err(), "non-bijective migration should not invert");
}
}