use anyhow::Result;
use std::collections::{HashMap, HashSet};
const RDF_NS: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
const RDFS_NS: &str = "http://www.w3.org/2000/01/rdf-schema#";
const OWL_NS: &str = "http://www.w3.org/2002/07/owl#";
const XSD_NS: &str = "http://www.w3.org/2001/XMLSchema#";
fn rdf(local: &str) -> String {
format!("{RDF_NS}{local}")
}
fn rdfs(local: &str) -> String {
format!("{RDFS_NS}{local}")
}
fn owl(local: &str) -> String {
format!("{OWL_NS}{local}")
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct EntailmentTriple {
pub subject: String,
pub predicate: String,
pub object: String,
}
impl EntailmentTriple {
pub fn new(
subject: impl Into<String>,
predicate: impl Into<String>,
object: impl Into<String>,
) -> Self {
Self {
subject: subject.into(),
predicate: predicate.into(),
object: object.into(),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct Graph {
triples: HashSet<EntailmentTriple>,
pred_index: HashMap<String, Vec<(String, String)>>,
}
impl Graph {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, triple: EntailmentTriple) -> bool {
let is_new = self.triples.insert(triple.clone());
if is_new {
self.pred_index
.entry(triple.predicate.clone())
.or_default()
.push((triple.subject.clone(), triple.object.clone()));
}
is_new
}
pub fn add(&mut self, subject: &str, predicate: &str, object: &str) -> bool {
self.insert(EntailmentTriple::new(subject, predicate, object))
}
pub fn contains(&self, subject: &str, predicate: &str, object: &str) -> bool {
self.triples
.contains(&EntailmentTriple::new(subject, predicate, object))
}
pub fn contains_predicate(&self, predicate: &str) -> bool {
self.pred_index.contains_key(predicate)
}
pub fn triples_by_predicate(&self, predicate: &str) -> Vec<&(String, String)> {
self.pred_index
.get(predicate)
.map(|v| v.iter().collect())
.unwrap_or_default()
}
pub fn all_triples(&self) -> impl Iterator<Item = &EntailmentTriple> {
self.triples.iter()
}
pub fn len(&self) -> usize {
self.triples.len()
}
pub fn is_empty(&self) -> bool {
self.triples.is_empty()
}
pub fn objects_for(&self, subject: &str, predicate: &str) -> Vec<String> {
self.pred_index
.get(predicate)
.map(|pairs| {
pairs
.iter()
.filter(|(s, _)| s == subject)
.map(|(_, o)| o.clone())
.collect()
})
.unwrap_or_default()
}
pub fn subjects_for(&self, predicate: &str, object: &str) -> Vec<String> {
self.pred_index
.get(predicate)
.map(|pairs| {
pairs
.iter()
.filter(|(_, o)| o == object)
.map(|(s, _)| s.clone())
.collect()
})
.unwrap_or_default()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum EntailmentRegime {
Simple,
Rdf,
Rdfs,
Owl2Direct,
Owl2Rl,
Owl2El,
Owl2Ql,
D,
}
pub struct EntailmentEngine {
regime: EntailmentRegime,
}
impl EntailmentEngine {
pub fn new(regime: EntailmentRegime) -> Self {
Self { regime }
}
pub fn regime(&self) -> &EntailmentRegime {
&self.regime
}
pub fn materialize(&self, graph: &Graph) -> Result<Vec<EntailmentTriple>> {
match &self.regime {
EntailmentRegime::Simple => Ok(Vec::new()),
EntailmentRegime::Rdf => Ok(RdfEntailment::apply_rules(graph)),
EntailmentRegime::Rdfs => {
let mut result = RdfEntailment::apply_rules(graph);
result.extend(RdfsEntailment::apply_rules(graph));
Ok(result)
}
EntailmentRegime::Owl2Rl => {
let mut result = RdfEntailment::apply_rules(graph);
result.extend(RdfsEntailment::apply_rules(graph));
result.extend(Owl2RlEntailment::apply_rules(graph));
Ok(result)
}
EntailmentRegime::Owl2El => {
let mut result = RdfEntailment::apply_rules(graph);
result.extend(RdfsEntailment::apply_rules(graph));
result.extend(Owl2ElEntailment::apply_rules(graph));
Ok(result)
}
EntailmentRegime::Owl2Ql => {
let mut result = RdfEntailment::apply_rules(graph);
result.extend(RdfsEntailment::apply_rules(graph));
result.extend(Owl2QlEntailment::apply_rules(graph));
Ok(result)
}
EntailmentRegime::Owl2Direct => {
let mut result = RdfEntailment::apply_rules(graph);
result.extend(RdfsEntailment::apply_rules(graph));
result.extend(Owl2DirectEntailment::apply_rules(graph));
Ok(result)
}
EntailmentRegime::D => Ok(DEntailment::apply_rules(graph)),
}
}
pub fn is_entailed(&self, graph: &Graph, triple: &EntailmentTriple) -> Result<bool> {
if graph.contains(&triple.subject, &triple.predicate, &triple.object) {
return Ok(true);
}
let entailed = self.materialize(graph)?;
Ok(entailed.iter().any(|t| t == triple))
}
pub fn expand_query(&self, query: &str, _tbox: &Graph) -> Result<String> {
match &self.regime {
EntailmentRegime::Simple => Ok(query.to_string()),
EntailmentRegime::Rdf => {
Ok(query.to_string())
}
EntailmentRegime::Rdfs => {
Ok(format!("# RDFS entailment regime\n{query}"))
}
EntailmentRegime::Owl2Ql => {
Ok(format!("# OWL2-QL entailment regime\n{query}"))
}
EntailmentRegime::Owl2Rl => Ok(format!("# OWL2-RL entailment regime\n{query}")),
EntailmentRegime::Owl2El => Ok(format!("# OWL2-EL entailment regime\n{query}")),
EntailmentRegime::Owl2Direct => Ok(format!("# OWL2-Direct entailment regime\n{query}")),
EntailmentRegime::D => Ok(format!("# D-entailment regime\n{query}")),
}
}
}
pub struct RdfEntailment;
impl RdfEntailment {
pub fn apply_rules(graph: &Graph) -> Vec<EntailmentTriple> {
let mut result: HashSet<EntailmentTriple> = HashSet::new();
let rdf_type = rdf("type");
let rdf_property = rdf("Property");
for triple in graph.all_triples() {
let t = EntailmentTriple::new(&triple.predicate, &rdf_type, &rdf_property);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.insert(t);
}
}
let t = EntailmentTriple::new(&rdf_type, &rdf_type, &rdf_property);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.insert(t);
}
result.into_iter().collect()
}
}
pub struct RdfsEntailment;
impl RdfsEntailment {
pub fn apply_rules(graph: &Graph) -> Vec<EntailmentTriple> {
let mut working: HashSet<EntailmentTriple> = graph.all_triples().cloned().collect();
let mut new_triples: HashSet<EntailmentTriple> = HashSet::new();
loop {
let snapshot: Vec<EntailmentTriple> = working.iter().cloned().collect();
let tmp_graph = Self::build_working_graph(&snapshot);
let additions: Vec<EntailmentTriple> = [
Self::rule_rdfs2(&tmp_graph),
Self::rule_rdfs3(&tmp_graph),
Self::rule_rdfs5(&tmp_graph),
Self::rule_rdfs7(&tmp_graph),
Self::rule_rdfs9(&tmp_graph),
Self::rule_rdfs11(&tmp_graph),
]
.into_iter()
.flatten()
.collect();
let mut changed = false;
for t in additions {
if working.insert(t.clone()) {
new_triples.insert(t);
changed = true;
}
}
if !changed {
break;
}
}
new_triples
.into_iter()
.filter(|t| !graph.contains(&t.subject, &t.predicate, &t.object))
.collect()
}
fn build_working_graph(triples: &[EntailmentTriple]) -> Graph {
let mut g = Graph::new();
for t in triples {
g.insert(t.clone());
}
g
}
pub fn rule_rdfs2(graph: &Graph) -> Vec<EntailmentTriple> {
let mut result = Vec::new();
let domain_pred = rdfs("domain");
let rdf_type = rdf("type");
for (p, c) in graph.triples_by_predicate(&domain_pred) {
for triple in graph.all_triples() {
if &triple.predicate == p {
let t = EntailmentTriple::new(&triple.subject, &rdf_type, c);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
}
}
result
}
pub fn rule_rdfs3(graph: &Graph) -> Vec<EntailmentTriple> {
let mut result = Vec::new();
let range_pred = rdfs("range");
let rdf_type = rdf("type");
for (p, c) in graph.triples_by_predicate(&range_pred) {
for triple in graph.all_triples() {
if &triple.predicate == p {
let t = EntailmentTriple::new(&triple.object, &rdf_type, c);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
}
}
result
}
pub fn rule_rdfs5(graph: &Graph) -> Vec<EntailmentTriple> {
let mut result = Vec::new();
let sub_prop = rdfs("subPropertyOf");
for (p, q) in graph.triples_by_predicate(&sub_prop) {
for (q2, r) in graph.triples_by_predicate(&sub_prop) {
if q == q2 {
let t = EntailmentTriple::new(p, &sub_prop, r);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
}
}
result
}
pub fn rule_rdfs7(graph: &Graph) -> Vec<EntailmentTriple> {
let mut result = Vec::new();
let sub_prop = rdfs("subPropertyOf");
for (p, q) in graph.triples_by_predicate(&sub_prop) {
for triple in graph.all_triples() {
if &triple.predicate == p {
let t = EntailmentTriple::new(&triple.subject, q, &triple.object);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
}
}
result
}
pub fn rule_rdfs9(graph: &Graph) -> Vec<EntailmentTriple> {
let mut result = Vec::new();
let rdf_type = rdf("type");
let sub_class = rdfs("subClassOf");
for (c, d) in graph.triples_by_predicate(&sub_class) {
for x in graph.subjects_for(&rdf_type, c) {
let t = EntailmentTriple::new(&x, &rdf_type, d);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
}
result
}
pub fn rule_rdfs11(graph: &Graph) -> Vec<EntailmentTriple> {
let mut result = Vec::new();
let sub_class = rdfs("subClassOf");
for (c, d) in graph.triples_by_predicate(&sub_class) {
for (d2, e) in graph.triples_by_predicate(&sub_class) {
if d == d2 {
let t = EntailmentTriple::new(c, &sub_class, e);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
}
}
result
}
}
pub struct Owl2RlEntailment;
impl Owl2RlEntailment {
pub fn apply_rules(graph: &Graph) -> Vec<EntailmentTriple> {
let mut working: HashSet<EntailmentTriple> = graph.all_triples().cloned().collect();
let mut new_triples: HashSet<EntailmentTriple> = HashSet::new();
loop {
let snapshot: Vec<EntailmentTriple> = working.iter().cloned().collect();
let tmp_graph = Self::build_working_graph(&snapshot);
let additions: Vec<EntailmentTriple> = [
Self::rule_equivalent_class(&tmp_graph),
Self::rule_equivalent_property(&tmp_graph),
Self::rule_inverse_property(&tmp_graph),
Self::rule_transitive_property(&tmp_graph),
Self::rule_symmetric_property(&tmp_graph),
]
.into_iter()
.flatten()
.collect();
let mut changed = false;
for t in additions {
if working.insert(t.clone()) {
new_triples.insert(t);
changed = true;
}
}
if !changed {
break;
}
}
new_triples
.into_iter()
.filter(|t| !graph.contains(&t.subject, &t.predicate, &t.object))
.collect()
}
fn build_working_graph(triples: &[EntailmentTriple]) -> Graph {
let mut g = Graph::new();
for t in triples {
g.insert(t.clone());
}
g
}
fn rule_equivalent_class(graph: &Graph) -> Vec<EntailmentTriple> {
let mut result = Vec::new();
let equiv_class = owl("equivalentClass");
let sub_class = rdfs("subClassOf");
for (c, d) in graph.triples_by_predicate(&equiv_class) {
let t1 = EntailmentTriple::new(c, &sub_class, d);
let t2 = EntailmentTriple::new(d, &sub_class, c);
if !graph.contains(&t1.subject, &t1.predicate, &t1.object) {
result.push(t1);
}
if !graph.contains(&t2.subject, &t2.predicate, &t2.object) {
result.push(t2);
}
}
result
}
fn rule_equivalent_property(graph: &Graph) -> Vec<EntailmentTriple> {
let mut result = Vec::new();
let equiv_prop = owl("equivalentProperty");
let sub_prop = rdfs("subPropertyOf");
for (p, q) in graph.triples_by_predicate(&equiv_prop) {
let t1 = EntailmentTriple::new(p, &sub_prop, q);
let t2 = EntailmentTriple::new(q, &sub_prop, p);
if !graph.contains(&t1.subject, &t1.predicate, &t1.object) {
result.push(t1);
}
if !graph.contains(&t2.subject, &t2.predicate, &t2.object) {
result.push(t2);
}
}
result
}
fn rule_inverse_property(graph: &Graph) -> Vec<EntailmentTriple> {
let mut result = Vec::new();
let inverse_of = owl("inverseOf");
for (p, q) in graph.triples_by_predicate(&inverse_of) {
for triple in graph.all_triples() {
if &triple.predicate == p {
let t = EntailmentTriple::new(&triple.object, q, &triple.subject);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
if &triple.predicate == q {
let t = EntailmentTriple::new(&triple.object, p, &triple.subject);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
}
}
result
}
fn rule_transitive_property(graph: &Graph) -> Vec<EntailmentTriple> {
let mut result = Vec::new();
let transitive = owl("TransitiveProperty");
let rdf_type = rdf("type");
let transitive_props: Vec<String> = graph
.subjects_for(&rdf_type, &transitive)
.into_iter()
.collect();
for p in &transitive_props {
let pairs: Vec<(String, String)> = graph
.all_triples()
.filter(|t| &t.predicate == p)
.map(|t| (t.subject.clone(), t.object.clone()))
.collect();
for (x, y1) in &pairs {
for (y2, z) in &pairs {
if y1 == y2 && x != z {
let t = EntailmentTriple::new(x, p, z);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
}
}
}
result
}
fn rule_symmetric_property(graph: &Graph) -> Vec<EntailmentTriple> {
let mut result = Vec::new();
let symmetric = owl("SymmetricProperty");
let rdf_type = rdf("type");
let symmetric_props: Vec<String> = graph
.subjects_for(&rdf_type, &symmetric)
.into_iter()
.collect();
for p in &symmetric_props {
for triple in graph.all_triples() {
if &triple.predicate == p {
let t = EntailmentTriple::new(&triple.object, p, &triple.subject);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
}
}
result
}
}
pub struct Owl2ElEntailment;
impl Owl2ElEntailment {
pub fn apply_rules(graph: &Graph) -> Vec<EntailmentTriple> {
let mut result = Vec::new();
let rdf_type = rdf("type");
let owl_class = owl("Class");
let sub_class = rdfs("subClassOf");
let some_values = owl("someValuesFrom");
let on_property = owl("onProperty");
for (restriction, filler) in graph.triples_by_predicate(&some_values) {
let properties = graph.objects_for(restriction, &on_property);
for property in &properties {
for (c, _) in graph.triples_by_predicate(&sub_class) {
for x in graph.subjects_for(&rdf_type, c) {
for y in graph.objects_for(&x, property) {
let t = EntailmentTriple::new(&y, &rdf_type, filler);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
}
}
}
}
for triple in graph.all_triples() {
if triple.object == owl_class {
let t = EntailmentTriple::new(&triple.subject, &rdf_type, &owl_class);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
}
result
}
}
pub struct Owl2QlEntailment;
impl Owl2QlEntailment {
pub fn apply_rules(graph: &Graph) -> Vec<EntailmentTriple> {
let mut result = Vec::new();
let rdf_type = rdf("type");
let sub_class = rdfs("subClassOf");
let domain_pred = rdfs("domain");
let range_pred = rdfs("range");
for (p, c) in graph.triples_by_predicate(&domain_pred) {
for triple in graph.all_triples() {
if &triple.predicate == p {
let t = EntailmentTriple::new(&triple.subject, &rdf_type, c);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
}
}
for (p, c) in graph.triples_by_predicate(&range_pred) {
for triple in graph.all_triples() {
if &triple.predicate == p {
let t = EntailmentTriple::new(&triple.object, &rdf_type, c);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
}
}
for (c, d) in graph.triples_by_predicate(&sub_class) {
for x in graph.subjects_for(&rdf_type, c) {
let t = EntailmentTriple::new(&x, &rdf_type, d);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
}
result
}
}
pub struct Owl2DirectEntailment;
impl Owl2DirectEntailment {
pub fn apply_rules(graph: &Graph) -> Vec<EntailmentTriple> {
Owl2RlEntailment::apply_rules(graph)
}
}
pub struct DEntailment;
impl DEntailment {
pub fn apply_rules(graph: &Graph) -> Vec<EntailmentTriple> {
let mut result = Vec::new();
let rdf_type = rdf("type");
let xsd_integer = format!("{XSD_NS}integer");
let xsd_decimal = format!("{XSD_NS}decimal");
let xsd_long = format!("{XSD_NS}long");
let xsd_int = format!("{XSD_NS}int");
let sub_class = rdfs("subClassOf");
let datatypes_used: HashSet<String> = graph
.all_triples()
.filter(|t| t.predicate == rdf_type)
.map(|t| t.object.clone())
.collect();
if datatypes_used.contains(&xsd_integer) || datatypes_used.contains(&xsd_long) {
let t = EntailmentTriple::new(&xsd_integer, &sub_class, &xsd_decimal);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
if datatypes_used.contains(&xsd_long) {
let t = EntailmentTriple::new(&xsd_long, &sub_class, &xsd_integer);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
if datatypes_used.contains(&xsd_int) {
let t = EntailmentTriple::new(&xsd_int, &sub_class, &xsd_long);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
for (c, d) in graph.triples_by_predicate(&sub_class) {
for x in graph.subjects_for(&rdf_type, c) {
let t = EntailmentTriple::new(&x, &rdf_type, d);
if !graph.contains(&t.subject, &t.predicate, &t.object) {
result.push(t);
}
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_graph_insert_and_contains() {
let mut g = Graph::new();
g.add(
"http://example.org/a",
"http://example.org/p",
"http://example.org/b",
);
assert!(g.contains(
"http://example.org/a",
"http://example.org/p",
"http://example.org/b"
));
}
#[test]
fn test_graph_does_not_contain_missing() {
let g = Graph::new();
assert!(!g.contains(
"http://example.org/a",
"http://example.org/p",
"http://example.org/b"
));
}
#[test]
fn test_graph_len() {
let mut g = Graph::new();
g.add("s", "p", "o");
g.add("s2", "p", "o2");
assert_eq!(g.len(), 2);
}
#[test]
fn test_graph_is_empty() {
let g = Graph::new();
assert!(g.is_empty());
}
#[test]
fn test_graph_dedup_on_insert() {
let mut g = Graph::new();
g.add("s", "p", "o");
g.add("s", "p", "o");
assert_eq!(g.len(), 1);
}
#[test]
fn test_graph_objects_for() {
let mut g = Graph::new();
g.add("alice", "knows", "bob");
g.add("alice", "knows", "carol");
let objs = g.objects_for("alice", "knows");
assert_eq!(objs.len(), 2);
assert!(objs.contains(&"bob".to_string()));
assert!(objs.contains(&"carol".to_string()));
}
#[test]
fn test_graph_subjects_for() {
let mut g = Graph::new();
g.add("alice", "type", "Person");
g.add("bob", "type", "Person");
let subjs = g.subjects_for("type", "Person");
assert_eq!(subjs.len(), 2);
assert!(subjs.contains(&"alice".to_string()));
assert!(subjs.contains(&"bob".to_string()));
}
#[test]
fn test_graph_triples_by_predicate() {
let mut g = Graph::new();
g.add("s1", "p", "o1");
g.add("s2", "p", "o2");
let pairs = g.triples_by_predicate("p");
assert_eq!(pairs.len(), 2);
}
#[test]
fn test_engine_regime_simple() {
let engine = EntailmentEngine::new(EntailmentRegime::Simple);
assert_eq!(engine.regime(), &EntailmentRegime::Simple);
}
#[test]
fn test_engine_regime_rdfs() {
let engine = EntailmentEngine::new(EntailmentRegime::Rdfs);
assert_eq!(engine.regime(), &EntailmentRegime::Rdfs);
}
#[test]
fn test_simple_entailment_no_new_triples() -> Result<(), Box<dyn std::error::Error>> {
let engine = EntailmentEngine::new(EntailmentRegime::Simple);
let g = Graph::new();
let result = engine.materialize(&g)?;
assert!(result.is_empty());
Ok(())
}
#[test]
fn test_simple_entailment_explicit_triple_is_entailed() -> Result<(), Box<dyn std::error::Error>>
{
let engine = EntailmentEngine::new(EntailmentRegime::Simple);
let mut g = Graph::new();
g.add(
"http://example.org/a",
"http://example.org/p",
"http://example.org/b",
);
let triple = EntailmentTriple::new(
"http://example.org/a",
"http://example.org/p",
"http://example.org/b",
);
assert!(engine.is_entailed(&g, &triple)?);
Ok(())
}
#[test]
fn test_simple_entailment_implicit_triple_not_entailed(
) -> Result<(), Box<dyn std::error::Error>> {
let engine = EntailmentEngine::new(EntailmentRegime::Simple);
let mut g = Graph::new();
g.add("alice", &rdf("type"), "Person");
g.add("Person", &rdfs("subClassOf"), "Animal");
let triple = EntailmentTriple::new("alice", rdf("type"), "Animal");
assert!(!engine.is_entailed(&g, &triple)?);
Ok(())
}
#[test]
fn test_rdf_entailment_predicates_become_properties() -> Result<(), Box<dyn std::error::Error>>
{
let mut g = Graph::new();
g.add(
"http://example.org/alice",
"http://xmlns.com/foaf/0.1/knows",
"http://example.org/bob",
);
let engine = EntailmentEngine::new(EntailmentRegime::Rdf);
let new_triples = engine.materialize(&g)?;
assert!(new_triples.iter().any(|t| {
t.subject == "http://xmlns.com/foaf/0.1/knows"
&& t.predicate == rdf("type")
&& t.object == rdf("Property")
}));
Ok(())
}
#[test]
fn test_rdf_entailment_empty_graph_yields_type_triple() {
let g = Graph::new();
let result = RdfEntailment::apply_rules(&g);
assert!(result.iter().any(|t| t.object == rdf("Property")));
}
#[test]
fn test_rdfs2_domain_inference() {
let mut g = Graph::new();
g.add(
"http://example.org/hasAge",
&rdfs("domain"),
"http://example.org/Person",
);
g.add(
"http://example.org/alice",
"http://example.org/hasAge",
"30",
);
let new = RdfsEntailment::rule_rdfs2(&g);
assert!(new.iter().any(|t| {
t.subject == "http://example.org/alice"
&& t.predicate == rdf("type")
&& t.object == "http://example.org/Person"
}));
}
#[test]
fn test_rdfs2_no_inference_without_domain() {
let mut g = Graph::new();
g.add("alice", "knows", "bob");
let new = RdfsEntailment::rule_rdfs2(&g);
assert!(new.is_empty());
}
#[test]
fn test_rdfs3_range_inference() {
let mut g = Graph::new();
g.add(
"http://example.org/knows",
&rdfs("range"),
"http://example.org/Person",
);
g.add(
"http://example.org/alice",
"http://example.org/knows",
"http://example.org/bob",
);
let new = RdfsEntailment::rule_rdfs3(&g);
assert!(new.iter().any(|t| {
t.subject == "http://example.org/bob"
&& t.predicate == rdf("type")
&& t.object == "http://example.org/Person"
}));
}
#[test]
fn test_rdfs3_no_inference_without_range() {
let mut g = Graph::new();
g.add("alice", "knows", "bob");
let new = RdfsEntailment::rule_rdfs3(&g);
assert!(new.is_empty());
}
#[test]
fn test_rdfs5_sub_property_transitivity() {
let mut g = Graph::new();
g.add("hasMother", &rdfs("subPropertyOf"), "hasParent");
g.add("hasParent", &rdfs("subPropertyOf"), "hasAncestor");
let new = RdfsEntailment::rule_rdfs5(&g);
assert!(new.iter().any(|t| {
t.subject == "hasMother"
&& t.predicate == rdfs("subPropertyOf")
&& t.object == "hasAncestor"
}));
}
#[test]
fn test_rdfs5_no_transitivity_for_unrelated() {
let mut g = Graph::new();
g.add("p1", &rdfs("subPropertyOf"), "q1");
g.add("p2", &rdfs("subPropertyOf"), "q2");
let new = RdfsEntailment::rule_rdfs5(&g);
assert!(!new.iter().any(|t| t.subject == "p1" && t.object == "q2"));
}
#[test]
fn test_rdfs7_sub_property_inheritance() {
let mut g = Graph::new();
g.add("hasMother", &rdfs("subPropertyOf"), "hasParent");
g.add("alice", "hasMother", "eve");
let new = RdfsEntailment::rule_rdfs7(&g);
assert!(new
.iter()
.any(|t| { t.subject == "alice" && t.predicate == "hasParent" && t.object == "eve" }));
}
#[test]
fn test_rdfs9_sub_class_type_inference() {
let mut g = Graph::new();
g.add(
"http://example.org/Dog",
&rdfs("subClassOf"),
"http://example.org/Animal",
);
g.add(
"http://example.org/fido",
&rdf("type"),
"http://example.org/Dog",
);
let new = RdfsEntailment::rule_rdfs9(&g);
assert!(new.iter().any(|t| {
t.subject == "http://example.org/fido"
&& t.predicate == rdf("type")
&& t.object == "http://example.org/Animal"
}));
}
#[test]
fn test_rdfs9_no_inference_without_type() {
let mut g = Graph::new();
g.add("Dog", &rdfs("subClassOf"), "Animal");
let new = RdfsEntailment::rule_rdfs9(&g);
assert!(new.is_empty());
}
#[test]
fn test_rdfs11_sub_class_transitivity() {
let mut g = Graph::new();
g.add("Poodle", &rdfs("subClassOf"), "Dog");
g.add("Dog", &rdfs("subClassOf"), "Animal");
let new = RdfsEntailment::rule_rdfs11(&g);
assert!(new.iter().any(|t| {
t.subject == "Poodle" && t.predicate == rdfs("subClassOf") && t.object == "Animal"
}));
}
#[test]
fn test_rdfs11_no_transitivity_unrelated() {
let mut g = Graph::new();
g.add("A", &rdfs("subClassOf"), "B");
g.add("C", &rdfs("subClassOf"), "D");
let new = RdfsEntailment::rule_rdfs11(&g);
assert!(!new.iter().any(|t| t.subject == "A" && t.object == "D"));
}
#[test]
fn test_rdfs_full_materialize_chain() -> Result<(), Box<dyn std::error::Error>> {
let mut g = Graph::new();
g.add("Poodle", &rdfs("subClassOf"), "Dog");
g.add("Dog", &rdfs("subClassOf"), "Animal");
g.add("fido", &rdf("type"), "Poodle");
let engine = EntailmentEngine::new(EntailmentRegime::Rdfs);
let new = engine.materialize(&g)?;
assert!(new
.iter()
.any(|t| { t.subject == "fido" && t.predicate == rdf("type") && t.object == "Dog" }));
assert!(new.iter().any(|t| {
t.subject == "fido" && t.predicate == rdf("type") && t.object == "Animal"
}));
Ok(())
}
#[test]
fn test_rdfs_is_entailed_chain() -> Result<(), Box<dyn std::error::Error>> {
let mut g = Graph::new();
g.add("Cat", &rdfs("subClassOf"), "Mammal");
g.add("Mammal", &rdfs("subClassOf"), "Animal");
g.add("whiskers", &rdf("type"), "Cat");
let engine = EntailmentEngine::new(EntailmentRegime::Rdfs);
let triple = EntailmentTriple::new("whiskers", rdf("type"), "Animal");
assert!(engine.is_entailed(&g, &triple)?);
Ok(())
}
#[test]
fn test_rdfs_is_not_entailed_without_facts() -> Result<(), Box<dyn std::error::Error>> {
let mut g = Graph::new();
g.add("Cat", &rdfs("subClassOf"), "Mammal");
let engine = EntailmentEngine::new(EntailmentRegime::Rdfs);
let triple = EntailmentTriple::new("whiskers", rdf("type"), "Mammal");
assert!(!engine.is_entailed(&g, &triple)?);
Ok(())
}
#[test]
fn test_owl_rl_equivalent_class() {
let mut g = Graph::new();
g.add("ClassA", &owl("equivalentClass"), "ClassB");
let new = Owl2RlEntailment::apply_rules(&g);
assert!(new.iter().any(|t| {
t.subject == "ClassA" && t.predicate == rdfs("subClassOf") && t.object == "ClassB"
}));
assert!(new.iter().any(|t| {
t.subject == "ClassB" && t.predicate == rdfs("subClassOf") && t.object == "ClassA"
}));
}
#[test]
fn test_owl_rl_equivalent_property() {
let mut g = Graph::new();
g.add("propA", &owl("equivalentProperty"), "propB");
let new = Owl2RlEntailment::apply_rules(&g);
assert!(new.iter().any(|t| {
t.subject == "propA" && t.predicate == rdfs("subPropertyOf") && t.object == "propB"
}));
}
#[test]
fn test_owl_rl_inverse_property() -> Result<(), Box<dyn std::error::Error>> {
let mut g = Graph::new();
g.add("hasMother", &owl("inverseOf"), "isMotherOf");
g.add("alice", "hasMother", "eve");
let engine = EntailmentEngine::new(EntailmentRegime::Owl2Rl);
let new = engine.materialize(&g)?;
assert!(new
.iter()
.any(|t| { t.subject == "eve" && t.predicate == "isMotherOf" && t.object == "alice" }));
Ok(())
}
#[test]
fn test_owl_rl_transitive_property() -> Result<(), Box<dyn std::error::Error>> {
let mut g = Graph::new();
g.add("ancestor", &rdf("type"), &owl("TransitiveProperty"));
g.add("alice", "ancestor", "bob");
g.add("bob", "ancestor", "carol");
let engine = EntailmentEngine::new(EntailmentRegime::Owl2Rl);
let new = engine.materialize(&g)?;
assert!(new
.iter()
.any(|t| { t.subject == "alice" && t.predicate == "ancestor" && t.object == "carol" }));
Ok(())
}
#[test]
fn test_owl_rl_symmetric_property() -> Result<(), Box<dyn std::error::Error>> {
let mut g = Graph::new();
g.add("marriedTo", &rdf("type"), &owl("SymmetricProperty"));
g.add("alice", "marriedTo", "bob");
let engine = EntailmentEngine::new(EntailmentRegime::Owl2Rl);
let new = engine.materialize(&g)?;
assert!(new
.iter()
.any(|t| { t.subject == "bob" && t.predicate == "marriedTo" && t.object == "alice" }));
Ok(())
}
#[test]
fn test_is_entailed_explicit_triple_all_regimes() -> Result<(), Box<dyn std::error::Error>> {
let mut g = Graph::new();
g.add("s", "p", "o");
let triple = EntailmentTriple::new("s", "p", "o");
for regime in &[
EntailmentRegime::Simple,
EntailmentRegime::Rdf,
EntailmentRegime::Rdfs,
EntailmentRegime::Owl2Rl,
] {
let engine = EntailmentEngine::new(regime.clone());
assert!(
engine.is_entailed(&g, &triple)?,
"explicit triple must be entailed under {regime:?}"
);
}
Ok(())
}
#[test]
fn test_is_entailed_inferred_triple_rdfs() -> Result<(), Box<dyn std::error::Error>> {
let mut g = Graph::new();
g.add("Dog", &rdfs("subClassOf"), "Animal");
g.add("rex", &rdf("type"), "Dog");
let engine = EntailmentEngine::new(EntailmentRegime::Rdfs);
let triple = EntailmentTriple::new("rex", rdf("type"), "Animal");
assert!(engine.is_entailed(&g, &triple)?);
Ok(())
}
#[test]
fn test_is_not_entailed_non_existing_triple() -> Result<(), Box<dyn std::error::Error>> {
let g = Graph::new();
let engine = EntailmentEngine::new(EntailmentRegime::Rdfs);
let triple = EntailmentTriple::new("a", "b", "c");
assert!(!engine.is_entailed(&g, &triple)?);
Ok(())
}
#[test]
fn test_query_expansion_simple_passthrough() -> Result<(), Box<dyn std::error::Error>> {
let engine = EntailmentEngine::new(EntailmentRegime::Simple);
let g = Graph::new();
let q = "SELECT * WHERE { ?s ?p ?o }";
let expanded = engine.expand_query(q, &g)?;
assert_eq!(expanded, q);
Ok(())
}
#[test]
fn test_query_expansion_rdfs_annotates() -> Result<(), Box<dyn std::error::Error>> {
let engine = EntailmentEngine::new(EntailmentRegime::Rdfs);
let g = Graph::new();
let q = "SELECT * WHERE { ?s a ?t }";
let expanded = engine.expand_query(q, &g)?;
assert!(expanded.contains("RDFS entailment"));
Ok(())
}
#[test]
fn test_query_expansion_owl_ql_annotates() -> Result<(), Box<dyn std::error::Error>> {
let engine = EntailmentEngine::new(EntailmentRegime::Owl2Ql);
let g = Graph::new();
let q = "SELECT * WHERE { ?s ?p ?o }";
let expanded = engine.expand_query(q, &g)?;
assert!(expanded.contains("OWL2-QL"));
Ok(())
}
#[test]
fn test_query_expansion_owl_rl_annotates() -> Result<(), Box<dyn std::error::Error>> {
let engine = EntailmentEngine::new(EntailmentRegime::Owl2Rl);
let g = Graph::new();
let q = "SELECT * WHERE { ?s ?p ?o }";
let expanded = engine.expand_query(q, &g)?;
assert!(expanded.contains("OWL2-RL"));
Ok(())
}
#[test]
fn test_query_expansion_d_entailment_annotates() -> Result<(), Box<dyn std::error::Error>> {
let engine = EntailmentEngine::new(EntailmentRegime::D);
let g = Graph::new();
let q = "SELECT * WHERE { ?s ?p ?o }";
let expanded = engine.expand_query(q, &g)?;
assert!(expanded.contains("D-entailment"));
Ok(())
}
#[test]
fn test_owl_ql_domain_range_inference() {
let mut g = Graph::new();
g.add("worksFor", &rdfs("domain"), "Employee");
g.add("worksFor", &rdfs("range"), "Company");
g.add("alice", "worksFor", "acme");
let new = Owl2QlEntailment::apply_rules(&g);
assert!(new.iter().any(|t| {
t.subject == "alice" && t.predicate == rdf("type") && t.object == "Employee"
}));
assert!(new.iter().any(|t| {
t.subject == "acme" && t.predicate == rdf("type") && t.object == "Company"
}));
}
#[test]
fn test_d_entailment_integer_subtype_of_decimal() {
let xsd_integer = format!("{XSD_NS}integer");
let xsd_decimal = format!("{XSD_NS}decimal");
let mut g = Graph::new();
g.add("42_literal", &rdf("type"), &xsd_integer);
let new = DEntailment::apply_rules(&g);
assert!(new.iter().any(|t| {
t.subject == xsd_integer && t.predicate == rdfs("subClassOf") && t.object == xsd_decimal
}));
}
#[test]
fn test_d_entailment_long_subtype_of_integer() {
let xsd_long = format!("{XSD_NS}long");
let xsd_integer = format!("{XSD_NS}integer");
let mut g = Graph::new();
g.add("val", &rdf("type"), &xsd_long);
let new = DEntailment::apply_rules(&g);
assert!(new.iter().any(|t| {
t.subject == xsd_long && t.predicate == rdfs("subClassOf") && t.object == xsd_integer
}));
}
#[test]
fn test_owl_rl_extends_rdfs() -> Result<(), Box<dyn std::error::Error>> {
let mut g = Graph::new();
g.add("Dog", &rdfs("subClassOf"), "Animal");
g.add("rex", &rdf("type"), "Dog");
g.add("knows", &rdf("type"), &owl("SymmetricProperty"));
g.add("alice", "knows", "bob");
let engine = EntailmentEngine::new(EntailmentRegime::Owl2Rl);
let new = engine.materialize(&g)?;
assert!(new
.iter()
.any(|t| { t.subject == "rex" && t.predicate == rdf("type") && t.object == "Animal" }));
assert!(new
.iter()
.any(|t| { t.subject == "bob" && t.predicate == "knows" && t.object == "alice" }));
Ok(())
}
#[test]
fn test_entailment_triple_equality() {
let t1 = EntailmentTriple::new("s", "p", "o");
let t2 = EntailmentTriple::new("s", "p", "o");
assert_eq!(t1, t2);
}
#[test]
fn test_entailment_triple_inequality() {
let t1 = EntailmentTriple::new("s", "p", "o1");
let t2 = EntailmentTriple::new("s", "p", "o2");
assert_ne!(t1, t2);
}
#[test]
fn test_rdf_helper_produces_correct_iri() {
assert_eq!(
rdf("type"),
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
);
}
#[test]
fn test_rdfs_helper_produces_correct_iri() {
assert_eq!(
rdfs("subClassOf"),
"http://www.w3.org/2000/01/rdf-schema#subClassOf"
);
}
#[test]
fn test_owl_helper_produces_correct_iri() {
assert_eq!(owl("Class"), "http://www.w3.org/2002/07/owl#Class");
}
}