use crate::Schema;
use crate::schema::Constraint;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error(
"cannot construct AbstractSchema: {count} layout-fibre constraint(s) present; \
call Schema::forget_layout first"
)]
pub struct LayoutConstraintsPresent {
pub count: usize,
}
#[derive(Clone, Debug)]
pub struct AbstractSchema {
inner: Schema,
}
#[derive(Clone, Debug)]
pub struct DecoratedSchema {
inner: Schema,
}
#[derive(Clone, Copy, Debug)]
pub struct LayoutWitness<'a> {
constraints: &'a [Constraint],
}
impl AbstractSchema {
pub fn from_layout_free(schema: Schema) -> Result<Self, LayoutConstraintsPresent> {
let offending = schema
.constraints
.values()
.flat_map(|cs| cs.iter())
.filter(|c| panproto_gat::is_layout_sort(c.sort.as_ref()))
.count();
if offending == 0 {
Ok(Self { inner: schema })
} else {
Err(LayoutConstraintsPresent { count: offending })
}
}
#[must_use]
pub const fn from_layout_free_unchecked(schema: Schema) -> Self {
Self { inner: schema }
}
#[must_use]
pub const fn as_schema(&self) -> &Schema {
&self.inner
}
#[must_use]
pub fn protocol(&self) -> &str {
&self.inner.protocol
}
#[must_use]
pub fn vertex_count(&self) -> usize {
self.inner.vertex_count()
}
}
impl DecoratedSchema {
#[must_use]
pub const fn wrap_unchecked(schema: Schema) -> Self {
Self { inner: schema }
}
#[must_use]
pub const fn as_schema(&self) -> &Schema {
&self.inner
}
#[must_use]
pub fn protocol(&self) -> &str {
&self.inner.protocol
}
#[must_use]
pub fn forget_layout(&self) -> AbstractSchema {
AbstractSchema::from_layout_free_unchecked(self.inner.forget_layout())
}
#[must_use]
pub fn layout_witness(&self, vertex_id: &str) -> Option<LayoutWitness<'_>> {
let cs = self.inner.constraints.get(vertex_id)?;
Some(LayoutWitness { constraints: cs })
}
}
impl<'a> LayoutWitness<'a> {
pub fn iter(&self) -> impl Iterator<Item = &'a Constraint> + '_ {
self.constraints
.iter()
.filter(|c| panproto_gat::is_layout_sort(c.sort.as_ref()))
}
#[must_use]
pub fn start_byte(&self) -> Option<usize> {
self.constraints
.iter()
.find(|c| c.sort.as_ref() == "start-byte")
.and_then(|c| c.value.parse().ok())
}
#[must_use]
pub fn end_byte(&self) -> Option<usize> {
self.constraints
.iter()
.find(|c| c.sort.as_ref() == "end-byte")
.and_then(|c| c.value.parse().ok())
}
#[must_use]
pub fn chose_alt_fingerprint(&self) -> Option<&'a str> {
self.constraints
.iter()
.find(|c| c.sort.as_ref() == "chose-alt-fingerprint")
.map(|c| c.value.as_str())
}
#[must_use]
pub fn chose_alt_child_kinds(&self) -> Option<&'a str> {
self.constraints
.iter()
.find(|c| c.sort.as_ref() == "chose-alt-child-kinds")
.map(|c| c.value.as_str())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::{EdgeRule, Protocol, SchemaBuilder, SchemaError};
use panproto_gat::Name;
fn empty_protocol() -> Protocol {
Protocol {
name: "test".to_owned(),
schema_theory: "ThTest".to_owned(),
instance_theory: "ThWType".to_owned(),
edge_rules: vec![EdgeRule {
edge_kind: "child_of".to_owned(),
src_kinds: vec!["node".to_owned()],
tgt_kinds: vec!["node".to_owned()],
}],
obj_kinds: vec!["node".to_owned()],
..Default::default()
}
}
#[test]
fn forget_layout_strips_layout_sorts_only() {
let p = empty_protocol();
let schema = SchemaBuilder::new(&p)
.vertex("v0", "node", None)
.unwrap()
.constraint("v0", "start-byte", "10")
.constraint("v0", "end-byte", "20")
.constraint("v0", "literal-value", "hi")
.build()
.unwrap();
let stripped = schema.forget_layout();
let cs = stripped.constraints.get(&Name::from("v0")).unwrap();
assert_eq!(cs.len(), 1);
assert_eq!(cs[0].sort.as_ref(), "literal-value");
assert!(stripped.is_layout_free());
}
#[test]
fn forget_layout_is_idempotent() {
let p = empty_protocol();
let schema = SchemaBuilder::new(&p)
.vertex("v0", "node", None)
.unwrap()
.constraint("v0", "interstitial-0", " ")
.constraint("v0", "chose-alt-fingerprint", "{ }")
.build()
.unwrap();
let once = schema.forget_layout();
let twice = once.forget_layout();
assert_eq!(once.constraints, twice.constraints);
assert!(twice.is_layout_free());
}
#[test]
fn decorated_layout_witness_round_trips_byte_span() {
let p = empty_protocol();
let schema = SchemaBuilder::new(&p)
.vertex("v0", "node", None)
.unwrap()
.constraint("v0", "start-byte", "3")
.constraint("v0", "end-byte", "7")
.build()
.unwrap();
let decorated = DecoratedSchema::wrap_unchecked(schema);
let w = decorated.layout_witness("v0").unwrap();
assert_eq!(w.start_byte(), Some(3));
assert_eq!(w.end_byte(), Some(7));
}
#[test]
fn build_abstract_accepts_layout_free_input() {
let p = empty_protocol();
let result = SchemaBuilder::new(&p)
.vertex("v0", "node", None)
.unwrap()
.constraint("v0", "literal-value", "hi")
.build_abstract();
assert!(
result.is_ok(),
"build_abstract should accept content-only constraints"
);
assert!(result.unwrap().as_schema().is_layout_free());
}
#[test]
fn build_abstract_rejects_layout_constraints() {
let p = empty_protocol();
let result = SchemaBuilder::new(&p)
.vertex("v0", "node", None)
.unwrap()
.constraint("v0", "start-byte", "0")
.build_abstract();
assert!(matches!(
result,
Err(SchemaError::LayoutConstraintsOnAbstractBuild)
));
}
#[test]
fn build_decorated_accepts_any_constraint_set() {
let p = empty_protocol();
let result = SchemaBuilder::new(&p)
.vertex("v0", "node", None)
.unwrap()
.constraint("v0", "start-byte", "0")
.constraint("v0", "end-byte", "4")
.build_decorated();
assert!(
result.is_ok(),
"build_decorated does not validate the fibre"
);
}
#[test]
fn layout_witness_iter_filters_to_layout_only() {
let p = empty_protocol();
let schema = SchemaBuilder::new(&p)
.vertex("v0", "node", None)
.unwrap()
.constraint("v0", "start-byte", "0")
.constraint("v0", "literal-value", "hi")
.constraint("v0", "interstitial-0", " ")
.build()
.unwrap();
let decorated = DecoratedSchema::wrap_unchecked(schema);
let w = decorated.layout_witness("v0").unwrap();
let sorts: Vec<&str> = w.iter().map(|c| c.sort.as_ref()).collect();
assert!(sorts.contains(&"start-byte"));
assert!(sorts.contains(&"interstitial-0"));
assert!(!sorts.contains(&"literal-value"));
}
#[test]
fn layout_witness_returns_chose_alt_constraints() {
let p = empty_protocol();
let schema = SchemaBuilder::new(&p)
.vertex("v0", "node", None)
.unwrap()
.constraint("v0", "chose-alt-fingerprint", "{ }")
.constraint("v0", "chose-alt-child-kinds", "symbol punctuation")
.build()
.unwrap();
let decorated = DecoratedSchema::wrap_unchecked(schema);
let w = decorated.layout_witness("v0").unwrap();
assert_eq!(w.chose_alt_fingerprint(), Some("{ }"));
assert_eq!(w.chose_alt_child_kinds(), Some("symbol punctuation"));
}
#[test]
fn layout_witness_returns_none_for_missing_vertex() {
let p = empty_protocol();
let schema = SchemaBuilder::new(&p)
.vertex("v0", "node", None)
.unwrap()
.build()
.unwrap();
let decorated = DecoratedSchema::wrap_unchecked(schema);
assert!(decorated.layout_witness("nonexistent").is_none());
}
#[test]
fn from_layout_free_reports_offending_count() {
let p = empty_protocol();
let schema = SchemaBuilder::new(&p)
.vertex("v0", "node", None)
.unwrap()
.constraint("v0", "start-byte", "0")
.constraint("v0", "end-byte", "4")
.constraint("v0", "chose-alt-fingerprint", "{")
.build()
.unwrap();
let err = AbstractSchema::from_layout_free(schema).unwrap_err();
assert_eq!(err.count, 3);
}
}