use std::collections::{HashMap, HashSet};
use std::hash::BuildHasher;
use panproto_gat::Name;
use rustc_hash::FxHashSet;
use smallvec::SmallVec;
use crate::error::SchemaError;
use crate::protocol::Protocol;
use crate::schema::{CoercionSpec, Edge, HyperEdge, RecursionPoint, Schema, Span, Variant, Vertex};
use crate::validate::validate;
pub fn induce<VS: BuildHasher, ES: BuildHasher>(
schema: &Schema,
protocol: &Protocol,
keep_v: &HashSet<Name, VS>,
keep_e: &HashSet<Edge, ES>,
) -> Result<Schema, SchemaError> {
let vertices = retain_by_vertex(&schema.vertices, keep_v);
let keep_v: HashSet<Name> = vertices.keys().cloned().collect();
let keep_v = &keep_v;
let edges: HashMap<Edge, Name> = schema
.edges
.keys()
.filter(|edge| {
keep_e.contains(*edge)
&& vertices.contains_key(&edge.src)
&& vertices.contains_key(&edge.tgt)
})
.map(|edge| (edge.clone(), edge.kind.clone()))
.collect();
let nsids = retain_by_vertex(&schema.nsids, keep_v);
debug_assert!(
nsids_agree_with_vertices(&nsids, &vertices),
"induce: schema.nsids disagrees with Vertex::nsid"
);
let required = induce_required(&schema.required, keep_v, &edges);
let orderings = retain_by_edge(&schema.orderings, &edges);
let usage_modes = retain_by_edge(&schema.usage_modes, &edges);
let adjacency = build_adjacency(&edges, schema);
let apex = Schema {
protocol: schema.protocol.clone(),
vertices,
edges,
hyper_edges: induce_hyper_edges(&schema.hyper_edges, keep_v),
constraints: retain_by_vertex(&schema.constraints, keep_v),
required,
nsids,
entries: induce_entries(&schema.entries, keep_v),
variants: induce_variants(&schema.variants, keep_v),
orderings,
recursion_points: induce_recursion_points(&schema.recursion_points, keep_v),
spans: induce_spans(&schema.spans, keep_v),
usage_modes,
nominal: retain_by_vertex(&schema.nominal, keep_v),
coercions: induce_coercions(&schema.coercions, &schema.vertices, keep_v),
mergers: retain_by_vertex(&schema.mergers, keep_v),
defaults: retain_by_vertex(&schema.defaults, keep_v),
policies: schema.policies.clone(),
outgoing: adjacency.outgoing,
incoming: adjacency.incoming,
between: adjacency.between,
};
let findings = validate(&apex, protocol);
if findings.is_empty() {
Ok(apex)
} else {
Err(SchemaError::InducedSchemaInvalid { findings })
}
}
pub fn induce_on_vertices<VS: BuildHasher>(
schema: &Schema,
protocol: &Protocol,
keep_v: &HashSet<Name, VS>,
) -> Result<Schema, SchemaError> {
let keep_e: FxHashSet<Edge> = schema
.edges
.keys()
.filter(|edge| keep_v.contains(&edge.src) && keep_v.contains(&edge.tgt))
.cloned()
.collect();
induce(schema, protocol, keep_v, &keep_e)
}
pub(crate) fn ordered_edges(schema: &Schema) -> Vec<Edge> {
let mut sources: Vec<&Name> = schema.outgoing.keys().collect();
sources.sort_unstable();
let mut out: Vec<Edge> = Vec::with_capacity(schema.edges.len());
let mut seen: FxHashSet<&Edge> = FxHashSet::default();
for src in sources {
let Some(bucket) = schema.outgoing.get(src) else {
continue;
};
for edge in bucket {
if edge.src == *src && schema.edges.contains_key(edge) && seen.insert(edge) {
out.push(edge.clone());
}
}
}
let mut rest: Vec<&Edge> = schema
.edges
.keys()
.filter(|edge| !seen.contains(*edge))
.collect();
rest.sort_unstable();
out.extend(rest.into_iter().cloned());
out
}
struct Adjacency {
outgoing: HashMap<Name, SmallVec<Edge, 4>>,
incoming: HashMap<Name, SmallVec<Edge, 4>>,
between: HashMap<(Name, Name), SmallVec<Edge, 2>>,
}
fn build_adjacency(edges: &HashMap<Edge, Name>, parent: &Schema) -> Adjacency {
Adjacency {
outgoing: restrict_index(&parent.outgoing, edges, |edge| edge.src.clone()),
incoming: restrict_index(&parent.incoming, edges, |edge| edge.tgt.clone()),
between: restrict_index(&parent.between, edges, |edge| {
(edge.src.clone(), edge.tgt.clone())
}),
}
}
fn restrict_index<K, const N: usize>(
index: &HashMap<K, SmallVec<Edge, N>>,
edges: &HashMap<Edge, Name>,
key_of: impl Fn(&Edge) -> K,
) -> HashMap<K, SmallVec<Edge, N>>
where
K: Clone + Eq + std::hash::Hash,
{
let mut out: HashMap<K, SmallVec<Edge, N>> = HashMap::new();
let mut placed: FxHashSet<Edge> = FxHashSet::default();
for (key, bucket) in index {
for edge in bucket {
if edges.contains_key(edge) && key_of(edge) == *key && placed.insert(edge.clone()) {
out.entry(key.clone()).or_default().push(edge.clone());
}
}
}
let mut omitted: Vec<&Edge> = edges
.keys()
.filter(|edge| !placed.contains(*edge))
.collect();
omitted.sort_unstable();
for edge in omitted {
out.entry(key_of(edge)).or_default().push(edge.clone());
}
out
}
fn nsids_agree_with_vertices(
nsids: &HashMap<Name, Name>,
vertices: &HashMap<Name, Vertex>,
) -> bool {
nsids.iter().all(|(id, nsid)| {
vertices
.get(id)
.is_none_or(|v| v.nsid.as_ref() == Some(nsid))
}) && vertices.iter().all(|(id, vertex)| {
vertex
.nsid
.as_ref()
.is_none_or(|declared| nsids.get(id) == Some(declared))
})
}
fn retain_by_vertex<V: Clone, S: BuildHasher>(
map: &HashMap<Name, V>,
keep_v: &HashSet<Name, S>,
) -> HashMap<Name, V> {
map.iter()
.filter(|(id, _)| keep_v.contains(*id))
.map(|(id, value)| (id.clone(), value.clone()))
.collect()
}
fn retain_by_edge<V: Clone>(
map: &HashMap<Edge, V>,
edges: &HashMap<Edge, Name>,
) -> HashMap<Edge, V> {
map.iter()
.filter(|(edge, _)| edges.contains_key(*edge))
.map(|(edge, value)| (edge.clone(), value.clone()))
.collect()
}
fn induce_hyper_edges<S: BuildHasher>(
hyper_edges: &HashMap<Name, HyperEdge>,
keep_v: &HashSet<Name, S>,
) -> HashMap<Name, HyperEdge> {
hyper_edges
.iter()
.filter(|(_, hyper_edge)| hyper_edge.signature.values().all(|v| keep_v.contains(v)))
.map(|(id, hyper_edge)| (id.clone(), hyper_edge.clone()))
.collect()
}
fn induce_required<S: BuildHasher>(
required: &HashMap<Name, Vec<Edge>>,
keep_v: &HashSet<Name, S>,
edges: &HashMap<Edge, Name>,
) -> HashMap<Name, Vec<Edge>> {
let mut out: HashMap<Name, Vec<Edge>> = HashMap::new();
for (vertex_id, required_edges) in required {
if !keep_v.contains(vertex_id) {
continue;
}
let kept: Vec<Edge> = required_edges
.iter()
.filter(|edge| edges.contains_key(*edge))
.cloned()
.collect();
if !kept.is_empty() {
out.insert(vertex_id.clone(), kept);
}
}
out
}
fn induce_entries<S: BuildHasher>(entries: &[Name], keep_v: &HashSet<Name, S>) -> Vec<Name> {
let mut seen: FxHashSet<Name> = FxHashSet::default();
entries
.iter()
.filter(|id| keep_v.contains(*id))
.filter(|id| seen.insert((*id).clone()))
.cloned()
.collect()
}
fn induce_variants<S: BuildHasher>(
variants: &HashMap<Name, Vec<Variant>>,
keep_v: &HashSet<Name, S>,
) -> HashMap<Name, Vec<Variant>> {
variants
.iter()
.filter(|(parent, _)| keep_v.contains(*parent))
.map(|(parent, arms)| {
let kept: Vec<Variant> = arms
.iter()
.filter(|arm| keep_v.contains(&arm.parent_vertex) && keep_v.contains(&arm.id))
.cloned()
.collect();
(parent.clone(), kept)
})
.collect()
}
fn induce_recursion_points<S: BuildHasher>(
recursion_points: &HashMap<Name, RecursionPoint>,
keep_v: &HashSet<Name, S>,
) -> HashMap<Name, RecursionPoint> {
recursion_points
.iter()
.filter(|(mu, point)| keep_v.contains(*mu) && keep_v.contains(&point.target_vertex))
.map(|(mu, point)| (mu.clone(), point.clone()))
.collect()
}
fn induce_spans<S: BuildHasher>(
spans: &HashMap<Name, Span>,
keep_v: &HashSet<Name, S>,
) -> HashMap<Name, Span> {
spans
.iter()
.filter(|(_, span)| keep_v.contains(&span.left) && keep_v.contains(&span.right))
.map(|(id, span)| (id.clone(), span.clone()))
.collect()
}
fn induce_coercions<S: BuildHasher>(
coercions: &HashMap<(Name, Name), CoercionSpec>,
vertices: &HashMap<Name, Vertex>,
keep_v: &HashSet<Name, S>,
) -> HashMap<(Name, Name), CoercionSpec> {
let surviving_kinds: FxHashSet<&Name> = vertices
.iter()
.filter(|(id, _)| keep_v.contains(*id))
.map(|(_, vertex)| &vertex.kind)
.collect();
coercions
.iter()
.filter(|((source_kind, target_kind), _)| {
surviving_kinds.contains(source_kind) && surviving_kinds.contains(target_kind)
})
.map(|(pair, spec)| (pair.clone(), spec.clone()))
.collect()
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::too_many_lines)]
mod tests {
use super::*;
use crate::builder::SchemaBuilder;
use crate::error::ValidationError;
use crate::schema::{Constraint, UsageMode};
use panproto_expr::{Expr, Literal};
use panproto_gat::CoercionClass;
fn protocol() -> Protocol {
Protocol {
name: "fixture".to_owned(),
..Protocol::default()
}
}
fn expr(tag: &str) -> Expr {
Expr::Lit(Literal::Str(tag.to_owned()))
}
fn coercion(tag: &str) -> CoercionSpec {
CoercionSpec {
forward: expr(tag),
inverse: None,
class: CoercionClass::Opaque,
}
}
fn edge(src: &str, tgt: &str, kind: &str, name: Option<&str>) -> Edge {
Edge {
src: Name::from(src),
tgt: Name::from(tgt),
kind: Name::from(kind),
name: name.map(Name::from),
}
}
fn names(ids: &[&str]) -> FxHashSet<Name> {
ids.iter().copied().map(Name::from).collect()
}
fn fixture() -> Schema {
let protocol = protocol();
let e_keep = edge("root", "kept", "prop", Some("kept"));
let e_cut = edge("root", "cut", "prop", Some("cut"));
let mut signature_kept: HashMap<String, String> = HashMap::new();
signature_kept.insert("parent".to_owned(), "root".to_owned());
signature_kept.insert("child".to_owned(), "kept".to_owned());
let mut signature_cut: HashMap<String, String> = HashMap::new();
signature_cut.insert("parent".to_owned(), "root".to_owned());
signature_cut.insert("child".to_owned(), "cut".to_owned());
let mut schema = SchemaBuilder::new(&protocol)
.vertex("root", "object", Some("com.example.root"))
.expect("root")
.vertex("kept", "string", None)
.expect("kept")
.vertex("cut", "integer", Some("com.example.cut"))
.expect("cut")
.vertex("mu", "mu", None)
.expect("mu")
.vertex("mu-dangling", "mu", None)
.expect("mu-dangling")
.vertex("union", "union", None)
.expect("union")
.vertex("arm", "string", None)
.expect("arm")
.vertex("arm-cut", "string", None)
.expect("arm-cut")
.edge("root", "kept", "prop", Some("kept"))
.expect("root -> kept")
.edge("root", "cut", "prop", Some("cut"))
.expect("root -> cut")
.edge("union", "arm", "variant", Some("arm"))
.expect("union -> arm")
.edge("union", "arm-cut", "variant", Some("arm-cut"))
.expect("union -> arm-cut")
.edge("mu", "root", "unfold", None)
.expect("mu -> root")
.edge("mu-dangling", "cut", "unfold", None)
.expect("mu-dangling -> cut")
.hyper_edge("he-kept", "record", signature_kept, "parent")
.expect("he-kept")
.hyper_edge("he-cut", "record", signature_cut, "parent")
.expect("he-cut")
.constraint("root", "maxLength", "10")
.constraint("cut", "maxLength", "5")
.required("root", vec![e_keep.clone(), e_cut.clone()])
.required("cut", vec![e_cut.clone()])
.coercion("object", "string", coercion("object->string"))
.coercion("string", "integer", coercion("string->integer"))
.coercion("root", "kept", coercion("vertex-id-shaped"))
.merger("root", expr("merge-root"))
.merger("cut", expr("merge-cut"))
.default_expr("kept", expr("default-kept"))
.default_expr("cut", expr("default-cut"))
.policy("maxLength", expr("policy-maxLength"))
.policy("cut", expr("policy-named-like-a-vertex"))
.entry("root")
.entry("cut")
.build()
.expect("build");
schema.entries.push(Name::from("root"));
schema.variants.insert(
Name::from("union"),
vec![
Variant {
id: Name::from("arm"),
parent_vertex: Name::from("union"),
tag: Some(Name::from("a")),
},
Variant {
id: Name::from("arm-cut"),
parent_vertex: Name::from("union"),
tag: Some(Name::from("b")),
},
],
);
schema.variants.insert(
Name::from("cut"),
vec![Variant {
id: Name::from("arm"),
parent_vertex: Name::from("cut"),
tag: None,
}],
);
schema.orderings.insert(e_keep.clone(), 0);
schema.orderings.insert(e_cut.clone(), 1);
schema.recursion_points.insert(
Name::from("mu"),
RecursionPoint {
target_vertex: Name::from("root"),
},
);
schema.recursion_points.insert(
Name::from("mu-dangling"),
RecursionPoint {
target_vertex: Name::from("cut"),
},
);
schema.spans.insert(
Name::from("span-kept"),
Span {
id: Name::from("span-kept"),
left: Name::from("root"),
right: Name::from("kept"),
},
);
schema.spans.insert(
Name::from("span-cut"),
Span {
id: Name::from("span-cut"),
left: Name::from("root"),
right: Name::from("cut"),
},
);
schema.usage_modes.insert(e_keep, UsageMode::Linear);
schema.usage_modes.insert(e_cut, UsageMode::Affine);
schema.nominal.insert(Name::from("root"), true);
schema.nominal.insert(Name::from("cut"), false);
schema
}
#[test]
fn fixture_populates_every_field() {
let schema = fixture();
assert!(!schema.protocol.is_empty());
assert!(!schema.vertices.is_empty());
assert!(!schema.edges.is_empty());
assert!(!schema.hyper_edges.is_empty());
assert!(!schema.constraints.is_empty());
assert!(!schema.required.is_empty());
assert!(!schema.nsids.is_empty());
assert!(!schema.entries.is_empty());
assert!(!schema.variants.is_empty());
assert!(!schema.orderings.is_empty());
assert!(!schema.recursion_points.is_empty());
assert!(!schema.spans.is_empty());
assert!(!schema.usage_modes.is_empty());
assert!(!schema.nominal.is_empty());
assert!(!schema.coercions.is_empty());
assert!(!schema.mergers.is_empty());
assert!(!schema.defaults.is_empty());
assert!(!schema.policies.is_empty());
assert!(!schema.outgoing.is_empty());
assert!(!schema.incoming.is_empty());
assert!(!schema.between.is_empty());
}
#[test]
fn induction_restricts_every_field() {
let protocol = protocol();
let schema = fixture();
let keep_v = names(&["root", "kept", "mu", "mu-dangling", "union", "arm"]);
let apex = induce_on_vertices(&schema, &protocol, &keep_v).expect("induce");
let e_keep = edge("root", "kept", "prop", Some("kept"));
let e_cut = edge("root", "cut", "prop", Some("cut"));
let e_arm = edge("union", "arm", "variant", Some("arm"));
let e_mu = edge("mu", "root", "unfold", None);
assert_eq!(apex.protocol, schema.protocol);
assert_eq!(apex.vertices.len(), 6);
assert!(apex.has_vertex("root"));
assert!(!apex.has_vertex("cut"));
assert!(!apex.has_vertex("arm-cut"));
assert_eq!(apex.edges.len(), 3);
assert!(apex.edges.contains_key(&e_keep));
assert!(apex.edges.contains_key(&e_arm));
assert!(apex.edges.contains_key(&e_mu));
assert!(!apex.edges.contains_key(&e_cut));
assert!(
apex.edges.iter().all(|(key, kind)| *kind == key.kind),
"the edge map's value must be the edge kind"
);
assert!(apex.hyper_edges.contains_key("he-kept"));
assert!(!apex.hyper_edges.contains_key("he-cut"));
assert!(apex.constraints.contains_key("root"));
assert!(!apex.constraints.contains_key("cut"));
assert!(!apex.required.contains_key("cut"));
assert_eq!(
apex.required.get("root").map(Vec::as_slice),
Some([e_keep.clone()].as_slice()),
"the inner Vec<Edge> of `required` must be filtered too"
);
assert!(apex.nsids.contains_key("root"));
assert!(!apex.nsids.contains_key("cut"));
assert_eq!(apex.entries, vec![Name::from("root")]);
assert!(!apex.variants.contains_key("cut"));
let union_arms = apex.variants.get("union").expect("union arms");
assert_eq!(union_arms.len(), 1);
assert_eq!(union_arms[0].id, Name::from("arm"));
assert!(apex.orderings.contains_key(&e_keep));
assert!(!apex.orderings.contains_key(&e_cut));
assert!(apex.recursion_points.contains_key("mu"));
assert!(
!apex.recursion_points.contains_key("mu-dangling"),
"a fixpoint whose target left must not survive"
);
assert!(apex.spans.contains_key("span-kept"));
assert!(!apex.spans.contains_key("span-cut"));
assert_eq!(apex.usage_modes.get(&e_keep), Some(&UsageMode::Linear));
assert!(!apex.usage_modes.contains_key(&e_cut));
assert_eq!(apex.nominal.get("root"), Some(&true));
assert!(!apex.nominal.contains_key("cut"));
let surviving: Vec<(Name, Name)> = apex.coercions.keys().cloned().collect();
assert_eq!(surviving.len(), 1, "surviving coercions: {surviving:?}");
assert!(
apex.coercions
.contains_key(&(Name::from("object"), Name::from("string")))
);
assert!(
!apex
.coercions
.contains_key(&(Name::from("string"), Name::from("integer")))
);
assert!(
!apex
.coercions
.contains_key(&(Name::from("root"), Name::from("kept"))),
"coercion keys are kinds, so a vertex-id-shaped key must not survive"
);
assert!(apex.mergers.contains_key("root"));
assert!(!apex.mergers.contains_key("cut"));
assert!(apex.defaults.contains_key("kept"));
assert!(!apex.defaults.contains_key("cut"));
assert_eq!(apex.policies.len(), schema.policies.len());
assert!(apex.policies.contains_key("cut"));
let fresh = build_adjacency(&apex.edges, &apex);
assert_eq!(apex.outgoing, fresh.outgoing);
assert_eq!(apex.incoming, fresh.incoming);
assert_eq!(apex.between, fresh.between);
assert!(
apex.outgoing
.values()
.flat_map(|bucket| bucket.as_slice().iter())
.all(|e| apex.has_vertex(&e.src) && apex.has_vertex(&e.tgt)),
"no index entry may name a dropped vertex"
);
assert!(validate(&apex, &protocol).is_empty());
}
#[test]
fn identity_induction_is_the_identity() {
let protocol = protocol();
let schema = fixture();
let keep_v: FxHashSet<Name> = schema.vertices.keys().cloned().collect();
let keep_e: FxHashSet<Edge> = schema.edges.keys().cloned().collect();
let apex = induce(&schema, &protocol, &keep_v, &keep_e).expect("induce");
assert_eq!(apex.protocol, schema.protocol);
assert_eq!(apex.vertices, schema.vertices);
assert_eq!(apex.edges, schema.edges);
assert_eq!(apex.hyper_edges, schema.hyper_edges);
assert_eq!(apex.constraints, schema.constraints);
assert_eq!(apex.required, schema.required);
assert_eq!(apex.nsids, schema.nsids);
assert_eq!(apex.entries, vec![Name::from("root"), Name::from("cut")]);
assert_eq!(apex.variants, schema.variants);
assert_eq!(apex.orderings, schema.orderings);
assert_eq!(apex.recursion_points, schema.recursion_points);
assert_eq!(apex.spans, schema.spans);
assert_eq!(apex.usage_modes, schema.usage_modes);
assert_eq!(apex.nominal, schema.nominal);
let mut expected_coercions = schema.coercions.clone();
expected_coercions.remove(&(Name::from("root"), Name::from("kept")));
assert_eq!(apex.coercions, expected_coercions);
assert_eq!(apex.mergers, schema.mergers);
assert_eq!(apex.defaults, schema.defaults);
assert_eq!(apex.policies, schema.policies);
assert_eq!(apex.outgoing, schema.outgoing);
assert_eq!(apex.incoming, schema.incoming);
assert_eq!(apex.between, schema.between);
}
#[test]
fn induction_carries_the_parents_bucket_order() {
let protocol = protocol();
let schema = SchemaBuilder::new(&protocol)
.vertex("root", "object", None)
.expect("root")
.vertex("leaf", "string", None)
.expect("leaf")
.edge("root", "leaf", "prop", Some("z"))
.expect("z")
.edge("root", "leaf", "prop", Some("y"))
.expect("y")
.edge("root", "leaf", "prop", Some("x"))
.expect("x")
.build()
.expect("build");
let names = |s: &Schema| -> Vec<Name> {
s.outgoing_edges("root")
.iter()
.filter_map(|e| e.name.clone())
.collect()
};
assert_eq!(names(&schema), ["z", "y", "x"].map(Name::from));
let keep_v: FxHashSet<Name> = schema.vertices.keys().cloned().collect();
let apex = induce_on_vertices(&schema, &protocol, &keep_v).expect("induce");
assert_eq!(
names(&apex),
names(&schema),
"the identity cut must not reorder an adjacency bucket"
);
let mut keep_e: FxHashSet<Edge> = schema.edges.keys().cloned().collect();
keep_e.remove(&edge("root", "leaf", "prop", Some("y")));
let cut = induce(&schema, &protocol, &keep_v, &keep_e).expect("induce");
assert_eq!(names(&cut), ["z", "x"].map(Name::from));
}
#[test]
fn an_edge_with_a_phantom_endpoint_never_reaches_the_apex() {
let protocol = protocol();
let mut schema = fixture();
let phantom = edge("phantom", "kept", "prop", Some("p"));
schema.edges.insert(phantom.clone(), Name::from("prop"));
schema
.outgoing
.entry(Name::from("phantom"))
.or_default()
.push(phantom.clone());
schema
.incoming
.entry(Name::from("kept"))
.or_default()
.push(phantom.clone());
let keep_v = names(&["root", "kept", "phantom"]);
let keep_e: FxHashSet<Edge> = schema.edges.keys().cloned().collect();
let apex = induce(&schema, &protocol, &keep_v, &keep_e).expect("induce");
assert!(!apex.has_vertex("phantom"));
assert!(
!apex.edges.contains_key(&phantom),
"an edge whose endpoint is not a vertex of the apex must be dropped"
);
assert!(
apex.outgoing_edges("phantom").is_empty(),
"and so must its adjacency entry"
);
assert!(
apex.edges
.keys()
.chain(apex.outgoing.values().flat_map(SmallVec::as_slice))
.chain(apex.incoming.values().flat_map(SmallVec::as_slice))
.chain(apex.between.values().flat_map(SmallVec::as_slice))
.all(|e| apex.has_vertex(&e.src) && apex.has_vertex(&e.tgt)),
"no edge anywhere in the apex may name a vertex the apex lacks"
);
}
#[test]
fn an_index_entry_the_parent_omitted_is_supplied() {
let protocol = protocol();
let mut schema = fixture();
let e_keep = edge("root", "kept", "prop", Some("kept"));
schema.outgoing.remove("root");
let keep_v = names(&["root", "kept"]);
let apex = induce_on_vertices(&schema, &protocol, &keep_v).expect("induce");
assert_eq!(apex.outgoing_edges("root"), [e_keep].as_slice());
}
#[test]
fn empty_induction_is_empty_but_valid() {
let protocol = protocol();
let schema = fixture();
let apex = induce_on_vertices(&schema, &protocol, &FxHashSet::default()).expect("induce");
assert!(apex.vertices.is_empty());
assert!(apex.edges.is_empty());
assert!(apex.hyper_edges.is_empty());
assert!(apex.constraints.is_empty());
assert!(apex.required.is_empty());
assert!(apex.nsids.is_empty());
assert!(apex.entries.is_empty());
assert!(apex.variants.is_empty());
assert!(apex.orderings.is_empty());
assert!(apex.recursion_points.is_empty());
assert!(apex.spans.is_empty());
assert!(apex.usage_modes.is_empty());
assert!(apex.nominal.is_empty());
assert!(apex.coercions.is_empty());
assert!(apex.mergers.is_empty());
assert!(apex.defaults.is_empty());
assert!(apex.outgoing.is_empty());
assert!(apex.incoming.is_empty());
assert!(apex.between.is_empty());
assert_eq!(apex.policies, schema.policies);
assert!(validate(&apex, &protocol).is_empty());
}
#[test]
fn induction_is_idempotent() {
let protocol = protocol();
let schema = fixture();
let keep_v = names(&["root", "kept", "mu", "union", "arm"]);
let once = induce_on_vertices(&schema, &protocol, &keep_v).expect("once");
let twice = induce_on_vertices(&once, &protocol, &keep_v).expect("twice");
assert_eq!(once.vertices, twice.vertices);
assert_eq!(once.edges, twice.edges);
assert_eq!(once.hyper_edges, twice.hyper_edges);
assert_eq!(once.constraints, twice.constraints);
assert_eq!(once.required, twice.required);
assert_eq!(once.nsids, twice.nsids);
assert_eq!(once.entries, twice.entries);
assert_eq!(once.variants, twice.variants);
assert_eq!(once.orderings, twice.orderings);
assert_eq!(once.recursion_points, twice.recursion_points);
assert_eq!(once.spans, twice.spans);
assert_eq!(once.usage_modes, twice.usage_modes);
assert_eq!(once.nominal, twice.nominal);
assert_eq!(once.coercions, twice.coercions);
assert_eq!(once.mergers, twice.mergers);
assert_eq!(once.defaults, twice.defaults);
assert_eq!(once.policies, twice.policies);
assert_eq!(once.outgoing, twice.outgoing);
assert_eq!(once.incoming, twice.incoming);
assert_eq!(once.between, twice.between);
}
#[test]
fn keep_e_is_intersected_not_rejected() {
let protocol = protocol();
let schema = fixture();
let keep_v = names(&["root", "kept"]);
let keep_e: FxHashSet<Edge> = schema.edges.keys().cloned().collect();
let apex = induce(&schema, &protocol, &keep_v, &keep_e).expect("induce");
assert_eq!(apex.edge_count(), 1);
assert!(
apex.edges
.contains_key(&edge("root", "kept", "prop", Some("kept")))
);
}
#[test]
fn unknown_ids_in_keep_sets_are_ignored() {
let protocol = protocol();
let schema = fixture();
let keep_v = names(&["root", "kept", "no-such-vertex"]);
let mut keep_e: FxHashSet<Edge> = schema.edges.keys().cloned().collect();
keep_e.insert(edge("root", "kept", "no-such-kind", None));
let apex = induce(&schema, &protocol, &keep_v, &keep_e).expect("induce");
assert_eq!(apex.vertex_count(), 2);
assert_eq!(apex.edge_count(), 1);
}
#[test]
fn an_inherited_defect_surfaces_as_induced_schema_invalid() {
let protocol = Protocol {
name: "strict".to_owned(),
constraint_sorts: vec!["format".to_owned()],
..Protocol::default()
};
let schema = SchemaBuilder::new(&protocol)
.vertex("root", "object", None)
.expect("root")
.constraint("root", "maxLength", "10")
.build()
.expect("build");
let err = induce_on_vertices(&schema, &protocol, &names(&["root"]))
.expect_err("the inherited constraint sort must be reported");
match err {
SchemaError::InducedSchemaInvalid { findings } => {
assert_eq!(findings.len(), 1);
assert!(matches!(
findings[0],
ValidationError::InvalidConstraintSort { .. }
));
}
other => panic!("expected InducedSchemaInvalid, got {other:?}"),
}
}
#[test]
fn constraints_survive_with_their_values() {
let protocol = protocol();
let schema = fixture();
let apex =
induce_on_vertices(&schema, &protocol, &names(&["root", "kept"])).expect("induce");
assert_eq!(
apex.constraints_for("root"),
[Constraint {
sort: Name::from("maxLength"),
value: "10".to_owned(),
}]
.as_slice()
);
}
fn wide_fixture() -> Schema {
let protocol = protocol();
let mut builder = SchemaBuilder::new(&protocol)
.vertex("root", "object", None::<&str>)
.expect("root");
for i in 0..12 {
builder = builder
.vertex(&format!("n{i}"), "string", None::<&str>)
.expect("leaf")
.edge("root", &format!("n{i}"), "prop", Some(&format!("f{i}")))
.expect("edge");
}
builder.build().expect("build")
}
fn wide_ref_fixture() -> Schema {
let protocol = protocol();
let mut builder = SchemaBuilder::new(&protocol)
.vertex("root", "object", None::<&str>)
.expect("root");
for i in 0..12 {
builder = builder
.vertex(&format!("r{i}"), "ref", None::<&str>)
.expect("ref")
.vertex(&format!("n{i}"), "string", None::<&str>)
.expect("leaf")
.edge("root", &format!("r{i}"), "prop", Some(&format!("f{i}")))
.expect("edge")
.edge(
&format!("r{i}"),
&format!("n{i}"),
"ref-target",
None::<&str>,
)
.expect("ref edge");
}
builder.build().expect("build")
}
fn index_snapshot(schema: &Schema) -> Vec<(String, Vec<Edge>)> {
let mut rows: Vec<(String, Vec<Edge>)> = Vec::new();
for (k, v) in &schema.outgoing {
rows.push((format!("out/{k}"), v.as_slice().to_vec()));
}
for (k, v) in &schema.incoming {
rows.push((format!("in/{k}"), v.as_slice().to_vec()));
}
for ((s, t), v) in &schema.between {
rows.push((format!("btw/{s}->{t}"), v.as_slice().to_vec()));
}
rows.sort_by(|a, b| a.0.cmp(&b.0));
rows
}
fn out_labels(schema: &Schema) -> Vec<String> {
schema
.outgoing_edges("root")
.iter()
.map(|e| e.name.clone().unwrap_or_default().to_string())
.collect()
}
fn insertion_labels() -> Vec<String> {
(0..12).map(|i| format!("f{i}")).collect()
}
#[test]
fn ordered_edges_preserves_sibling_order() {
let schema = wide_fixture();
let seq: Vec<String> = ordered_edges(&schema)
.iter()
.map(|e| e.name.clone().unwrap_or_default().to_string())
.collect();
assert_eq!(
seq,
insertion_labels(),
"ordered_edges must reproduce the builder's sibling order, not a sort"
);
}
#[test]
fn ordered_edges_orders_by_source_and_keeps_siblings_as_stored() {
let protocol = protocol();
let schema = SchemaBuilder::new(&protocol)
.vertex("zeta", "object", None::<&str>)
.expect("zeta")
.vertex("alpha", "object", None::<&str>)
.expect("alpha")
.vertex("leaf", "string", None::<&str>)
.expect("leaf")
.vertex("other", "string", None::<&str>)
.expect("other")
.edge("zeta", "leaf", "prop", Some("first"))
.expect("first")
.edge("alpha", "leaf", "prop", Some("second"))
.expect("second")
.edge("zeta", "other", "prop", Some("third"))
.expect("third")
.edge("alpha", "other", "prop", Some("fourth"))
.expect("fourth")
.build()
.expect("build");
let seq: Vec<String> = ordered_edges(&schema)
.iter()
.map(|e| e.name.clone().unwrap_or_default().to_string())
.collect();
assert_eq!(
seq,
vec![
"second".to_owned(),
"fourth".to_owned(),
"first".to_owned(),
"third".to_owned(),
],
"alpha's bucket in its own order, then zeta's"
);
assert_eq!(out_labels_of(&schema, "zeta"), vec!["first", "third"]);
assert_eq!(out_labels_of(&schema, "alpha"), vec!["second", "fourth"]);
}
fn out_labels_of(schema: &Schema, vertex: &str) -> Vec<String> {
schema
.outgoing_edges(vertex)
.iter()
.map(|e| e.name.clone().unwrap_or_default().to_string())
.collect()
}
#[test]
fn ordered_edges_is_independent_of_the_hash_seed() {
let first = ordered_edges(&wide_fixture());
for _ in 0..8 {
assert_eq!(
ordered_edges(&wide_fixture()),
first,
"ordered_edges varied across two equal schemas, so it is reading a hash order"
);
}
}
#[test]
fn builder_indices_are_independent_of_the_hash_seed() {
let first = index_snapshot(&wide_fixture());
for _ in 0..8 {
assert_eq!(index_snapshot(&wide_fixture()), first);
}
assert_eq!(out_labels(&wide_fixture()), insertion_labels());
}
#[test]
fn colimit_indices_are_independent_of_the_hash_seed() {
use crate::colimit::{SchemaOverlap, schema_pushout};
let pushout = || {
let left = wide_fixture();
let right = SchemaBuilder::new(&protocol())
.vertex("other", "object", None::<&str>)
.expect("other")
.vertex("other.z", "string", None::<&str>)
.expect("z")
.edge("other", "other.z", "prop", Some("z"))
.expect("edge")
.build()
.expect("build");
let (apex, _, _) =
schema_pushout(&left, &right, &SchemaOverlap::default()).expect("pushout");
apex
};
let first = index_snapshot(&pushout());
for _ in 0..8 {
assert_eq!(
index_snapshot(&pushout()),
first,
"pushout bucket order varied across runs"
);
}
assert_eq!(
out_labels(&pushout()),
insertion_labels(),
"the pushout must carry the left schema's sibling order through"
);
}
#[test]
fn normalize_indices_are_independent_of_the_hash_seed() {
use crate::normalize::normalize;
let first = index_snapshot(&normalize(&wide_ref_fixture()));
for _ in 0..8 {
assert_eq!(
index_snapshot(&normalize(&wide_ref_fixture())),
first,
"normalized bucket order varied across runs"
);
}
assert_eq!(
out_labels(&normalize(&wide_ref_fixture())),
insertion_labels(),
"collapsing refs must keep the order the edges were declared in"
);
}
#[test]
fn induce_indices_are_independent_of_the_hash_seed() {
let induced = || {
let schema = wide_fixture();
let keep_v: FxHashSet<Name> = schema.vertices.keys().cloned().collect();
let keep_e: FxHashSet<Edge> = schema.edges.keys().cloned().collect();
induce(&schema, &protocol(), &keep_v, &keep_e).expect("induce")
};
let first = index_snapshot(&induced());
for _ in 0..8 {
assert_eq!(index_snapshot(&induced()), first);
}
assert_eq!(out_labels(&induced()), insertion_labels());
}
#[test]
fn every_path_producing_a_schema_agrees_on_bucket_order() {
use crate::colimit::{SchemaOverlap, schema_pushout};
use crate::normalize::normalize;
let built = wide_fixture();
let expected = insertion_labels();
assert_eq!(out_labels(&built), expected, "builder");
assert_eq!(out_labels(&normalize(&built)), expected, "normalize");
assert_eq!(
out_labels(&normalize(&wide_ref_fixture())),
expected,
"normalize over refs"
);
let overlap = SchemaOverlap {
vertex_pairs: built
.vertices
.keys()
.map(|v| (v.clone(), v.clone()))
.collect(),
edge_pairs: built.edges.keys().map(|e| (e.clone(), e.clone())).collect(),
};
let (apex, _, _) = schema_pushout(&built, &built, &overlap).expect("pushout");
assert_eq!(out_labels(&apex), expected, "colimit");
let keep_v: FxHashSet<Name> = built.vertices.keys().cloned().collect();
let keep_e: FxHashSet<Edge> = built.edges.keys().cloned().collect();
let ind = induce(&built, &protocol(), &keep_v, &keep_e).expect("induce");
assert_eq!(out_labels(&ind), expected, "induce");
let composite = induce(&normalize(&apex), &protocol(), &keep_v, &keep_e)
.expect("induce of normalize of colimit");
assert_eq!(
out_labels(&composite),
expected,
"induce ∘ normalize ∘ colimit"
);
}
#[test]
fn a_row_keyed_at_a_non_vertex_never_reaches_the_apex() {
let mut schema = SchemaBuilder::new(&protocol())
.vertex("a", "object", None)
.expect("a")
.build()
.expect("build");
let ghost = Name::from("ghost");
schema.nsids.insert(ghost.clone(), Name::from("g"));
schema.constraints.insert(
ghost.clone(),
vec![Constraint {
sort: Name::from("maxLength"),
value: "1".to_owned(),
}],
);
schema.nominal.insert(ghost.clone(), true);
schema.mergers.insert(ghost.clone(), expr("merge"));
schema.defaults.insert(ghost.clone(), expr("default"));
schema.entries.push(ghost.clone());
let keep_v = names(&["a", "ghost"]);
let keep_e: FxHashSet<Edge> = FxHashSet::default();
let apex = induce(&schema, &protocol(), &keep_v, &keep_e).expect("induce");
assert!(!apex.vertices.contains_key(&ghost), "no ghost vertex");
assert!(!apex.nsids.contains_key(&ghost), "nsids");
assert!(!apex.constraints.contains_key(&ghost), "constraints");
assert!(!apex.nominal.contains_key(&ghost), "nominal");
assert!(!apex.mergers.contains_key(&ghost), "mergers");
assert!(!apex.defaults.contains_key(&ghost), "defaults");
assert!(
!apex.entries.contains(&ghost),
"entries: a basepoint naming no vertex is the worst of the set, since `entry_vertices` then hands out a vertex the apex does not hold"
);
}
}