use crate::{Rule, RuleAtom, RuleEngine, Term};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use tracing::{debug, info, trace};
pub mod vocabulary {
pub const RDF_TYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
pub const RDFS_SUBCLASS_OF: &str = "http://www.w3.org/2000/01/rdf-schema#subClassOf";
pub const RDFS_SUBPROPERTY_OF: &str = "http://www.w3.org/2000/01/rdf-schema#subPropertyOf";
pub const RDFS_DOMAIN: &str = "http://www.w3.org/2000/01/rdf-schema#domain";
pub const RDFS_RANGE: &str = "http://www.w3.org/2000/01/rdf-schema#range";
pub const RDFS_CLASS: &str = "http://www.w3.org/2000/01/rdf-schema#Class";
pub const RDFS_RESOURCE: &str = "http://www.w3.org/2000/01/rdf-schema#Resource";
pub const RDFS_LITERAL: &str = "http://www.w3.org/2000/01/rdf-schema#Literal";
pub const RDFS_DATATYPE: &str = "http://www.w3.org/2000/01/rdf-schema#Datatype";
pub const RDF_PROPERTY: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#Property";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RdfsRule {
Rdfs1,
Rdfs2,
Rdfs3,
Rdfs4a,
Rdfs4b,
Rdfs5,
Rdfs6,
Rdfs7,
Rdfs8,
Rdfs9,
Rdfs10,
Rdfs11,
Rdfs13,
}
impl RdfsRule {
pub fn all() -> &'static [RdfsRule] {
&[
RdfsRule::Rdfs1,
RdfsRule::Rdfs2,
RdfsRule::Rdfs3,
RdfsRule::Rdfs4a,
RdfsRule::Rdfs4b,
RdfsRule::Rdfs5,
RdfsRule::Rdfs6,
RdfsRule::Rdfs7,
RdfsRule::Rdfs8,
RdfsRule::Rdfs9,
RdfsRule::Rdfs10,
RdfsRule::Rdfs11,
RdfsRule::Rdfs13,
]
}
pub fn minimal() -> &'static [RdfsRule] {
&[
RdfsRule::Rdfs2,
RdfsRule::Rdfs3,
RdfsRule::Rdfs5,
RdfsRule::Rdfs7,
RdfsRule::Rdfs9,
RdfsRule::Rdfs11,
RdfsRule::Rdfs13,
]
}
pub fn noisy() -> &'static [RdfsRule] {
&[
RdfsRule::Rdfs1,
RdfsRule::Rdfs4a,
RdfsRule::Rdfs4b,
RdfsRule::Rdfs6,
RdfsRule::Rdfs8,
RdfsRule::Rdfs10,
]
}
pub fn name(&self) -> &'static str {
match self {
RdfsRule::Rdfs1 => "rdfs1",
RdfsRule::Rdfs2 => "rdfs2",
RdfsRule::Rdfs3 => "rdfs3",
RdfsRule::Rdfs4a => "rdfs4a",
RdfsRule::Rdfs4b => "rdfs4b",
RdfsRule::Rdfs5 => "rdfs5",
RdfsRule::Rdfs6 => "rdfs6",
RdfsRule::Rdfs7 => "rdfs7",
RdfsRule::Rdfs8 => "rdfs8",
RdfsRule::Rdfs9 => "rdfs9",
RdfsRule::Rdfs10 => "rdfs10",
RdfsRule::Rdfs11 => "rdfs11",
RdfsRule::Rdfs13 => "rdfs13",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum RdfsProfile {
#[default]
Minimal,
Full,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RdfsConfig {
pub enabled_rules: HashSet<RdfsRule>,
}
impl Default for RdfsConfig {
fn default() -> Self {
Self::from_profile(RdfsProfile::Minimal)
}
}
impl RdfsConfig {
pub fn from_profile(profile: RdfsProfile) -> Self {
let enabled_rules = match profile {
RdfsProfile::Minimal => RdfsRule::minimal().iter().copied().collect(),
RdfsProfile::Full => RdfsRule::all().iter().copied().collect(),
RdfsProfile::None => HashSet::new(),
};
Self { enabled_rules }
}
pub fn full() -> Self {
Self::from_profile(RdfsProfile::Full)
}
pub fn minimal() -> Self {
Self::from_profile(RdfsProfile::Minimal)
}
pub fn none() -> Self {
Self::from_profile(RdfsProfile::None)
}
pub fn is_enabled(&self, rule: RdfsRule) -> bool {
self.enabled_rules.contains(&rule)
}
pub fn enable(&mut self, rule: RdfsRule) {
self.enabled_rules.insert(rule);
}
pub fn disable(&mut self, rule: RdfsRule) {
self.enabled_rules.remove(&rule);
}
}
#[derive(Debug, Clone)]
pub struct RdfsReasonerBuilder {
config: RdfsConfig,
}
impl Default for RdfsReasonerBuilder {
fn default() -> Self {
Self::new()
}
}
impl RdfsReasonerBuilder {
pub fn new() -> Self {
Self {
config: RdfsConfig::default(),
}
}
pub fn with_profile(mut self, profile: RdfsProfile) -> Self {
self.config = RdfsConfig::from_profile(profile);
self
}
pub fn enable_rule(mut self, rule: RdfsRule) -> Self {
self.config.enable(rule);
self
}
pub fn disable_rule(mut self, rule: RdfsRule) -> Self {
self.config.disable(rule);
self
}
pub fn enable_rules(mut self, rules: &[RdfsRule]) -> Self {
for rule in rules {
self.config.enable(*rule);
}
self
}
pub fn disable_rules(mut self, rules: &[RdfsRule]) -> Self {
for rule in rules {
self.config.disable(*rule);
}
self
}
pub fn build(self) -> RdfsReasoner {
RdfsReasoner::with_config(self.config)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RdfsContext {
pub class_hierarchy: HashMap<String, HashSet<String>>,
pub property_hierarchy: HashMap<String, HashSet<String>>,
pub property_domains: HashMap<String, HashSet<String>>,
pub property_ranges: HashMap<String, HashSet<String>>,
pub classes: HashSet<String>,
pub properties: HashSet<String>,
}
impl Default for RdfsContext {
fn default() -> Self {
let mut context = Self {
class_hierarchy: HashMap::new(),
property_hierarchy: HashMap::new(),
property_domains: HashMap::new(),
property_ranges: HashMap::new(),
classes: HashSet::new(),
properties: HashSet::new(),
};
context.initialize_builtin_vocabulary();
context
}
}
impl RdfsContext {
fn initialize_builtin_vocabulary(&mut self) {
use vocabulary::*;
self.classes.insert(RDFS_CLASS.to_string());
self.classes.insert(RDFS_RESOURCE.to_string());
self.classes.insert(RDFS_LITERAL.to_string());
self.classes.insert(RDFS_DATATYPE.to_string());
self.properties.insert(RDF_TYPE.to_string());
self.properties.insert(RDFS_SUBCLASS_OF.to_string());
self.properties.insert(RDFS_SUBPROPERTY_OF.to_string());
self.properties.insert(RDFS_DOMAIN.to_string());
self.properties.insert(RDFS_RANGE.to_string());
self.add_subclass_relation(RDFS_CLASS, RDFS_RESOURCE);
self.add_subclass_relation(RDFS_DATATYPE, RDFS_CLASS);
self.add_property_domain(RDFS_SUBCLASS_OF, RDFS_CLASS);
self.add_property_range(RDFS_SUBCLASS_OF, RDFS_CLASS);
self.add_property_domain(RDFS_SUBPROPERTY_OF, RDF_PROPERTY);
self.add_property_range(RDFS_SUBPROPERTY_OF, RDF_PROPERTY);
self.add_property_domain(RDFS_DOMAIN, RDF_PROPERTY);
self.add_property_range(RDFS_DOMAIN, RDFS_CLASS);
self.add_property_domain(RDFS_RANGE, RDF_PROPERTY);
self.add_property_range(RDFS_RANGE, RDFS_CLASS);
}
pub fn add_subclass_relation(&mut self, subclass: &str, superclass: &str) {
self.class_hierarchy
.entry(subclass.to_string())
.or_default()
.insert(superclass.to_string());
}
pub fn add_subproperty_relation(&mut self, subproperty: &str, superproperty: &str) {
self.property_hierarchy
.entry(subproperty.to_string())
.or_default()
.insert(superproperty.to_string());
}
pub fn add_property_domain(&mut self, property: &str, domain: &str) {
self.property_domains
.entry(property.to_string())
.or_default()
.insert(domain.to_string());
}
pub fn add_property_range(&mut self, property: &str, range: &str) {
self.property_ranges
.entry(property.to_string())
.or_default()
.insert(range.to_string());
}
pub fn get_superclasses(&self, class: &str) -> HashSet<String> {
let mut superclasses = HashSet::new();
let mut to_visit = vec![class.to_string()];
let mut visited = HashSet::new();
while let Some(current) = to_visit.pop() {
if visited.contains(¤t) {
continue;
}
visited.insert(current.clone());
if let Some(direct_superclasses) = self.class_hierarchy.get(¤t) {
for superclass in direct_superclasses {
superclasses.insert(superclass.clone());
to_visit.push(superclass.clone());
}
}
}
superclasses
}
pub fn get_superproperties(&self, property: &str) -> HashSet<String> {
let mut superproperties = HashSet::new();
let mut to_visit = vec![property.to_string()];
let mut visited = HashSet::new();
while let Some(current) = to_visit.pop() {
if visited.contains(¤t) {
continue;
}
visited.insert(current.clone());
if let Some(direct_superproperties) = self.property_hierarchy.get(¤t) {
for superproperty in direct_superproperties {
superproperties.insert(superproperty.clone());
to_visit.push(superproperty.clone());
}
}
}
superproperties
}
pub fn is_subclass_of(&self, subclass: &str, superclass: &str) -> bool {
if subclass == superclass {
return true;
}
self.get_superclasses(subclass).contains(superclass)
}
pub fn is_subproperty_of(&self, subproperty: &str, superproperty: &str) -> bool {
if subproperty == superproperty {
return true;
}
self.get_superproperties(subproperty)
.contains(superproperty)
}
}
#[derive(Debug)]
pub struct RdfsReasoner {
pub context: RdfsContext,
pub rule_engine: RuleEngine,
pub config: RdfsConfig,
}
impl Default for RdfsReasoner {
fn default() -> Self {
Self::new()
}
}
impl RdfsReasoner {
pub fn new() -> Self {
Self::with_config(RdfsConfig::default())
}
pub fn with_profile(profile: RdfsProfile) -> Self {
Self::with_config(RdfsConfig::from_profile(profile))
}
pub fn with_config(config: RdfsConfig) -> Self {
let mut reasoner = Self {
context: RdfsContext::default(),
rule_engine: RuleEngine::new(),
config,
};
reasoner.initialize_rdfs_rules();
reasoner
}
pub fn context_only() -> Self {
Self::with_profile(RdfsProfile::None)
}
pub fn builder() -> RdfsReasonerBuilder {
RdfsReasonerBuilder::new()
}
pub fn get_config(&self) -> &RdfsConfig {
&self.config
}
pub fn is_rule_enabled(&self, rule: RdfsRule) -> bool {
self.config.is_enabled(rule)
}
fn initialize_rdfs_rules(&mut self) {
use vocabulary::*;
if self.config.is_enabled(RdfsRule::Rdfs1) {
self.rule_engine.add_rule(Rule {
name: "rdfs1".to_string(),
body: vec![RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Variable("a".to_string()),
object: Term::Variable("y".to_string()),
}],
head: vec![RuleAtom::Triple {
subject: Term::Variable("a".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(RDF_PROPERTY.to_string()),
}],
});
}
if self.config.is_enabled(RdfsRule::Rdfs2) {
self.rule_engine.add_rule(Rule {
name: "rdfs2".to_string(),
body: vec![
RuleAtom::Triple {
subject: Term::Variable("p".to_string()),
predicate: Term::Constant(RDFS_DOMAIN.to_string()),
object: Term::Variable("c".to_string()),
},
RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Variable("p".to_string()),
object: Term::Variable("y".to_string()),
},
],
head: vec![RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Variable("c".to_string()),
}],
});
}
if self.config.is_enabled(RdfsRule::Rdfs3) {
self.rule_engine.add_rule(Rule {
name: "rdfs3".to_string(),
body: vec![
RuleAtom::Triple {
subject: Term::Variable("p".to_string()),
predicate: Term::Constant(RDFS_RANGE.to_string()),
object: Term::Variable("c".to_string()),
},
RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Variable("p".to_string()),
object: Term::Variable("y".to_string()),
},
],
head: vec![RuleAtom::Triple {
subject: Term::Variable("y".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Variable("c".to_string()),
}],
});
}
if self.config.is_enabled(RdfsRule::Rdfs4a) {
self.rule_engine.add_rule(Rule {
name: "rdfs4a".to_string(),
body: vec![RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Variable("a".to_string()),
object: Term::Variable("y".to_string()),
}],
head: vec![RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(RDFS_RESOURCE.to_string()),
}],
});
}
if self.config.is_enabled(RdfsRule::Rdfs4b) {
self.rule_engine.add_rule(Rule {
name: "rdfs4b".to_string(),
body: vec![RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Variable("a".to_string()),
object: Term::Variable("y".to_string()),
}],
head: vec![RuleAtom::Triple {
subject: Term::Variable("y".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(RDFS_RESOURCE.to_string()),
}],
});
}
if self.config.is_enabled(RdfsRule::Rdfs5) {
self.rule_engine.add_rule(Rule {
name: "rdfs5".to_string(),
body: vec![
RuleAtom::Triple {
subject: Term::Variable("p".to_string()),
predicate: Term::Constant(RDFS_SUBPROPERTY_OF.to_string()),
object: Term::Variable("q".to_string()),
},
RuleAtom::Triple {
subject: Term::Variable("q".to_string()),
predicate: Term::Constant(RDFS_SUBPROPERTY_OF.to_string()),
object: Term::Variable("r".to_string()),
},
],
head: vec![RuleAtom::Triple {
subject: Term::Variable("p".to_string()),
predicate: Term::Constant(RDFS_SUBPROPERTY_OF.to_string()),
object: Term::Variable("r".to_string()),
}],
});
}
if self.config.is_enabled(RdfsRule::Rdfs6) {
self.rule_engine.add_rule(Rule {
name: "rdfs6".to_string(),
body: vec![RuleAtom::Triple {
subject: Term::Variable("p".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(RDF_PROPERTY.to_string()),
}],
head: vec![RuleAtom::Triple {
subject: Term::Variable("p".to_string()),
predicate: Term::Constant(RDFS_SUBPROPERTY_OF.to_string()),
object: Term::Variable("p".to_string()),
}],
});
}
if self.config.is_enabled(RdfsRule::Rdfs7) {
self.rule_engine.add_rule(Rule {
name: "rdfs7".to_string(),
body: vec![
RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Variable("p".to_string()),
object: Term::Variable("y".to_string()),
},
RuleAtom::Triple {
subject: Term::Variable("p".to_string()),
predicate: Term::Constant(RDFS_SUBPROPERTY_OF.to_string()),
object: Term::Variable("q".to_string()),
},
],
head: vec![RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Variable("q".to_string()),
object: Term::Variable("y".to_string()),
}],
});
}
if self.config.is_enabled(RdfsRule::Rdfs8) {
self.rule_engine.add_rule(Rule {
name: "rdfs8".to_string(),
body: vec![RuleAtom::Triple {
subject: Term::Variable("c".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(RDFS_CLASS.to_string()),
}],
head: vec![RuleAtom::Triple {
subject: Term::Variable("c".to_string()),
predicate: Term::Constant(RDFS_SUBCLASS_OF.to_string()),
object: Term::Constant(RDFS_RESOURCE.to_string()),
}],
});
}
if self.config.is_enabled(RdfsRule::Rdfs9) {
self.rule_engine.add_rule(Rule {
name: "rdfs9".to_string(),
body: vec![
RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Variable("c".to_string()),
},
RuleAtom::Triple {
subject: Term::Variable("c".to_string()),
predicate: Term::Constant(RDFS_SUBCLASS_OF.to_string()),
object: Term::Variable("d".to_string()),
},
],
head: vec![RuleAtom::Triple {
subject: Term::Variable("x".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Variable("d".to_string()),
}],
});
}
if self.config.is_enabled(RdfsRule::Rdfs10) {
self.rule_engine.add_rule(Rule {
name: "rdfs10".to_string(),
body: vec![RuleAtom::Triple {
subject: Term::Variable("c".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(RDFS_CLASS.to_string()),
}],
head: vec![RuleAtom::Triple {
subject: Term::Variable("c".to_string()),
predicate: Term::Constant(RDFS_SUBCLASS_OF.to_string()),
object: Term::Variable("c".to_string()),
}],
});
}
if self.config.is_enabled(RdfsRule::Rdfs11) {
self.rule_engine.add_rule(Rule {
name: "rdfs11".to_string(),
body: vec![
RuleAtom::Triple {
subject: Term::Variable("c".to_string()),
predicate: Term::Constant(RDFS_SUBCLASS_OF.to_string()),
object: Term::Variable("d".to_string()),
},
RuleAtom::Triple {
subject: Term::Variable("d".to_string()),
predicate: Term::Constant(RDFS_SUBCLASS_OF.to_string()),
object: Term::Variable("e".to_string()),
},
],
head: vec![RuleAtom::Triple {
subject: Term::Variable("c".to_string()),
predicate: Term::Constant(RDFS_SUBCLASS_OF.to_string()),
object: Term::Variable("e".to_string()),
}],
});
}
if self.config.is_enabled(RdfsRule::Rdfs13) {
self.rule_engine.add_rule(Rule {
name: "rdfs13".to_string(),
body: vec![RuleAtom::Triple {
subject: Term::Variable("c".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(RDFS_DATATYPE.to_string()),
}],
head: vec![RuleAtom::Triple {
subject: Term::Variable("c".to_string()),
predicate: Term::Constant(RDFS_SUBCLASS_OF.to_string()),
object: Term::Constant(RDFS_LITERAL.to_string()),
}],
});
}
let enabled_count = self.config.enabled_rules.len();
let enabled_names: Vec<&str> = self.config.enabled_rules.iter().map(|r| r.name()).collect();
info!(
"Initialized {} RDFS entailment rules: {:?}",
enabled_count, enabled_names
);
}
pub fn process_triple(
&mut self,
subject: &str,
predicate: &str,
object: &str,
) -> Result<Vec<RuleAtom>> {
use vocabulary::*;
let mut new_facts = Vec::new();
match predicate {
RDFS_SUBCLASS_OF => {
debug!("Processing subClassOf: {} -> {}", subject, object);
self.context.add_subclass_relation(subject, object);
self.context.classes.insert(subject.to_string());
self.context.classes.insert(object.to_string());
}
RDFS_SUBPROPERTY_OF => {
debug!("Processing subPropertyOf: {} -> {}", subject, object);
self.context.add_subproperty_relation(subject, object);
self.context.properties.insert(subject.to_string());
self.context.properties.insert(object.to_string());
}
RDFS_DOMAIN => {
debug!("Processing domain: {} -> {}", subject, object);
self.context.add_property_domain(subject, object);
self.context.properties.insert(subject.to_string());
self.context.classes.insert(object.to_string());
}
RDFS_RANGE => {
debug!("Processing range: {} -> {}", subject, object);
self.context.add_property_range(subject, object);
self.context.properties.insert(subject.to_string());
self.context.classes.insert(object.to_string());
}
RDF_TYPE => {
debug!("Processing type: {} -> {}", subject, object);
if object == RDFS_CLASS {
self.context.classes.insert(subject.to_string());
} else if object == RDF_PROPERTY {
self.context.properties.insert(subject.to_string());
}
}
_ => {
trace!(
"Processing regular triple: {} {} {}",
subject,
predicate,
object
);
}
}
let input_fact = RuleAtom::Triple {
subject: Term::Constant(subject.to_string()),
predicate: Term::Constant(predicate.to_string()),
object: Term::Constant(object.to_string()),
};
new_facts.push(input_fact);
if self.config.is_enabled(RdfsRule::Rdfs1) {
new_facts.push(RuleAtom::Triple {
subject: Term::Constant(predicate.to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(RDF_PROPERTY.to_string()),
});
}
if self.config.is_enabled(RdfsRule::Rdfs4a) {
new_facts.push(RuleAtom::Triple {
subject: Term::Constant(subject.to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(RDFS_RESOURCE.to_string()),
});
}
if self.config.is_enabled(RdfsRule::Rdfs4b) {
new_facts.push(RuleAtom::Triple {
subject: Term::Constant(object.to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(RDFS_RESOURCE.to_string()),
});
}
if self.config.is_enabled(RdfsRule::Rdfs2) {
if let Some(domains) = self.context.property_domains.get(predicate) {
for domain in domains {
new_facts.push(RuleAtom::Triple {
subject: Term::Constant(subject.to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(domain.clone()),
});
}
}
}
if self.config.is_enabled(RdfsRule::Rdfs3) {
if let Some(ranges) = self.context.property_ranges.get(predicate) {
for range in ranges {
new_facts.push(RuleAtom::Triple {
subject: Term::Constant(object.to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(range.clone()),
});
}
}
}
if self.config.is_enabled(RdfsRule::Rdfs7) {
let superproperties = self.context.get_superproperties(predicate);
for superproperty in superproperties {
new_facts.push(RuleAtom::Triple {
subject: Term::Constant(subject.to_string()),
predicate: Term::Constant(superproperty),
object: Term::Constant(object.to_string()),
});
}
}
if self.config.is_enabled(RdfsRule::Rdfs9) && predicate == RDF_TYPE {
let superclasses = self.context.get_superclasses(object);
for superclass in superclasses {
new_facts.push(RuleAtom::Triple {
subject: Term::Constant(subject.to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(superclass),
});
}
}
if self.config.is_enabled(RdfsRule::Rdfs6)
&& predicate == RDF_TYPE
&& object == RDF_PROPERTY
{
new_facts.push(RuleAtom::Triple {
subject: Term::Constant(subject.to_string()),
predicate: Term::Constant(RDFS_SUBPROPERTY_OF.to_string()),
object: Term::Constant(subject.to_string()),
});
}
if self.config.is_enabled(RdfsRule::Rdfs8) && predicate == RDF_TYPE && object == RDFS_CLASS
{
new_facts.push(RuleAtom::Triple {
subject: Term::Constant(subject.to_string()),
predicate: Term::Constant(RDFS_SUBCLASS_OF.to_string()),
object: Term::Constant(RDFS_RESOURCE.to_string()),
});
}
if self.config.is_enabled(RdfsRule::Rdfs10) && predicate == RDF_TYPE && object == RDFS_CLASS
{
new_facts.push(RuleAtom::Triple {
subject: Term::Constant(subject.to_string()),
predicate: Term::Constant(RDFS_SUBCLASS_OF.to_string()),
object: Term::Constant(subject.to_string()),
});
}
if self.config.is_enabled(RdfsRule::Rdfs13)
&& predicate == RDF_TYPE
&& object == RDFS_DATATYPE
{
new_facts.push(RuleAtom::Triple {
subject: Term::Constant(subject.to_string()),
predicate: Term::Constant(RDFS_SUBCLASS_OF.to_string()),
object: Term::Constant(RDFS_LITERAL.to_string()),
});
}
Ok(new_facts)
}
pub fn infer(&mut self, facts: &[RuleAtom]) -> Result<Vec<RuleAtom>> {
let mut all_facts = facts.to_vec();
let mut new_facts_added = true;
let mut iteration = 0;
while new_facts_added {
new_facts_added = false;
iteration += 1;
debug!("RDFS inference iteration {}", iteration);
let current_facts = all_facts.clone();
for fact in ¤t_facts {
if let RuleAtom::Triple {
subject,
predicate,
object,
} = fact
{
if let (Term::Constant(s), Term::Constant(p), Term::Constant(o)) =
(subject, predicate, object)
{
let inferred = self.process_triple(s, p, o)?;
for new_fact in inferred {
if !all_facts.contains(&new_fact) {
all_facts.push(new_fact);
new_facts_added = true;
}
}
}
}
}
if iteration > 100 {
return Err(anyhow::anyhow!(
"RDFS inference did not converge after 100 iterations"
));
}
}
info!(
"RDFS inference completed after {} iterations, {} facts total",
iteration,
all_facts.len()
);
Ok(all_facts)
}
pub fn entails(&self, subject: &str, predicate: &str, object: &str) -> bool {
use vocabulary::*;
match predicate {
RDF_TYPE => {
if self.context.classes.contains(object) {
return self.context.is_subclass_of(object, object); }
false
}
RDFS_SUBCLASS_OF => self.context.is_subclass_of(subject, object),
RDFS_SUBPROPERTY_OF => self.context.is_subproperty_of(subject, object),
_ => {
let superproperties = self.context.get_superproperties(predicate);
superproperties.contains(predicate)
}
}
}
pub fn get_schema_info(&self) -> RdfsSchemaInfo {
RdfsSchemaInfo {
classes: self.context.classes.clone(),
properties: self.context.properties.clone(),
class_hierarchy: self.context.class_hierarchy.clone(),
property_hierarchy: self.context.property_hierarchy.clone(),
property_domains: self.context.property_domains.clone(),
property_ranges: self.context.property_ranges.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RdfsSchemaInfo {
pub classes: HashSet<String>,
pub properties: HashSet<String>,
pub class_hierarchy: HashMap<String, HashSet<String>>,
pub property_hierarchy: HashMap<String, HashSet<String>>,
pub property_domains: HashMap<String, HashSet<String>>,
pub property_ranges: HashMap<String, HashSet<String>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rdfs_context_initialization() {
let context = RdfsContext::default();
assert!(context
.classes
.contains("http://www.w3.org/2000/01/rdf-schema#Class"));
assert!(context
.properties
.contains("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"));
}
#[test]
fn test_subclass_inference() {
let mut context = RdfsContext::default();
context.add_subclass_relation("A", "B");
context.add_subclass_relation("B", "C");
assert!(context.is_subclass_of("A", "B"));
assert!(context.is_subclass_of("A", "C"));
assert!(context.is_subclass_of("B", "C"));
assert!(!context.is_subclass_of("C", "A"));
}
#[test]
fn test_rdfs_reasoner() -> Result<(), Box<dyn std::error::Error>> {
let mut reasoner = RdfsReasoner::new();
let facts = vec![
RuleAtom::Triple {
subject: Term::Constant("Person".to_string()),
predicate: Term::Constant(
"http://www.w3.org/2000/01/rdf-schema#subClassOf".to_string(),
),
object: Term::Constant("Agent".to_string()),
},
RuleAtom::Triple {
subject: Term::Constant("john".to_string()),
predicate: Term::Constant(
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
),
object: Term::Constant("Person".to_string()),
},
];
let inferred = reasoner.infer(&facts)?;
let expected = RuleAtom::Triple {
subject: Term::Constant("john".to_string()),
predicate: Term::Constant(
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
),
object: Term::Constant("Agent".to_string()),
};
assert!(inferred.contains(&expected));
Ok(())
}
#[test]
fn test_rdfs_rule_enum() {
assert_eq!(RdfsRule::all().len(), 13);
assert_eq!(RdfsRule::minimal().len(), 7);
assert!(RdfsRule::minimal().contains(&RdfsRule::Rdfs2));
assert!(RdfsRule::minimal().contains(&RdfsRule::Rdfs9));
assert!(!RdfsRule::minimal().contains(&RdfsRule::Rdfs1));
assert!(!RdfsRule::minimal().contains(&RdfsRule::Rdfs4a));
assert_eq!(RdfsRule::noisy().len(), 6);
assert!(RdfsRule::noisy().contains(&RdfsRule::Rdfs1));
assert!(RdfsRule::noisy().contains(&RdfsRule::Rdfs4a));
assert!(!RdfsRule::noisy().contains(&RdfsRule::Rdfs9));
assert_eq!(RdfsRule::Rdfs1.name(), "rdfs1");
assert_eq!(RdfsRule::Rdfs9.name(), "rdfs9");
assert_eq!(RdfsRule::Rdfs13.name(), "rdfs13");
}
#[test]
fn test_rdfs_profile_default() {
assert_eq!(RdfsProfile::default(), RdfsProfile::Minimal);
}
#[test]
fn test_rdfs_config_from_profile() {
let full_config = RdfsConfig::from_profile(RdfsProfile::Full);
assert_eq!(full_config.enabled_rules.len(), 13);
assert!(full_config.is_enabled(RdfsRule::Rdfs1));
assert!(full_config.is_enabled(RdfsRule::Rdfs9));
let minimal_config = RdfsConfig::from_profile(RdfsProfile::Minimal);
assert_eq!(minimal_config.enabled_rules.len(), 7);
assert!(!minimal_config.is_enabled(RdfsRule::Rdfs1));
assert!(minimal_config.is_enabled(RdfsRule::Rdfs9));
let none_config = RdfsConfig::from_profile(RdfsProfile::None);
assert!(none_config.enabled_rules.is_empty());
}
#[test]
fn test_rdfs_config_enable_disable() {
let mut config = RdfsConfig::none();
assert!(!config.is_enabled(RdfsRule::Rdfs9));
config.enable(RdfsRule::Rdfs9);
assert!(config.is_enabled(RdfsRule::Rdfs9));
config.disable(RdfsRule::Rdfs9);
assert!(!config.is_enabled(RdfsRule::Rdfs9));
}
#[test]
fn test_rdfs_reasoner_builder_with_profile() {
let full_reasoner = RdfsReasoner::builder()
.with_profile(RdfsProfile::Full)
.build();
assert_eq!(full_reasoner.config.enabled_rules.len(), 13);
let minimal_reasoner = RdfsReasoner::builder()
.with_profile(RdfsProfile::Minimal)
.build();
assert_eq!(minimal_reasoner.config.enabled_rules.len(), 7);
let none_reasoner = RdfsReasoner::builder()
.with_profile(RdfsProfile::None)
.build();
assert!(none_reasoner.config.enabled_rules.is_empty());
}
#[test]
fn test_rdfs_reasoner_builder_enable_disable() {
let reasoner = RdfsReasoner::builder()
.with_profile(RdfsProfile::Minimal)
.enable_rule(RdfsRule::Rdfs8)
.build();
assert!(reasoner.is_rule_enabled(RdfsRule::Rdfs8));
assert!(reasoner.is_rule_enabled(RdfsRule::Rdfs9));
assert_eq!(reasoner.config.enabled_rules.len(), 8);
let reasoner2 = RdfsReasoner::builder()
.with_profile(RdfsProfile::Full)
.disable_rules(RdfsRule::noisy())
.build();
assert!(!reasoner2.is_rule_enabled(RdfsRule::Rdfs1));
assert!(reasoner2.is_rule_enabled(RdfsRule::Rdfs9));
assert_eq!(reasoner2.config.enabled_rules.len(), 7);
let reasoner3 = RdfsReasoner::builder()
.with_profile(RdfsProfile::None)
.enable_rules(&[RdfsRule::Rdfs9, RdfsRule::Rdfs11])
.build();
assert!(reasoner3.is_rule_enabled(RdfsRule::Rdfs9));
assert!(reasoner3.is_rule_enabled(RdfsRule::Rdfs11));
assert!(!reasoner3.is_rule_enabled(RdfsRule::Rdfs2));
assert_eq!(reasoner3.config.enabled_rules.len(), 2);
}
#[test]
fn test_rdfs_reasoner_with_profile() {
let full_reasoner = RdfsReasoner::with_profile(RdfsProfile::Full);
assert_eq!(full_reasoner.config.enabled_rules.len(), 13);
let minimal_reasoner = RdfsReasoner::with_profile(RdfsProfile::Minimal);
assert_eq!(minimal_reasoner.config.enabled_rules.len(), 7);
}
#[test]
fn test_rdfs_reasoner_context_only() {
let reasoner = RdfsReasoner::context_only();
assert!(reasoner.config.enabled_rules.is_empty());
assert!(reasoner.context.classes.contains(vocabulary::RDFS_CLASS));
assert!(reasoner
.context
.is_subclass_of(vocabulary::RDFS_DATATYPE, vocabulary::RDFS_CLASS));
}
#[test]
fn test_rdfs_reasoner_default_is_minimal() {
let reasoner = RdfsReasoner::new();
assert_eq!(reasoner.config.enabled_rules.len(), 7);
assert!(!reasoner.is_rule_enabled(RdfsRule::Rdfs1));
assert!(reasoner.is_rule_enabled(RdfsRule::Rdfs9));
}
#[test]
fn test_disabled_rules_not_generating_facts() -> Result<(), Box<dyn std::error::Error>> {
use vocabulary::*;
let mut reasoner = RdfsReasoner::builder()
.with_profile(RdfsProfile::None)
.enable_rule(RdfsRule::Rdfs9)
.build();
let facts = vec![
RuleAtom::Triple {
subject: Term::Constant("Person".to_string()),
predicate: Term::Constant(RDFS_SUBCLASS_OF.to_string()),
object: Term::Constant("Agent".to_string()),
},
RuleAtom::Triple {
subject: Term::Constant("john".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant("Person".to_string()),
},
];
let inferred = reasoner.infer(&facts)?;
let expected_agent = RuleAtom::Triple {
subject: Term::Constant("john".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant("Agent".to_string()),
};
assert!(inferred.contains(&expected_agent));
let unexpected_resource = RuleAtom::Triple {
subject: Term::Constant("john".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(RDFS_RESOURCE.to_string()),
};
assert!(!inferred.contains(&unexpected_resource));
Ok(())
}
#[test]
fn test_full_profile_generates_noisy_facts() -> Result<(), Box<dyn std::error::Error>> {
use vocabulary::*;
let mut reasoner = RdfsReasoner::with_profile(RdfsProfile::Full);
let facts = vec![RuleAtom::Triple {
subject: Term::Constant("john".to_string()),
predicate: Term::Constant("http://example.org/knows".to_string()),
object: Term::Constant("mary".to_string()),
}];
let inferred = reasoner.infer(&facts)?;
let john_resource = RuleAtom::Triple {
subject: Term::Constant("john".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(RDFS_RESOURCE.to_string()),
};
let mary_resource = RuleAtom::Triple {
subject: Term::Constant("mary".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(RDFS_RESOURCE.to_string()),
};
assert!(inferred.contains(&john_resource));
assert!(inferred.contains(&mary_resource));
Ok(())
}
#[test]
fn test_minimal_profile_skips_noisy_facts() -> Result<(), Box<dyn std::error::Error>> {
use vocabulary::*;
let mut reasoner = RdfsReasoner::new();
let facts = vec![RuleAtom::Triple {
subject: Term::Constant("john".to_string()),
predicate: Term::Constant("http://example.org/knows".to_string()),
object: Term::Constant("mary".to_string()),
}];
let inferred = reasoner.infer(&facts)?;
let john_resource = RuleAtom::Triple {
subject: Term::Constant("john".to_string()),
predicate: Term::Constant(RDF_TYPE.to_string()),
object: Term::Constant(RDFS_RESOURCE.to_string()),
};
assert!(!inferred.contains(&john_resource));
Ok(())
}
#[test]
fn bench_profile_comparison() -> Result<(), Box<dyn std::error::Error>> {
let mut triples = Vec::new();
for i in 0..100 {
triples.push(RuleAtom::Triple {
subject: Term::Constant(format!("entity_{i}")),
predicate: Term::Constant("http://example.org/property".to_string()),
object: Term::Constant(format!("value_{i}")),
});
}
let mut minimal_reasoner = RdfsReasoner::with_profile(RdfsProfile::Minimal);
let minimal_start = std::time::Instant::now();
let minimal_result = minimal_reasoner.infer(&triples)?;
let minimal_duration = minimal_start.elapsed();
let mut full_reasoner = RdfsReasoner::with_profile(RdfsProfile::Full);
let full_start = std::time::Instant::now();
let full_result = full_reasoner.infer(&triples)?;
let full_duration = full_start.elapsed();
println!(
"Minimal profile: {} facts in {:?}",
minimal_result.len(),
minimal_duration
);
println!(
"Full profile: {} facts in {:?}",
full_result.len(),
full_duration
);
assert!(full_result.len() > minimal_result.len());
println!(
"Full profile generated {}x more facts",
full_result.len() as f64 / minimal_result.len() as f64
);
Ok(())
}
}