use std::collections::HashMap;
use panproto_schema::Schema;
use super::{Anchor, StrategyTag, kinds_and_constraints_compatible};
#[must_use]
pub fn suffix_anchors(src: &Schema, tgt: &Schema) -> Vec<Anchor> {
let mut src_ids: Vec<&panproto_gat::Name> = src.vertices.keys().collect();
src_ids.sort_by(|a, b| a.as_str().cmp(b.as_str()));
let mut tgt_ids: Vec<&panproto_gat::Name> = tgt.vertices.keys().collect();
tgt_ids.sort_by(|a, b| a.as_str().cmp(b.as_str()));
let mut tgt_by_tail: HashMap<&str, Vec<&panproto_gat::Name>> = HashMap::new();
for tgt_id in &tgt_ids {
let tail = terminal_segment(tgt_id.as_str());
tgt_by_tail.entry(tail).or_default().push(tgt_id);
}
let mut out = Vec::new();
for src_id in src_ids {
let src_s = src_id.as_str();
if !src_s.contains('.') {
continue;
}
let tail = terminal_segment(src_s);
let Some(tgts) = tgt_by_tail.get(tail) else {
continue;
};
for tgt_id in tgts {
if src_id.as_str() == tgt_id.as_str() {
continue;
}
if !kinds_and_constraints_compatible(src, src_id, tgt, tgt_id) {
continue;
}
out.push(Anchor {
src: (*src_id).clone(),
tgt: (*tgt_id).clone(),
confidence: 1.0,
strategy: StrategyTag::ExactSuffix,
explanation: format!(
"suffix match on '.{tail}': {} against {}",
src_id.as_str(),
tgt_id.as_str(),
),
});
}
}
out
}
fn terminal_segment(id: &str) -> &str {
id.rsplit_once('.').map_or(id, |(_, tail)| tail)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use panproto_schema::{Protocol, SchemaBuilder};
fn test_protocol() -> Protocol {
Protocol {
name: "test".into(),
schema_theory: "ThTest".into(),
instance_theory: "ThWType".into(),
edge_rules: vec![],
obj_kinds: vec!["object".into(), "string".into()],
constraint_sorts: vec![],
..Protocol::default()
}
}
fn schema(verts: &[(&str, &str)]) -> Schema {
let proto = test_protocol();
let mut b = SchemaBuilder::new(&proto);
for (id, k) in verts {
b = b.vertex(id, k, None::<&str>).unwrap();
}
b.build().unwrap()
}
#[test]
fn cross_namespace_tail_matches_produce_anchor() {
let src = schema(&[("alpha.beta.gamma.tags", "object")]);
let tgt = schema(&[("delta.epsilon.zeta.tags", "object")]);
let anchors = suffix_anchors(&src, &tgt);
assert_eq!(anchors.len(), 1);
assert_eq!(anchors[0].src.as_str(), "alpha.beta.gamma.tags");
assert_eq!(anchors[0].tgt.as_str(), "delta.epsilon.zeta.tags");
assert!((anchors[0].confidence - 1.0).abs() < f64::EPSILON);
assert_eq!(anchors[0].strategy, StrategyTag::ExactSuffix);
}
#[test]
fn no_tail_match_returns_empty() {
let src = schema(&[("a.b.foo", "string")]);
let tgt = schema(&[("c.d.bar", "string")]);
assert!(suffix_anchors(&src, &tgt).is_empty());
}
#[test]
fn tail_match_with_incompatible_kinds_skipped() {
let src = schema(&[("a.b.tags", "object")]);
let tgt = schema(&[("c.d.tags", "string")]);
assert!(suffix_anchors(&src, &tgt).is_empty());
}
#[test]
fn full_id_match_delegates_to_exact_strategy() {
let src = schema(&[("a.b.tags", "object")]);
let tgt = schema(&[("a.b.tags", "object")]);
assert!(suffix_anchors(&src, &tgt).is_empty());
}
#[test]
fn bare_name_source_skipped() {
let src = schema(&[("tags", "object")]);
let tgt = schema(&[("a.b.tags", "object")]);
assert!(suffix_anchors(&src, &tgt).is_empty());
}
#[test]
fn multiple_targets_same_tail_all_emitted() {
let src = schema(&[("a.b.foo", "string")]);
let tgt = schema(&[
("x.y.foo", "string"),
("p.q.foo", "string"),
("r.s.bar", "string"),
]);
let anchors = suffix_anchors(&src, &tgt);
let targets: Vec<&str> = anchors.iter().map(|a| a.tgt.as_str()).collect();
assert_eq!(anchors.len(), 2);
assert!(targets.contains(&"x.y.foo"));
assert!(targets.contains(&"p.q.foo"));
assert!(!targets.contains(&"r.s.bar"));
}
#[test]
fn deterministic_emission_order() {
let src1 = schema(&[
("a.b.foo", "string"),
("a.b.bar", "string"),
("a.b.baz", "string"),
]);
let src2 = schema(&[
("a.b.baz", "string"),
("a.b.foo", "string"),
("a.b.bar", "string"),
]);
let tgt = schema(&[
("x.y.foo", "string"),
("x.y.bar", "string"),
("x.y.baz", "string"),
]);
let r1: Vec<_> = suffix_anchors(&src1, &tgt)
.iter()
.map(|a| (a.src.as_str().to_owned(), a.tgt.as_str().to_owned()))
.collect();
let r2: Vec<_> = suffix_anchors(&src2, &tgt)
.iter()
.map(|a| (a.src.as_str().to_owned(), a.tgt.as_str().to_owned()))
.collect();
assert_eq!(r1, r2);
}
#[test]
fn multi_prop_cross_namespace_recovery() {
let src = schema(&[
("alpha.one.tags", "object"),
("alpha.one.text", "string"),
("alpha.one.labels", "object"),
("alpha.one.createdAt", "string"),
]);
let tgt = schema(&[
("gamma.two.tags", "object"),
("gamma.two.labels", "object"),
("gamma.two.title", "string"),
("gamma.two.path", "string"),
]);
let anchors = suffix_anchors(&src, &tgt);
let pairs: Vec<(&str, &str)> = anchors
.iter()
.map(|a| (a.src.as_str(), a.tgt.as_str()))
.collect();
assert!(pairs.contains(&("alpha.one.tags", "gamma.two.tags")));
assert!(pairs.contains(&("alpha.one.labels", "gamma.two.labels")));
assert!(!pairs.iter().any(|(s, _)| *s == "alpha.one.text"));
assert!(!pairs.iter().any(|(s, _)| *s == "alpha.one.createdAt"));
}
#[test]
fn terminal_segment_handles_no_dot() {
assert_eq!(terminal_segment("tags"), "tags");
assert_eq!(terminal_segment("a.b.c"), "c");
assert_eq!(terminal_segment(""), "");
}
}