use std::collections::HashMap;
use tracing::{debug, span, Level};
use crate::model::{StarGraph, StarTerm, StarTriple};
use crate::{StarError, StarResult};
pub mod vocab {
pub const RDF_STATEMENT: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement";
pub const RDF_SUBJECT: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#subject";
pub const RDF_PREDICATE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#predicate";
pub const RDF_OBJECT: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#object";
pub const RDF_TYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
}
#[derive(Debug, Clone, PartialEq)]
pub enum ReificationStrategy {
StandardReification,
UniqueIris,
BlankNodes,
SingletonProperties,
}
#[derive(Debug)]
pub struct ReificationContext {
strategy: ReificationStrategy,
counter: usize,
base_iri: String,
triple_to_id: HashMap<String, String>,
id_to_triple: HashMap<String, StarTriple>,
}
impl ReificationContext {
pub fn new(strategy: ReificationStrategy, base_iri: Option<String>) -> Self {
Self {
strategy,
counter: 0,
base_iri: base_iri.unwrap_or_else(|| "http://example.org/statement/".to_string()),
triple_to_id: HashMap::new(),
id_to_triple: HashMap::new(),
}
}
fn generate_id(&mut self, triple: &StarTriple) -> String {
let triple_key = format!("{}|{}|{}", triple.subject, triple.predicate, triple.object);
if let Some(existing_id) = self.triple_to_id.get(&triple_key) {
return existing_id.clone();
}
let id = match self.strategy {
ReificationStrategy::StandardReification | ReificationStrategy::UniqueIris => {
self.counter += 1;
format!("{}{}", self.base_iri, self.counter)
}
ReificationStrategy::BlankNodes => {
self.counter += 1;
format!("_:stmt{}", self.counter)
}
ReificationStrategy::SingletonProperties => {
self.counter += 1;
format!("{}property/{}", self.base_iri, self.counter)
}
};
self.triple_to_id.insert(triple_key, id.clone());
self.id_to_triple.insert(id.clone(), triple.clone());
id
}
pub fn get_id(&self, triple: &StarTriple) -> Option<&String> {
let triple_key = format!("{}|{}|{}", triple.subject, triple.predicate, triple.object);
self.triple_to_id.get(&triple_key)
}
pub fn get_triple(&self, id: &str) -> Option<&StarTriple> {
self.id_to_triple.get(id)
}
}
pub struct Reificator {
context: ReificationContext,
}
impl Reificator {
pub fn new(strategy: ReificationStrategy, base_iri: Option<String>) -> Self {
Self {
context: ReificationContext::new(strategy, base_iri),
}
}
pub fn reify_graph(&mut self, star_graph: &StarGraph) -> StarResult<StarGraph> {
let span = span!(Level::INFO, "reify_graph");
let _enter = span.enter();
let mut reified_graph = StarGraph::new();
for triple in star_graph.triples() {
let reified_triples = self.reify_triple(triple)?;
for reified_triple in reified_triples {
reified_graph.insert(reified_triple)?;
}
}
debug!(
"Reified {} triples to {} standard RDF triples",
star_graph.len(),
reified_graph.len()
);
Ok(reified_graph)
}
pub fn reify_triple(&mut self, triple: &StarTriple) -> StarResult<Vec<StarTriple>> {
let mut result = Vec::new();
let subject = self.reify_term(&triple.subject, &mut result)?;
let predicate = self.reify_term(&triple.predicate, &mut result)?;
let object = self.reify_term(&triple.object, &mut result)?;
let main_triple = StarTriple::new(subject, predicate, object);
result.push(main_triple);
Ok(result)
}
fn reify_term(
&mut self,
term: &StarTerm,
additional_triples: &mut Vec<StarTriple>,
) -> StarResult<StarTerm> {
match term {
StarTerm::QuotedTriple(quoted_triple) => {
let stmt_id = self.context.generate_id(quoted_triple);
let reification_triples =
self.create_reification_triples(&stmt_id, quoted_triple)?;
additional_triples.extend(reification_triples);
match self.context.strategy {
ReificationStrategy::StandardReification | ReificationStrategy::UniqueIris => {
Ok(StarTerm::iri(&stmt_id)?)
}
ReificationStrategy::BlankNodes => {
let blank_id = &stmt_id[2..]; Ok(StarTerm::blank_node(blank_id)?)
}
ReificationStrategy::SingletonProperties => {
Ok(StarTerm::iri(&stmt_id)?)
}
}
}
_ => Ok(term.clone()),
}
}
fn create_reification_triples(
&mut self,
stmt_id: &str,
triple: &StarTriple,
) -> StarResult<Vec<StarTriple>> {
let mut triples = Vec::new();
if matches!(
self.context.strategy,
ReificationStrategy::SingletonProperties
) {
let property_term = StarTerm::iri(stmt_id)?;
let mut subject_additional = Vec::new();
let reified_subject = self.reify_term(&triple.subject, &mut subject_additional)?;
triples.extend(subject_additional);
let mut object_additional = Vec::new();
let reified_object = self.reify_term(&triple.object, &mut object_additional)?;
triples.extend(object_additional);
triples.push(StarTriple::new(
reified_subject,
property_term.clone(),
reified_object,
));
triples.push(StarTriple::new(
property_term,
StarTerm::iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#singletonPropertyOf")?,
triple.predicate.clone(),
));
return Ok(triples);
}
let stmt_term = match self.context.strategy {
ReificationStrategy::StandardReification | ReificationStrategy::UniqueIris => {
StarTerm::iri(stmt_id)?
}
ReificationStrategy::BlankNodes => {
let blank_id = &stmt_id[2..]; StarTerm::blank_node(blank_id)?
}
ReificationStrategy::SingletonProperties => {
unreachable!("Handled above")
}
};
if matches!(
self.context.strategy,
ReificationStrategy::StandardReification
) {
triples.push(StarTriple::new(
stmt_term.clone(),
StarTerm::iri(vocab::RDF_TYPE)?,
StarTerm::iri(vocab::RDF_STATEMENT)?,
));
}
let mut subject_additional = Vec::new();
let reified_subject = self.reify_term(&triple.subject, &mut subject_additional)?;
triples.extend(subject_additional);
let mut predicate_additional = Vec::new();
let reified_predicate = self.reify_term(&triple.predicate, &mut predicate_additional)?;
triples.extend(predicate_additional);
let mut object_additional = Vec::new();
let reified_object = self.reify_term(&triple.object, &mut object_additional)?;
triples.extend(object_additional);
triples.push(StarTriple::new(
stmt_term.clone(),
StarTerm::iri(vocab::RDF_SUBJECT)?,
reified_subject,
));
triples.push(StarTriple::new(
stmt_term.clone(),
StarTerm::iri(vocab::RDF_PREDICATE)?,
reified_predicate,
));
triples.push(StarTriple::new(
stmt_term,
StarTerm::iri(vocab::RDF_OBJECT)?,
reified_object,
));
Ok(triples)
}
pub fn dereify_graph(&mut self, reified_graph: &StarGraph) -> StarResult<StarGraph> {
let span = span!(Level::INFO, "dereify_graph");
let _enter = span.enter();
let mut star_graph = StarGraph::new();
let mut processed_statements = std::collections::HashSet::new();
let mut reconstructed_triples = std::collections::HashMap::new();
for triple in reified_graph.triples() {
if let (StarTerm::NamedNode(predicate), StarTerm::NamedNode(object)) =
(&triple.predicate, &triple.object)
{
if predicate.iri == vocab::RDF_TYPE && object.iri == vocab::RDF_STATEMENT {
if let StarTerm::NamedNode(stmt_node) = &triple.subject {
if !processed_statements.contains(&stmt_node.iri) {
if let Some(star_triple) =
self.reconstruct_quoted_triple(reified_graph, &stmt_node.iri)?
{
reconstructed_triples.insert(stmt_node.iri.clone(), star_triple);
processed_statements.insert(stmt_node.iri.clone());
}
}
}
}
}
}
for triple in reified_graph.triples() {
if self.is_reification_meta_triple(triple, &processed_statements) {
continue;
}
if let StarTerm::NamedNode(subject_node) = &triple.subject {
if let Some(quoted_triple) = reconstructed_triples.get(&subject_node.iri) {
let new_triple = StarTriple::new(
StarTerm::quoted_triple(quoted_triple.clone()),
triple.predicate.clone(),
triple.object.clone(),
);
star_graph.insert(new_triple)?;
} else {
star_graph.insert(triple.clone())?;
}
} else {
star_graph.insert(triple.clone())?;
}
}
debug!(
"Dereified {} reified triples back to {} RDF-star triples",
reified_graph.len(),
star_graph.len()
);
Ok(star_graph)
}
fn reconstruct_quoted_triple(
&self,
graph: &StarGraph,
stmt_iri: &str,
) -> StarResult<Option<StarTriple>> {
let mut subject = None;
let mut predicate = None;
let mut object = None;
let stmt_term = StarTerm::iri(stmt_iri)?;
for triple in graph.triples() {
if triple.subject == stmt_term {
if let StarTerm::NamedNode(pred_node) = &triple.predicate {
match pred_node.iri.as_str() {
vocab::RDF_SUBJECT => subject = Some(triple.object.clone()),
vocab::RDF_PREDICATE => predicate = Some(triple.object.clone()),
vocab::RDF_OBJECT => object = Some(triple.object.clone()),
_ => {}
}
}
}
}
if let (Some(s), Some(p), Some(o)) = (subject, predicate, object) {
Ok(Some(StarTriple::new(s, p, o)))
} else {
Ok(None)
}
}
fn is_reification_meta_triple(
&self,
triple: &StarTriple,
processed_statements: &std::collections::HashSet<String>,
) -> bool {
if let StarTerm::NamedNode(subj_node) = &triple.subject {
if processed_statements.contains(&subj_node.iri) {
if let StarTerm::NamedNode(pred_node) = &triple.predicate {
match pred_node.iri.as_str() {
vocab::RDF_TYPE
| vocab::RDF_SUBJECT
| vocab::RDF_PREDICATE
| vocab::RDF_OBJECT => {
return true;
}
_ => {}
}
}
}
}
false
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AdvancedReificationStrategy {
Standard(ReificationStrategy),
Hybrid {
simple_strategy: ReificationStrategy,
nested_strategy: ReificationStrategy,
predicate_strategies: HashMap<String, ReificationStrategy>,
},
Conditional {
default_strategy: ReificationStrategy,
rules: Vec<ReificationRule>,
},
Optimized {
base_strategy: ReificationStrategy,
aggressive_caching: bool,
max_cache_size: usize,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct ReificationRule {
pub condition: ReificationCondition,
pub strategy: ReificationStrategy,
pub priority: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ReificationCondition {
PredicateIri(String),
SubjectType(TermType),
ObjectType(TermType),
NestingDepth(usize),
GraphSize(usize),
Custom(String),
}
#[derive(Debug, Clone, PartialEq)]
pub enum TermType {
NamedNode,
BlankNode,
Literal,
QuotedTriple,
Variable,
}
pub struct AdvancedReificator {
strategy: AdvancedReificationStrategy,
contexts: HashMap<String, ReificationContext>,
#[allow(dead_code)]
cache: lru::LruCache<String, Vec<StarTriple>>,
statistics: ReificationStatistics,
}
#[derive(Debug, Clone, Default)]
pub struct ReificationStatistics {
pub total_triples: usize,
pub quoted_triples: usize,
pub reification_triples: usize,
pub cache_hit_rate: f64,
pub avg_processing_time: f64,
pub strategy_usage: HashMap<String, usize>,
}
impl AdvancedReificator {
pub fn new(strategy: AdvancedReificationStrategy) -> Self {
Self {
strategy,
contexts: HashMap::new(),
cache: lru::LruCache::new(std::num::NonZeroUsize::new(1000).expect("1000 is non-zero")),
statistics: ReificationStatistics::default(),
}
}
pub fn reify_graph_advanced(&mut self, star_graph: &StarGraph) -> StarResult<StarGraph> {
let span = span!(Level::INFO, "reify_graph_advanced");
let _enter = span.enter();
let start_time = std::time::Instant::now();
let mut reified_graph = StarGraph::new();
for triple in star_graph.triples() {
self.statistics.total_triples += 1;
let strategy = self.select_strategy_for_triple(triple)?;
let strategy_name = format!("{strategy:?}");
*self
.statistics
.strategy_usage
.entry(strategy_name)
.or_insert(0) += 1;
let reified_triples = self.reify_triple_with_strategy(triple, &strategy)?;
for reified_triple in reified_triples {
reified_graph.insert(reified_triple)?;
self.statistics.reification_triples += 1;
}
}
let processing_time = start_time.elapsed();
self.statistics.avg_processing_time =
processing_time.as_micros() as f64 / self.statistics.total_triples as f64;
debug!(
"Advanced reification completed: {} triples -> {} triples in {:?}",
star_graph.len(),
reified_graph.len(),
processing_time
);
Ok(reified_graph)
}
fn select_strategy_for_triple(&self, triple: &StarTriple) -> StarResult<ReificationStrategy> {
match &self.strategy {
AdvancedReificationStrategy::Standard(strategy) => Ok(strategy.clone()),
AdvancedReificationStrategy::Hybrid {
simple_strategy,
nested_strategy,
predicate_strategies,
} => {
if let StarTerm::NamedNode(pred_node) = &triple.predicate {
if let Some(strategy) = predicate_strategies.get(&pred_node.iri) {
return Ok(strategy.clone());
}
}
if self.has_nested_quoted_triples(triple) {
Ok(nested_strategy.clone())
} else {
Ok(simple_strategy.clone())
}
}
AdvancedReificationStrategy::Conditional {
default_strategy,
rules,
} => {
let mut applicable_rules: Vec<_> = rules
.iter()
.filter(|rule| self.evaluate_condition(&rule.condition, triple))
.collect();
applicable_rules.sort_by_key(|rule| std::cmp::Reverse(rule.priority));
if let Some(rule) = applicable_rules.first() {
Ok(rule.strategy.clone())
} else {
Ok(default_strategy.clone())
}
}
AdvancedReificationStrategy::Optimized { base_strategy, .. } => {
Ok(base_strategy.clone())
}
}
}
fn has_nested_quoted_triples(&self, triple: &StarTriple) -> bool {
self.term_has_quoted_triples(&triple.subject)
|| self.term_has_quoted_triples(&triple.predicate)
|| self.term_has_quoted_triples(&triple.object)
}
fn term_has_quoted_triples(&self, term: &StarTerm) -> bool {
match term {
StarTerm::QuotedTriple(_inner_triple) => {
true
}
_ => false,
}
}
fn evaluate_condition(&self, condition: &ReificationCondition, triple: &StarTriple) -> bool {
match condition {
ReificationCondition::PredicateIri(iri) => {
if let StarTerm::NamedNode(pred_node) = &triple.predicate {
pred_node.iri == *iri
} else {
false
}
}
ReificationCondition::SubjectType(term_type) => {
self.matches_term_type(&triple.subject, term_type)
}
ReificationCondition::ObjectType(term_type) => {
self.matches_term_type(&triple.object, term_type)
}
ReificationCondition::NestingDepth(max_depth) => {
self.calculate_nesting_depth(triple) <= *max_depth
}
ReificationCondition::GraphSize(_) => {
true
}
ReificationCondition::Custom(_) => {
false
}
}
}
fn matches_term_type(&self, term: &StarTerm, term_type: &TermType) -> bool {
matches!(
(term, term_type),
(StarTerm::NamedNode(_), TermType::NamedNode)
| (StarTerm::BlankNode(_), TermType::BlankNode)
| (StarTerm::Literal(_), TermType::Literal)
| (StarTerm::QuotedTriple(_), TermType::QuotedTriple)
| (StarTerm::Variable(_), TermType::Variable)
)
}
fn calculate_nesting_depth(&self, triple: &StarTriple) -> usize {
let subject_depth = self.term_nesting_depth(&triple.subject);
let predicate_depth = self.term_nesting_depth(&triple.predicate);
let object_depth = self.term_nesting_depth(&triple.object);
subject_depth.max(predicate_depth).max(object_depth)
}
fn term_nesting_depth(&self, term: &StarTerm) -> usize {
match term {
StarTerm::QuotedTriple(inner_triple) => 1 + self.calculate_nesting_depth(inner_triple),
_ => 0,
}
}
fn reify_triple_with_strategy(
&mut self,
triple: &StarTriple,
strategy: &ReificationStrategy,
) -> StarResult<Vec<StarTriple>> {
let context_key = format!("{strategy:?}");
if !self.contexts.contains_key(&context_key) {
self.contexts.insert(
context_key.clone(),
ReificationContext::new(strategy.clone(), None),
);
}
let context = self
.contexts
.get_mut(&context_key)
.expect("context should exist after insertion");
let mut temp_reificator = Reificator {
context: ReificationContext::new(strategy.clone(), None),
};
temp_reificator.context.counter = context.counter;
temp_reificator.context.triple_to_id = context.triple_to_id.clone();
temp_reificator.context.id_to_triple = context.id_to_triple.clone();
let result = temp_reificator.reify_triple(triple);
context.counter = temp_reificator.context.counter;
context.triple_to_id = temp_reificator.context.triple_to_id;
context.id_to_triple = temp_reificator.context.id_to_triple;
result
}
pub fn get_statistics(&self) -> &ReificationStatistics {
&self.statistics
}
pub fn reset_statistics(&mut self) {
self.statistics = ReificationStatistics::default();
}
pub fn export_mappings(&self) -> HashMap<String, HashMap<String, String>> {
let mut mappings = HashMap::new();
for (strategy_key, context) in &self.contexts {
mappings.insert(strategy_key.clone(), context.triple_to_id.clone());
}
mappings
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_reification() {
let mut reificator = Reificator::new(ReificationStrategy::StandardReification, None);
let quoted_triple = StarTriple::new(
StarTerm::iri("http://example.org/subject").unwrap(),
StarTerm::iri("http://example.org/predicate").unwrap(),
StarTerm::iri("http://example.org/object").unwrap(),
);
let triple_with_quoted = StarTriple::new(
StarTerm::QuotedTriple(Box::new(quoted_triple)),
StarTerm::iri("http://example.org/hasMetadata").unwrap(),
StarTerm::literal("metadata").unwrap(),
);
let reified_triples = reificator.reify_triple(&triple_with_quoted).unwrap();
assert!(reified_triples.len() > 1);
}
#[test]
fn test_dereification() {
let mut reificator = Reificator::new(ReificationStrategy::StandardReification, None);
let mut star_graph = StarGraph::new();
let quoted_triple = StarTriple::new(
StarTerm::iri("http://example.org/subject").unwrap(),
StarTerm::iri("http://example.org/predicate").unwrap(),
StarTerm::iri("http://example.org/object").unwrap(),
);
let triple_with_quoted = StarTriple::new(
StarTerm::QuotedTriple(Box::new(quoted_triple)),
StarTerm::iri("http://example.org/hasMetadata").unwrap(),
StarTerm::literal("metadata").unwrap(),
);
star_graph.insert(triple_with_quoted).unwrap();
let reified_graph = reificator.reify_graph(&star_graph).unwrap();
let dereified_graph = reificator.dereify_graph(&reified_graph).unwrap();
assert_eq!(star_graph.len(), dereified_graph.len());
}
#[test]
fn test_advanced_reification_strategies() {
let hybrid_strategy = AdvancedReificationStrategy::Hybrid {
simple_strategy: ReificationStrategy::StandardReification,
nested_strategy: ReificationStrategy::BlankNodes,
predicate_strategies: HashMap::new(),
};
let mut advanced_reificator = AdvancedReificator::new(hybrid_strategy);
let mut star_graph = StarGraph::new();
let quoted_triple = StarTriple::new(
StarTerm::iri("http://example.org/subject").unwrap(),
StarTerm::iri("http://example.org/predicate").unwrap(),
StarTerm::iri("http://example.org/object").unwrap(),
);
let triple_with_quoted = StarTriple::new(
StarTerm::QuotedTriple(Box::new(quoted_triple)),
StarTerm::iri("http://example.org/hasMetadata").unwrap(),
StarTerm::literal("metadata").unwrap(),
);
star_graph.insert(triple_with_quoted).unwrap();
let reified_graph = advanced_reificator
.reify_graph_advanced(&star_graph)
.unwrap();
assert!(!reified_graph.is_empty());
let stats = advanced_reificator.get_statistics();
assert!(stats.total_triples > 0);
}
#[test]
fn test_conditional_reification() {
let rules = vec![ReificationRule {
condition: ReificationCondition::PredicateIri("http://example.org/special".to_string()),
strategy: ReificationStrategy::BlankNodes,
priority: 10,
}];
let conditional_strategy = AdvancedReificationStrategy::Conditional {
default_strategy: ReificationStrategy::StandardReification,
rules,
};
let mut advanced_reificator = AdvancedReificator::new(conditional_strategy);
let mut star_graph = StarGraph::new();
let quoted_triple = StarTriple::new(
StarTerm::iri("http://example.org/subject").unwrap(),
StarTerm::iri("http://example.org/special").unwrap(), StarTerm::iri("http://example.org/object").unwrap(),
);
let triple_with_quoted = StarTriple::new(
StarTerm::QuotedTriple(Box::new(quoted_triple)),
StarTerm::iri("http://example.org/hasMetadata").unwrap(),
StarTerm::literal("metadata").unwrap(),
);
star_graph.insert(triple_with_quoted).unwrap();
let reified_graph = advanced_reificator
.reify_graph_advanced(&star_graph)
.unwrap();
assert!(!reified_graph.is_empty());
let stats = advanced_reificator.get_statistics();
assert!(!stats.strategy_usage.is_empty());
}
}
pub mod utils {
use super::*;
pub fn has_reifications(graph: &StarGraph) -> bool {
for triple in graph.triples() {
if let StarTerm::NamedNode(node) = &triple.predicate {
if matches!(
node.iri.as_str(),
vocab::RDF_SUBJECT | vocab::RDF_PREDICATE | vocab::RDF_OBJECT
) {
return true;
}
}
}
false
}
pub fn count_reifications(graph: &StarGraph) -> usize {
let mut statements = std::collections::HashSet::new();
for triple in graph.triples() {
if let StarTerm::NamedNode(pred_node) = &triple.predicate {
if matches!(
pred_node.iri.as_str(),
vocab::RDF_SUBJECT | vocab::RDF_PREDICATE | vocab::RDF_OBJECT
) {
if let StarTerm::NamedNode(subj_node) = &triple.subject {
statements.insert(&subj_node.iri);
} else if let StarTerm::BlankNode(subj_node) = &triple.subject {
statements.insert(&subj_node.id);
}
}
}
}
statements.len()
}
pub fn validate_reifications(graph: &StarGraph) -> StarResult<()> {
let mut statements = HashMap::new();
for triple in graph.triples() {
if let StarTerm::NamedNode(pred_node) = &triple.predicate {
match pred_node.iri.as_str() {
vocab::RDF_SUBJECT => {
if let Some(stmt_id) = extract_statement_id(&triple.subject) {
statements.entry(stmt_id).or_insert([false, false, false])[0] = true;
}
}
vocab::RDF_PREDICATE => {
if let Some(stmt_id) = extract_statement_id(&triple.subject) {
statements.entry(stmt_id).or_insert([false, false, false])[1] = true;
}
}
vocab::RDF_OBJECT => {
if let Some(stmt_id) = extract_statement_id(&triple.subject) {
statements.entry(stmt_id).or_insert([false, false, false])[2] = true;
}
}
_ => {}
}
}
}
for (stmt_id, completeness) in statements {
if !completeness.iter().all(|&x| x) {
return Err(StarError::reification_error(format!(
"Incomplete reification for statement {stmt_id}"
)));
}
}
Ok(())
}
fn extract_statement_id(term: &StarTerm) -> Option<String> {
match term {
StarTerm::NamedNode(node) => Some(node.iri.clone()),
StarTerm::BlankNode(node) => Some(format!("_:{}", node.id)),
_ => None,
}
}
}
#[cfg(test)]
mod additional_tests {
use super::*;
#[test]
fn test_basic_reification() {
let mut reificator = Reificator::new(
ReificationStrategy::StandardReification,
Some("http://example.org/stmt/".to_string()),
);
let inner = StarTriple::new(
StarTerm::iri("http://example.org/alice").unwrap(),
StarTerm::iri("http://example.org/age").unwrap(),
StarTerm::literal("25").unwrap(),
);
let outer = StarTriple::new(
StarTerm::quoted_triple(inner.clone()),
StarTerm::iri("http://example.org/certainty").unwrap(),
StarTerm::literal("0.9").unwrap(),
);
let mut star_graph = StarGraph::new();
star_graph.insert(outer).unwrap();
let reified = reificator.reify_graph(&star_graph).unwrap();
assert!(reified.len() > 1);
let has_type_triple = reified.triples().iter().any(|t| {
if let (StarTerm::NamedNode(p), StarTerm::NamedNode(o)) = (&t.predicate, &t.object) {
p.iri == vocab::RDF_TYPE && o.iri == vocab::RDF_STATEMENT
} else {
false
}
});
assert!(has_type_triple);
}
#[test]
fn test_dereification() {
let mut reified_graph = StarGraph::new();
let stmt_iri = "http://example.org/stmt/1";
reified_graph
.insert(StarTriple::new(
StarTerm::iri(stmt_iri).unwrap(),
StarTerm::iri(vocab::RDF_TYPE).unwrap(),
StarTerm::iri(vocab::RDF_STATEMENT).unwrap(),
))
.unwrap();
reified_graph
.insert(StarTriple::new(
StarTerm::iri(stmt_iri).unwrap(),
StarTerm::iri(vocab::RDF_SUBJECT).unwrap(),
StarTerm::iri("http://example.org/alice").unwrap(),
))
.unwrap();
reified_graph
.insert(StarTriple::new(
StarTerm::iri(stmt_iri).unwrap(),
StarTerm::iri(vocab::RDF_PREDICATE).unwrap(),
StarTerm::iri("http://example.org/age").unwrap(),
))
.unwrap();
reified_graph
.insert(StarTriple::new(
StarTerm::iri(stmt_iri).unwrap(),
StarTerm::iri(vocab::RDF_OBJECT).unwrap(),
StarTerm::literal("25").unwrap(),
))
.unwrap();
reified_graph
.insert(StarTriple::new(
StarTerm::iri(stmt_iri).unwrap(),
StarTerm::iri("http://example.org/certainty").unwrap(),
StarTerm::literal("0.9").unwrap(),
))
.unwrap();
let mut dereificator = Reificator::new(
ReificationStrategy::StandardReification,
Some("http://example.org/stmt/".to_string()),
);
let star_graph = dereificator.dereify_graph(&reified_graph).unwrap();
assert_eq!(star_graph.len(), 1);
let triple = &star_graph.triples()[0];
assert!(triple.subject.is_quoted_triple());
}
#[test]
fn test_reification_roundtrip() {
let inner = StarTriple::new(
StarTerm::iri("http://example.org/alice").unwrap(),
StarTerm::iri("http://example.org/age").unwrap(),
StarTerm::literal("25").unwrap(),
);
let outer = StarTriple::new(
StarTerm::quoted_triple(inner),
StarTerm::iri("http://example.org/certainty").unwrap(),
StarTerm::literal("0.9").unwrap(),
);
let mut original_graph = StarGraph::new();
original_graph.insert(outer).unwrap();
let mut reificator = Reificator::new(
ReificationStrategy::StandardReification,
Some("http://example.org/stmt/".to_string()),
);
let reified_graph = reificator.reify_graph(&original_graph).unwrap();
let mut dereificator = Reificator::new(
ReificationStrategy::StandardReification,
Some("http://example.org/stmt/".to_string()),
);
let recovered_graph = dereificator.dereify_graph(&reified_graph).unwrap();
assert_eq!(recovered_graph.len(), original_graph.len());
let recovered_triple = &recovered_graph.triples()[0];
assert!(recovered_triple.subject.is_quoted_triple());
}
#[test]
fn test_utils() {
let mut graph = StarGraph::new();
graph
.insert(StarTriple::new(
StarTerm::iri("http://example.org/stmt1").unwrap(),
StarTerm::iri(vocab::RDF_SUBJECT).unwrap(),
StarTerm::iri("http://example.org/alice").unwrap(),
))
.unwrap();
assert!(utils::has_reifications(&graph));
assert_eq!(utils::count_reifications(&graph), 1);
assert!(utils::validate_reifications(&graph).is_err());
}
}
pub type EmbeddedTriple = StarTriple;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum AnnotationStyle {
#[default]
Reification,
Singleton,
NaryRelation,
}
pub struct ReificationBridge {
style: AnnotationStyle,
base_iri: String,
counter: std::sync::atomic::AtomicUsize,
}
impl ReificationBridge {
pub fn new(style: AnnotationStyle) -> Self {
Self {
style,
base_iri: "http://reification.example/stmt/".to_string(),
counter: std::sync::atomic::AtomicUsize::new(1),
}
}
pub fn with_base_iri(style: AnnotationStyle, base_iri: impl Into<String>) -> Self {
Self {
style,
base_iri: base_iri.into(),
counter: std::sync::atomic::AtomicUsize::new(1),
}
}
pub fn style(&self) -> &AnnotationStyle {
&self.style
}
pub fn star_to_reification(&self, triple: &EmbeddedTriple) -> Vec<StarTriple> {
match self.style {
AnnotationStyle::Reification => self.to_standard_reification(triple),
AnnotationStyle::Singleton => self.to_singleton(triple),
AnnotationStyle::NaryRelation => self.to_nary_relation(triple),
}
}
fn next_id(&self) -> String {
let n = self
.counter
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!("{}{}", self.base_iri, n)
}
fn to_standard_reification(&self, triple: &EmbeddedTriple) -> Vec<StarTriple> {
let stmt_iri = self.next_id();
let mut triples = Vec::with_capacity(4);
let stmt_term = match StarTerm::iri(&stmt_iri) {
Ok(t) => t,
Err(_) => return triples,
};
let rdf_type = match StarTerm::iri(vocab::RDF_TYPE) {
Ok(t) => t,
Err(_) => return triples,
};
let rdf_statement = match StarTerm::iri(vocab::RDF_STATEMENT) {
Ok(t) => t,
Err(_) => return triples,
};
let rdf_subject = match StarTerm::iri(vocab::RDF_SUBJECT) {
Ok(t) => t,
Err(_) => return triples,
};
let rdf_predicate = match StarTerm::iri(vocab::RDF_PREDICATE) {
Ok(t) => t,
Err(_) => return triples,
};
let rdf_object = match StarTerm::iri(vocab::RDF_OBJECT) {
Ok(t) => t,
Err(_) => return triples,
};
triples.push(StarTriple::new(stmt_term.clone(), rdf_type, rdf_statement));
triples.push(StarTriple::new(
stmt_term.clone(),
rdf_subject,
triple.subject.clone(),
));
triples.push(StarTriple::new(
stmt_term.clone(),
rdf_predicate,
triple.predicate.clone(),
));
triples.push(StarTriple::new(
stmt_term,
rdf_object,
triple.object.clone(),
));
triples
}
fn to_singleton(&self, triple: &EmbeddedTriple) -> Vec<StarTriple> {
let prop_iri = self.next_id();
let mut triples = Vec::with_capacity(2);
let prop_term = match StarTerm::iri(&prop_iri) {
Ok(t) => t,
Err(_) => return triples,
};
let singleton_of =
match StarTerm::iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#singletonPropertyOf") {
Ok(t) => t,
Err(_) => return triples,
};
triples.push(StarTriple::new(
triple.subject.clone(),
prop_term.clone(),
triple.object.clone(),
));
triples.push(StarTriple::new(
prop_term,
singleton_of,
triple.predicate.clone(),
));
triples
}
fn to_nary_relation(&self, triple: &EmbeddedTriple) -> Vec<StarTriple> {
let node_iri = self.next_id();
let mut triples = Vec::with_capacity(3);
let node_term = match StarTerm::iri(&node_iri) {
Ok(t) => t,
Err(_) => return triples,
};
let nary_subject = match StarTerm::iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#subject")
{
Ok(t) => t,
Err(_) => return triples,
};
let nary_predicate =
match StarTerm::iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#predicate") {
Ok(t) => t,
Err(_) => return triples,
};
let nary_object = match StarTerm::iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#object") {
Ok(t) => t,
Err(_) => return triples,
};
triples.push(StarTriple::new(
node_term.clone(),
nary_subject,
triple.subject.clone(),
));
triples.push(StarTriple::new(
node_term.clone(),
nary_predicate,
triple.predicate.clone(),
));
triples.push(StarTriple::new(
node_term,
nary_object,
triple.object.clone(),
));
triples
}
pub fn reification_to_star(&self, stmts: &[StarTriple]) -> Option<EmbeddedTriple> {
let mut subject = None;
let mut predicate = None;
let mut object = None;
for triple in stmts {
if let StarTerm::NamedNode(pred_node) = &triple.predicate {
match pred_node.iri.as_str() {
s if s == vocab::RDF_SUBJECT => subject = Some(triple.object.clone()),
s if s == vocab::RDF_PREDICATE => predicate = Some(triple.object.clone()),
s if s == vocab::RDF_OBJECT => object = Some(triple.object.clone()),
_ => {}
}
}
}
if let (Some(s), Some(p), Some(o)) = (subject, predicate, object) {
Some(StarTriple::new(s, p, o))
} else {
None
}
}
pub fn convert_graph_to_reification(&self, graph: &[StarTriple]) -> Vec<StarTriple> {
let mut result = Vec::new();
for triple in graph {
let has_quoted = matches!(&triple.subject, StarTerm::QuotedTriple(_))
|| matches!(&triple.object, StarTerm::QuotedTriple(_));
if has_quoted {
if let StarTerm::QuotedTriple(inner) = &triple.subject {
result.extend(self.star_to_reification(inner));
}
if let StarTerm::QuotedTriple(inner) = &triple.object {
result.extend(self.star_to_reification(inner));
}
} else {
result.push(triple.clone());
}
}
result
}
pub fn convert_reification_to_star(&self, graph: &[StarTriple]) -> Vec<EmbeddedTriple> {
use std::collections::{HashMap, HashSet};
let mut stmt_nodes: HashSet<String> = HashSet::new();
for triple in graph {
if let (StarTerm::NamedNode(pred), StarTerm::NamedNode(obj)) =
(&triple.predicate, &triple.object)
{
if pred.iri == vocab::RDF_TYPE && obj.iri == vocab::RDF_STATEMENT {
if let StarTerm::NamedNode(subj) = &triple.subject {
stmt_nodes.insert(subj.iri.clone());
}
}
}
}
let mut clusters: HashMap<String, Vec<StarTriple>> = HashMap::new();
for triple in graph {
if let StarTerm::NamedNode(subj) = &triple.subject {
if stmt_nodes.contains(&subj.iri) {
clusters
.entry(subj.iri.clone())
.or_default()
.push(triple.clone());
}
}
}
let mut embedded_triples = Vec::new();
for cluster_triples in clusters.values() {
if let Some(et) = self.reification_to_star(cluster_triples) {
embedded_triples.push(et);
}
}
embedded_triples
}
}
#[cfg(test)]
mod bridge_tests {
use super::*;
fn iri(s: &str) -> StarTerm {
StarTerm::iri(s).expect("iri")
}
fn lit(s: &str) -> StarTerm {
StarTerm::literal(s).expect("lit")
}
fn sample_triple() -> StarTriple {
StarTriple::new(
iri("http://example.org/alice"),
iri("http://example.org/age"),
lit("30"),
)
}
#[test]
fn test_bridge_reification_style_generates_four_triples() {
let bridge = ReificationBridge::new(AnnotationStyle::Reification);
let triples = bridge.star_to_reification(&sample_triple());
assert_eq!(
triples.len(),
4,
"rdf:Statement reification needs 4 triples"
);
}
#[test]
fn test_bridge_reification_contains_rdf_type() {
let bridge = ReificationBridge::new(AnnotationStyle::Reification);
let triples = bridge.star_to_reification(&sample_triple());
let has_type = triples.iter().any(|t| {
if let (StarTerm::NamedNode(p), StarTerm::NamedNode(o)) = (&t.predicate, &t.object) {
p.iri == vocab::RDF_TYPE && o.iri == vocab::RDF_STATEMENT
} else {
false
}
});
assert!(has_type, "must include rdf:type rdf:Statement triple");
}
#[test]
fn test_bridge_reification_contains_rdf_subject() {
let bridge = ReificationBridge::new(AnnotationStyle::Reification);
let triples = bridge.star_to_reification(&sample_triple());
let has_subject = triples.iter().any(|t| {
if let StarTerm::NamedNode(p) = &t.predicate {
p.iri == vocab::RDF_SUBJECT
} else {
false
}
});
assert!(has_subject);
}
#[test]
fn test_bridge_reification_contains_rdf_predicate() {
let bridge = ReificationBridge::new(AnnotationStyle::Reification);
let triples = bridge.star_to_reification(&sample_triple());
let has_predicate = triples.iter().any(|t| {
if let StarTerm::NamedNode(p) = &t.predicate {
p.iri == vocab::RDF_PREDICATE
} else {
false
}
});
assert!(has_predicate);
}
#[test]
fn test_bridge_reification_contains_rdf_object() {
let bridge = ReificationBridge::new(AnnotationStyle::Reification);
let triples = bridge.star_to_reification(&sample_triple());
let has_object = triples.iter().any(|t| {
if let StarTerm::NamedNode(p) = &t.predicate {
p.iri == vocab::RDF_OBJECT
} else {
false
}
});
assert!(has_object);
}
#[test]
fn test_bridge_singleton_style_generates_two_triples() {
let bridge = ReificationBridge::new(AnnotationStyle::Singleton);
let triples = bridge.star_to_reification(&sample_triple());
assert_eq!(triples.len(), 2, "singleton style needs 2 triples");
}
#[test]
fn test_bridge_nary_relation_style_generates_three_triples() {
let bridge = ReificationBridge::new(AnnotationStyle::NaryRelation);
let triples = bridge.star_to_reification(&sample_triple());
assert_eq!(triples.len(), 3, "nary relation style needs 3 triples");
}
#[test]
fn test_bridge_reification_roundtrip() {
let bridge = ReificationBridge::new(AnnotationStyle::Reification);
let original = sample_triple();
let reified = bridge.star_to_reification(&original);
let recovered = bridge.reification_to_star(&reified);
assert!(recovered.is_some(), "should recover the embedded triple");
let recovered = recovered.unwrap();
assert_eq!(recovered.subject, original.subject);
assert_eq!(recovered.predicate, original.predicate);
assert_eq!(recovered.object, original.object);
}
#[test]
fn test_bridge_reification_to_star_incomplete_returns_none() {
let bridge = ReificationBridge::new(AnnotationStyle::Reification);
let partial = vec![StarTriple::new(
iri("http://example.org/stmt1"),
iri(vocab::RDF_SUBJECT),
iri("http://example.org/alice"),
)];
let recovered = bridge.reification_to_star(&partial);
assert!(
recovered.is_none(),
"incomplete reification should return None"
);
}
#[test]
fn test_bridge_reification_to_star_empty_returns_none() {
let bridge = ReificationBridge::new(AnnotationStyle::Reification);
let recovered = bridge.reification_to_star(&[]);
assert!(recovered.is_none());
}
#[test]
fn test_convert_graph_to_reification_expands_quoted_subjects() {
let bridge = ReificationBridge::new(AnnotationStyle::Reification);
let inner = sample_triple();
let outer = StarTriple::new(
StarTerm::quoted_triple(inner),
iri("http://example.org/certainty"),
lit("high"),
);
let expanded = bridge.convert_graph_to_reification(&[outer]);
assert!(expanded.len() >= 4);
}
#[test]
fn test_convert_graph_plain_triples_pass_through() {
let bridge = ReificationBridge::new(AnnotationStyle::Reification);
let plain = sample_triple();
let expanded = bridge.convert_graph_to_reification(std::slice::from_ref(&plain));
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0], plain);
}
#[test]
fn test_convert_reification_to_star_roundtrip() {
let bridge = ReificationBridge::new(AnnotationStyle::Reification);
let inner = sample_triple();
let reified = bridge.star_to_reification(&inner);
let recovered = bridge.convert_reification_to_star(&reified);
assert_eq!(recovered.len(), 1);
assert_eq!(recovered[0].subject, inner.subject);
assert_eq!(recovered[0].predicate, inner.predicate);
assert_eq!(recovered[0].object, inner.object);
}
#[test]
fn test_convert_reification_to_star_multiple_clusters() {
let bridge = ReificationBridge::new(AnnotationStyle::Reification);
let t1 = sample_triple();
let t2 = StarTriple::new(
iri("http://example.org/bob"),
iri("http://example.org/age"),
lit("25"),
);
let mut all_reified = bridge.star_to_reification(&t1);
all_reified.extend(bridge.star_to_reification(&t2));
let recovered = bridge.convert_reification_to_star(&all_reified);
assert_eq!(recovered.len(), 2, "should recover both embedded triples");
}
#[test]
fn test_annotation_style_default_is_reification() {
assert_eq!(AnnotationStyle::default(), AnnotationStyle::Reification);
}
#[test]
fn test_bridge_with_base_iri() {
let bridge =
ReificationBridge::with_base_iri(AnnotationStyle::Reification, "http://my.org/stmts/");
let triples = bridge.star_to_reification(&sample_triple());
let stmt_node = triples
.iter()
.filter_map(|t| {
if let StarTerm::NamedNode(n) = &t.subject {
Some(n.iri.clone())
} else {
None
}
})
.next();
assert!(stmt_node.is_some());
assert!(
stmt_node.unwrap().starts_with("http://my.org/stmts/"),
"statement node should use custom base IRI"
);
}
#[test]
fn test_embedded_triple_type_alias() {
let et: EmbeddedTriple = sample_triple();
assert!(et.validate().is_ok());
}
}