use std::fmt::Write as FmtWrite;
use std::io::Write as IoWrite;
use tensorlogic_adapters::{DomainInfo, PredicateInfo, SymbolTable};
use crate::error::BridgeError;
pub struct TurtleExporter {
pub base_iri: String,
pub prefixes: Vec<(String, String)>,
}
impl TurtleExporter {
pub fn new(base_iri: impl Into<String>) -> Self {
Self {
base_iri: base_iri.into(),
prefixes: Vec::new(),
}
}
pub fn with_prefix(mut self, prefix: &str, namespace: &str) -> Self {
self.prefixes
.push((prefix.to_owned(), namespace.to_owned()));
self
}
pub fn export_symbol_table(&self, table: &SymbolTable) -> Result<String, BridgeError> {
let mut out = String::new();
self.write_prefixes(&mut out);
for (name, info) in &table.domains {
self.write_class(name, info, &mut out);
}
for (name, info) in &table.predicates {
self.write_property(name, info, &mut out);
}
Ok(out)
}
pub fn write_to_file(
&self,
table: &SymbolTable,
path: &std::path::Path,
) -> Result<(), BridgeError> {
let content = self.export_symbol_table(table)?;
let mut file = std::fs::File::create(path).map_err(|e| {
BridgeError::InvalidSchema(format!("Cannot create file {}: {}", path.display(), e))
})?;
file.write_all(content.as_bytes()).map_err(|e| {
BridgeError::InvalidSchema(format!("Cannot write to file {}: {}", path.display(), e))
})?;
Ok(())
}
fn escape_iri(iri: &str) -> String {
let escaped = iri
.replace('\\', "\\\\")
.replace('<', "%3C")
.replace('>', "%3E")
.replace('"', "%22")
.replace('{', "%7B")
.replace('}', "%7D")
.replace('|', "%7C")
.replace('^', "%5E")
.replace('`', "%60")
.replace(' ', "%20");
format!("<{}>", escaped)
}
fn escape_literal(s: &str) -> String {
let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{}\"", escaped)
}
fn write_prefixes(&self, out: &mut String) {
writeln!(
out,
"@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> ."
)
.ok();
writeln!(
out,
"@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> ."
)
.ok();
writeln!(out, "@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .").ok();
writeln!(out, "@prefix owl: <http://www.w3.org/2002/07/owl#> .").ok();
for (prefix, ns) in &self.prefixes {
writeln!(out, "@prefix {}: {} .", prefix, Self::escape_iri(ns)).ok();
}
writeln!(out).ok(); }
fn write_class(&self, class_name: &str, _info: &DomainInfo, out: &mut String) {
let iri = Self::escape_iri(&format!("{}{}", self.base_iri, class_name));
let label = Self::escape_literal(class_name);
writeln!(out, "{} a rdfs:Class ;", iri).ok();
writeln!(out, " rdfs:label {} .", label).ok();
writeln!(out).ok();
}
fn write_property(&self, pred_name: &str, info: &PredicateInfo, out: &mut String) {
let iri = Self::escape_iri(&format!("{}{}", self.base_iri, pred_name));
let label = Self::escape_literal(pred_name);
writeln!(out, "{} a rdf:Property ;", iri).ok();
writeln!(out, " rdfs:label {} ;", label).ok();
if let Some(domain_name) = info.arg_domains.first() {
let domain_iri = Self::escape_iri(&format!("{}{}", self.base_iri, domain_name));
writeln!(out, " rdfs:domain {} ;", domain_iri).ok();
}
writeln!(out, " rdfs:range xsd:string .").ok();
writeln!(out).ok();
}
}
#[cfg(test)]
mod tests {
use super::*;
use tensorlogic_adapters::{DomainInfo, PredicateInfo};
fn make_table_empty() -> SymbolTable {
SymbolTable::new()
}
fn make_table_with_domain() -> SymbolTable {
let mut t = SymbolTable::new();
t.add_domain(DomainInfo::new("Person", 0)).unwrap();
t
}
fn make_table_with_predicate() -> SymbolTable {
let mut t = SymbolTable::new();
t.add_domain(DomainInfo::new("Person", 0)).unwrap();
t.add_predicate(PredicateInfo::new("name", vec!["Person".to_owned()]))
.unwrap();
t
}
#[test]
fn test_export_empty_symbol_table_has_prefixes() {
let exp = TurtleExporter::new("http://example.org/");
let ttl = exp.export_symbol_table(&make_table_empty()).unwrap();
assert!(ttl.contains("@prefix rdf:"), "missing rdf prefix");
assert!(ttl.contains("@prefix rdfs:"), "missing rdfs prefix");
assert!(ttl.contains("@prefix xsd:"), "missing xsd prefix");
assert!(ttl.contains("@prefix owl:"), "missing owl prefix");
}
#[test]
fn test_export_single_domain_as_class() {
let exp = TurtleExporter::new("http://example.org/");
let ttl = exp.export_symbol_table(&make_table_with_domain()).unwrap();
assert!(ttl.contains("rdfs:Class"), "expected rdfs:Class");
assert!(ttl.contains("Person"), "expected class name");
}
#[test]
fn test_export_single_predicate_as_property() {
let exp = TurtleExporter::new("http://example.org/");
let ttl = exp
.export_symbol_table(&make_table_with_predicate())
.unwrap();
assert!(ttl.contains("rdf:Property"), "expected rdf:Property");
assert!(ttl.contains("name"), "expected predicate name");
}
#[test]
fn test_export_property_with_domain() {
let exp = TurtleExporter::new("http://example.org/");
let ttl = exp
.export_symbol_table(&make_table_with_predicate())
.unwrap();
assert!(ttl.contains("rdfs:domain"), "expected rdfs:domain");
assert!(ttl.contains("Person"), "expected domain IRI fragment");
}
#[test]
fn test_escape_literal_special_chars() {
let escaped = TurtleExporter::escape_literal("say \"hello\" and \\goodbye");
assert!(escaped.contains("\\\""), "quote not escaped");
assert!(escaped.contains("\\\\"), "backslash not escaped");
}
#[test]
fn test_export_multiple_domains() {
let mut t = SymbolTable::new();
t.add_domain(DomainInfo::new("Dog", 0)).unwrap();
t.add_domain(DomainInfo::new("Cat", 0)).unwrap();
let exp = TurtleExporter::new("http://example.org/");
let ttl = exp.export_symbol_table(&t).unwrap();
assert!(ttl.contains("Dog"), "expected Dog");
assert!(ttl.contains("Cat"), "expected Cat");
}
#[test]
fn test_export_multiple_predicates() {
let mut t = SymbolTable::new();
t.add_domain(DomainInfo::new("Entity", 0)).unwrap();
t.add_predicate(PredicateInfo::new("likes", vec!["Entity".to_owned()]))
.unwrap();
t.add_predicate(PredicateInfo::new("dislikes", vec!["Entity".to_owned()]))
.unwrap();
let exp = TurtleExporter::new("http://example.org/");
let ttl = exp.export_symbol_table(&t).unwrap();
assert!(ttl.contains("likes"), "expected predicate 'likes'");
assert!(ttl.contains("dislikes"), "expected predicate 'dislikes'");
}
#[test]
fn test_output_contains_base_iri() {
let base = "http://mybase.org/onto/";
let exp = TurtleExporter::new(base);
let mut t = SymbolTable::new();
t.add_domain(DomainInfo::new("Widget", 0)).unwrap();
let ttl = exp.export_symbol_table(&t).unwrap();
assert!(
ttl.contains(base),
"output should contain the configured base IRI"
);
}
#[test]
fn test_write_to_file_creates_file() {
let dir = std::env::temp_dir();
let path = dir.join("test_turtle_export_output.ttl");
let exp = TurtleExporter::new("http://example.org/");
exp.write_to_file(&make_table_with_domain(), &path).unwrap();
assert!(path.exists(), "file should have been created");
let _ = std::fs::remove_file(&path);
}
}