use crate::model::StarGraph;
use crate::reification::{ReificationStrategy, Reificator};
use crate::{StarError, StarResult};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{debug, info, span, Level};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompatibilityConfig {
pub strategy: ReificationStrategyConfig,
pub base_iri: Option<String>,
pub auto_detect_reification: bool,
pub preserve_blank_nodes: bool,
pub max_nesting_depth: usize,
pub validate_reifications: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ReificationStrategyConfig {
StandardReification,
UniqueIris,
BlankNodes,
SingletonProperties,
}
impl From<ReificationStrategyConfig> for ReificationStrategy {
fn from(config: ReificationStrategyConfig) -> Self {
match config {
ReificationStrategyConfig::StandardReification => {
ReificationStrategy::StandardReification
}
ReificationStrategyConfig::UniqueIris => ReificationStrategy::UniqueIris,
ReificationStrategyConfig::BlankNodes => ReificationStrategy::BlankNodes,
ReificationStrategyConfig::SingletonProperties => {
ReificationStrategy::SingletonProperties
}
}
}
}
impl From<ReificationStrategy> for ReificationStrategyConfig {
fn from(strategy: ReificationStrategy) -> Self {
match strategy {
ReificationStrategy::StandardReification => {
ReificationStrategyConfig::StandardReification
}
ReificationStrategy::UniqueIris => ReificationStrategyConfig::UniqueIris,
ReificationStrategy::BlankNodes => ReificationStrategyConfig::BlankNodes,
ReificationStrategy::SingletonProperties => {
ReificationStrategyConfig::SingletonProperties
}
}
}
}
impl Default for CompatibilityConfig {
fn default() -> Self {
Self {
strategy: ReificationStrategyConfig::StandardReification,
base_iri: Some("http://example.org/statement/".to_string()),
auto_detect_reification: true,
preserve_blank_nodes: true,
max_nesting_depth: 10,
validate_reifications: true,
}
}
}
impl CompatibilityConfig {
pub fn standard_reification() -> Self {
Self {
strategy: ReificationStrategyConfig::StandardReification,
..Default::default()
}
}
pub fn unique_iris(base_iri: String) -> Self {
Self {
strategy: ReificationStrategyConfig::UniqueIris,
base_iri: Some(base_iri),
..Default::default()
}
}
pub fn blank_nodes() -> Self {
Self {
strategy: ReificationStrategyConfig::BlankNodes,
..Default::default()
}
}
pub fn singleton_properties() -> Self {
Self {
strategy: ReificationStrategyConfig::SingletonProperties,
..Default::default()
}
}
pub fn with_base_iri(mut self, base_iri: String) -> Self {
self.base_iri = Some(base_iri);
self
}
pub fn with_auto_detect(mut self, enabled: bool) -> Self {
self.auto_detect_reification = enabled;
self
}
pub fn with_max_nesting_depth(mut self, depth: usize) -> Self {
self.max_nesting_depth = depth;
self
}
}
pub struct CompatibilityMode {
config: CompatibilityConfig,
reificator: Reificator,
statistics: CompatibilityStatistics,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CompatibilityStatistics {
pub conversions_to_standard: usize,
pub conversions_from_standard: usize,
pub quoted_triples_converted: usize,
pub reifications_detected: usize,
pub roundtrip_success_rate: f64,
pub avg_conversion_time_us: f64,
pub strategy_stats: HashMap<String, usize>,
}
impl CompatibilityMode {
pub fn new(config: CompatibilityConfig) -> Self {
let strategy: ReificationStrategy = config.strategy.clone().into();
let reificator = Reificator::new(strategy, config.base_iri.clone());
Self {
config,
reificator,
statistics: CompatibilityStatistics::default(),
}
}
pub fn to_standard_rdf(&mut self, star_graph: &StarGraph) -> StarResult<StarGraph> {
let span = span!(Level::INFO, "to_standard_rdf");
let _enter = span.enter();
let start_time = std::time::Instant::now();
let quoted_count = star_graph
.triples()
.iter()
.filter(|t| t.subject.is_quoted_triple() || t.object.is_quoted_triple())
.count();
let standard_graph = self.reificator.reify_graph(star_graph)?;
self.statistics.conversions_to_standard += 1;
self.statistics.quoted_triples_converted += quoted_count;
let conversion_time = start_time.elapsed().as_micros() as f64;
self.update_avg_conversion_time(conversion_time);
let strategy_name = format!("{:?}", self.config.strategy);
*self
.statistics
.strategy_stats
.entry(strategy_name)
.or_insert(0) += 1;
info!(
"Converted RDF-star graph ({} triples, {} quoted) to standard RDF ({} triples) in {:.2}ms",
star_graph.len(),
quoted_count,
standard_graph.len(),
conversion_time / 1000.0
);
Ok(standard_graph)
}
pub fn from_standard_rdf(&mut self, standard_graph: &StarGraph) -> StarResult<StarGraph> {
let span = span!(Level::INFO, "from_standard_rdf");
let _enter = span.enter();
let start_time = std::time::Instant::now();
if self.config.auto_detect_reification {
let reification_count = crate::reification::utils::count_reifications(standard_graph);
self.statistics.reifications_detected += reification_count;
debug!("Detected {} reification patterns", reification_count);
}
if self.config.validate_reifications {
if let Err(e) = crate::reification::utils::validate_reifications(standard_graph) {
return Err(StarError::reification_error(format!(
"Invalid reification patterns: {}",
e
)));
}
}
let star_graph = self.reificator.dereify_graph(standard_graph)?;
self.statistics.conversions_from_standard += 1;
let conversion_time = start_time.elapsed().as_micros() as f64;
self.update_avg_conversion_time(conversion_time);
info!(
"Converted standard RDF ({} triples) back to RDF-star ({} triples) in {:.2}ms",
standard_graph.len(),
star_graph.len(),
conversion_time / 1000.0
);
Ok(star_graph)
}
pub fn test_roundtrip(&mut self, star_graph: &StarGraph) -> StarResult<bool> {
let span = span!(Level::INFO, "test_roundtrip");
let _enter = span.enter();
let standard_graph = self.to_standard_rdf(star_graph)?;
let recovered_graph = self.from_standard_rdf(&standard_graph)?;
let success = star_graph.len() == recovered_graph.len();
let total_roundtrips = (self.statistics.conversions_to_standard
+ self.statistics.conversions_from_standard) as f64
/ 2.0;
if success {
self.statistics.roundtrip_success_rate =
(self.statistics.roundtrip_success_rate * (total_roundtrips - 1.0) + 1.0)
/ total_roundtrips;
} else {
self.statistics.roundtrip_success_rate = (self.statistics.roundtrip_success_rate
* (total_roundtrips - 1.0))
/ total_roundtrips;
}
info!(
"Round-trip test: {} (original: {} triples, recovered: {} triples)",
if success { "SUCCESS" } else { "FAILED" },
star_graph.len(),
recovered_graph.len()
);
Ok(success)
}
pub fn has_quoted_triples(graph: &StarGraph) -> bool {
graph
.triples()
.iter()
.any(|t| t.subject.is_quoted_triple() || t.object.is_quoted_triple())
}
pub fn has_reifications(graph: &StarGraph) -> bool {
crate::reification::utils::has_reifications(graph)
}
pub fn count_quoted_triples(graph: &StarGraph) -> usize {
graph
.triples()
.iter()
.filter(|t| t.subject.is_quoted_triple() || t.object.is_quoted_triple())
.count()
}
pub fn count_reifications(graph: &StarGraph) -> usize {
crate::reification::utils::count_reifications(graph)
}
pub fn statistics(&self) -> &CompatibilityStatistics {
&self.statistics
}
pub fn reset_statistics(&mut self) {
self.statistics = CompatibilityStatistics::default();
}
pub fn config(&self) -> &CompatibilityConfig {
&self.config
}
pub fn set_config(&mut self, config: CompatibilityConfig) {
let strategy: ReificationStrategy = config.strategy.clone().into();
self.reificator = Reificator::new(strategy, config.base_iri.clone());
self.config = config;
}
fn update_avg_conversion_time(&mut self, new_time: f64) {
let total_conversions = (self.statistics.conversions_to_standard
+ self.statistics.conversions_from_standard) as f64;
if total_conversions == 1.0 {
self.statistics.avg_conversion_time_us = new_time;
} else {
self.statistics.avg_conversion_time_us =
(self.statistics.avg_conversion_time_us * (total_conversions - 1.0) + new_time)
/ total_conversions;
}
}
pub fn unstar(&mut self, star_graph: &StarGraph) -> StarResult<StarGraph> {
let original_strategy = self.config.strategy.clone();
if original_strategy != ReificationStrategyConfig::StandardReification {
self.config.strategy = ReificationStrategyConfig::StandardReification;
let strategy: ReificationStrategy = self.config.strategy.clone().into();
self.reificator = Reificator::new(strategy, self.config.base_iri.clone());
}
let result = self.to_standard_rdf(star_graph);
if original_strategy != ReificationStrategyConfig::StandardReification {
self.config.strategy = original_strategy;
let strategy: ReificationStrategy = self.config.strategy.clone().into();
self.reificator = Reificator::new(strategy, self.config.base_iri.clone());
}
result
}
pub fn rdfstar(&mut self, standard_graph: &StarGraph) -> StarResult<StarGraph> {
let original_strategy = self.config.strategy.clone();
if original_strategy != ReificationStrategyConfig::StandardReification {
self.config.strategy = ReificationStrategyConfig::StandardReification;
let strategy: ReificationStrategy = self.config.strategy.clone().into();
self.reificator = Reificator::new(strategy, self.config.base_iri.clone());
}
let result = self.from_standard_rdf(standard_graph);
if original_strategy != ReificationStrategyConfig::StandardReification {
self.config.strategy = original_strategy;
let strategy: ReificationStrategy = self.config.strategy.clone().into();
self.reificator = Reificator::new(strategy, self.config.base_iri.clone());
}
result
}
pub fn test_unstar_roundtrip(&mut self, star_graph: &StarGraph) -> StarResult<bool> {
let span = span!(Level::INFO, "test_unstar_roundtrip");
let _enter = span.enter();
let unstarred = self.unstar(star_graph)?;
let recovered = self.rdfstar(&unstarred)?;
let success = star_graph.len() == recovered.len();
info!(
"W3C unstar round-trip test: {} (original: {} triples, recovered: {} triples)",
if success { "SUCCESS" } else { "FAILED" },
star_graph.len(),
recovered.len()
);
Ok(success)
}
}
pub struct CompatibilityPresets;
impl CompatibilityPresets {
pub fn apache_jena() -> CompatibilityConfig {
CompatibilityConfig {
strategy: ReificationStrategyConfig::StandardReification,
base_iri: Some("http://jena.apache.org/statement/".to_string()),
auto_detect_reification: true,
preserve_blank_nodes: true,
max_nesting_depth: 5,
validate_reifications: true,
}
}
pub fn rdf4j() -> CompatibilityConfig {
CompatibilityConfig {
strategy: ReificationStrategyConfig::UniqueIris,
base_iri: Some("http://rdf4j.org/statement/".to_string()),
auto_detect_reification: true,
preserve_blank_nodes: true,
max_nesting_depth: 5,
validate_reifications: true,
}
}
pub fn virtuoso() -> CompatibilityConfig {
CompatibilityConfig {
strategy: ReificationStrategyConfig::BlankNodes,
base_iri: None,
auto_detect_reification: true,
preserve_blank_nodes: true,
max_nesting_depth: 3,
validate_reifications: false, }
}
pub fn efficient() -> CompatibilityConfig {
CompatibilityConfig {
strategy: ReificationStrategyConfig::SingletonProperties,
base_iri: Some("http://example.org/property/".to_string()),
auto_detect_reification: true,
preserve_blank_nodes: false,
max_nesting_depth: 10,
validate_reifications: false,
}
}
}
pub struct BatchCompatibilityConverter {
config: CompatibilityConfig,
statistics: CompatibilityStatistics,
}
impl BatchCompatibilityConverter {
pub fn new(config: CompatibilityConfig) -> Self {
Self {
config,
statistics: CompatibilityStatistics::default(),
}
}
pub fn batch_to_standard_rdf(
&mut self,
star_graphs: Vec<StarGraph>,
) -> StarResult<Vec<StarGraph>> {
let span = span!(Level::INFO, "batch_to_standard_rdf");
let _enter = span.enter();
let start_time = std::time::Instant::now();
let mut results = Vec::new();
for star_graph in star_graphs {
let mut compat = CompatibilityMode::new(self.config.clone());
let standard_graph = compat.to_standard_rdf(&star_graph)?;
results.push(standard_graph);
self.aggregate_statistics(&compat.statistics);
}
let total_time = start_time.elapsed();
info!(
"Batch converted {} graphs to standard RDF in {:?}",
results.len(),
total_time
);
Ok(results)
}
pub fn batch_from_standard_rdf(
&mut self,
standard_graphs: Vec<StarGraph>,
) -> StarResult<Vec<StarGraph>> {
let span = span!(Level::INFO, "batch_from_standard_rdf");
let _enter = span.enter();
let start_time = std::time::Instant::now();
let mut results = Vec::new();
for standard_graph in standard_graphs {
let mut compat = CompatibilityMode::new(self.config.clone());
let star_graph = compat.from_standard_rdf(&standard_graph)?;
results.push(star_graph);
self.aggregate_statistics(&compat.statistics);
}
let total_time = start_time.elapsed();
info!(
"Batch converted {} graphs from standard RDF in {:?}",
results.len(),
total_time
);
Ok(results)
}
pub fn statistics(&self) -> &CompatibilityStatistics {
&self.statistics
}
pub fn reset_statistics(&mut self) {
self.statistics = CompatibilityStatistics::default();
}
fn aggregate_statistics(&mut self, other: &CompatibilityStatistics) {
self.statistics.conversions_to_standard += other.conversions_to_standard;
self.statistics.conversions_from_standard += other.conversions_from_standard;
self.statistics.quoted_triples_converted += other.quoted_triples_converted;
self.statistics.reifications_detected += other.reifications_detected;
for (strategy, count) in &other.strategy_stats {
*self
.statistics
.strategy_stats
.entry(strategy.clone())
.or_insert(0) += count;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{StarTerm, StarTriple};
#[test]
fn test_basic_compatibility_mode() {
let config = CompatibilityConfig::standard_reification();
let mut compat = CompatibilityMode::new(config);
let mut star_graph = StarGraph::new();
let quoted = StarTriple::new(
StarTerm::iri("http://example.org/alice").unwrap(),
StarTerm::iri("http://example.org/age").unwrap(),
StarTerm::literal("25").unwrap(),
);
let meta = StarTriple::new(
StarTerm::quoted_triple(quoted),
StarTerm::iri("http://example.org/certainty").unwrap(),
StarTerm::literal("0.9").unwrap(),
);
star_graph.insert(meta).unwrap();
let standard_graph = compat.to_standard_rdf(&star_graph).unwrap();
assert!(standard_graph.len() > 1);
let recovered = compat.from_standard_rdf(&standard_graph).unwrap();
assert_eq!(recovered.len(), 1); }
#[test]
fn test_roundtrip_conversion() {
let config = CompatibilityConfig::standard_reification();
let mut compat = CompatibilityMode::new(config);
let mut star_graph = StarGraph::new();
let quoted = StarTriple::new(
StarTerm::iri("http://example.org/alice").unwrap(),
StarTerm::iri("http://example.org/age").unwrap(),
StarTerm::literal("25").unwrap(),
);
let meta = StarTriple::new(
StarTerm::quoted_triple(quoted),
StarTerm::iri("http://example.org/certainty").unwrap(),
StarTerm::literal("0.9").unwrap(),
);
star_graph.insert(meta).unwrap();
let success = compat.test_roundtrip(&star_graph).unwrap();
assert!(success);
}
#[test]
fn test_detection_functions() {
let mut star_graph = StarGraph::new();
let quoted = StarTriple::new(
StarTerm::iri("http://example.org/alice").unwrap(),
StarTerm::iri("http://example.org/age").unwrap(),
StarTerm::literal("25").unwrap(),
);
let meta = StarTriple::new(
StarTerm::quoted_triple(quoted),
StarTerm::iri("http://example.org/certainty").unwrap(),
StarTerm::literal("0.9").unwrap(),
);
star_graph.insert(meta).unwrap();
assert!(CompatibilityMode::has_quoted_triples(&star_graph));
assert_eq!(CompatibilityMode::count_quoted_triples(&star_graph), 1);
let config = CompatibilityConfig::standard_reification();
let mut compat = CompatibilityMode::new(config);
let standard_graph = compat.to_standard_rdf(&star_graph).unwrap();
assert!(CompatibilityMode::has_reifications(&standard_graph));
assert_eq!(CompatibilityMode::count_reifications(&standard_graph), 1);
}
#[test]
fn test_compatibility_presets() {
let _jena = CompatibilityPresets::apache_jena();
let _rdf4j = CompatibilityPresets::rdf4j();
let _virtuoso = CompatibilityPresets::virtuoso();
let _efficient = CompatibilityPresets::efficient();
}
#[test]
fn test_batch_conversion() {
let config = CompatibilityConfig::standard_reification();
let mut batch = BatchCompatibilityConverter::new(config);
let mut graphs = Vec::new();
for i in 0..3 {
let mut graph = StarGraph::new();
let quoted = StarTriple::new(
StarTerm::iri(&format!("http://example.org/subject{i}")).unwrap(),
StarTerm::iri("http://example.org/predicate").unwrap(),
StarTerm::iri("http://example.org/object").unwrap(),
);
let meta = StarTriple::new(
StarTerm::quoted_triple(quoted),
StarTerm::iri("http://example.org/meta").unwrap(),
StarTerm::literal("value").unwrap(),
);
graph.insert(meta).unwrap();
graphs.push(graph);
}
let standard_graphs = batch.batch_to_standard_rdf(graphs).unwrap();
assert_eq!(standard_graphs.len(), 3);
let stats = batch.statistics();
assert_eq!(stats.conversions_to_standard, 3);
}
#[test]
fn test_statistics() {
let config = CompatibilityConfig::standard_reification();
let mut compat = CompatibilityMode::new(config);
let mut star_graph = StarGraph::new();
let quoted = StarTriple::new(
StarTerm::iri("http://example.org/alice").unwrap(),
StarTerm::iri("http://example.org/age").unwrap(),
StarTerm::literal("25").unwrap(),
);
let meta = StarTriple::new(
StarTerm::quoted_triple(quoted),
StarTerm::iri("http://example.org/certainty").unwrap(),
StarTerm::literal("0.9").unwrap(),
);
star_graph.insert(meta).unwrap();
let _standard = compat.to_standard_rdf(&star_graph).unwrap();
let stats = compat.statistics();
assert_eq!(stats.conversions_to_standard, 1);
assert_eq!(stats.quoted_triples_converted, 1);
assert!(stats.avg_conversion_time_us > 0.0);
}
}