use crate::graph::core::Graph;
use crate::utils::error::{Error, Result};
use oxigraph::io::{RdfFormat, RdfSerializer};
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
pub struct GraphExport<'a> {
graph: &'a Graph,
}
impl<'a> GraphExport<'a> {
pub fn new(graph: &'a Graph) -> Self {
Self { graph }
}
pub fn write_to_file<P: AsRef<Path>>(&self, path: P, format: RdfFormat) -> Result<()> {
let file =
File::create(path).map_err(|e| Error::new(&format!("Failed to create file: {}", e)))?;
let writer = BufWriter::new(file);
self.write_to_writer(writer, format)
}
pub fn write_to_writer<W: Write>(&self, mut writer: W, format: RdfFormat) -> Result<()> {
use oxigraph::model::GraphName;
match format {
RdfFormat::Turtle | RdfFormat::NTriples | RdfFormat::RdfXml => {
let mut serializer = RdfSerializer::from_format(format).for_writer(&mut writer);
let default_graph_quads = self.graph.quads_for_pattern(
None, None, None, Some(&GraphName::DefaultGraph), )?;
for quad in default_graph_quads {
serializer
.serialize_quad(&quad)
.map_err(|e| Error::new(&format!("Failed to serialize quad: {}", e)))?;
}
serializer.finish().map_err(|e| {
Error::new(&format!("Failed to finish RDF serialization: {}", e))
})?;
}
RdfFormat::TriG | RdfFormat::NQuads => {
let serializer = RdfSerializer::from_format(format);
self.graph.inner().dump_to_writer(serializer, &mut writer)?;
}
_ => {
let serializer = RdfSerializer::from_format(format);
self.graph.inner().dump_to_writer(serializer, &mut writer)?;
}
}
Ok(())
}
pub fn write_to_string(&self, format: RdfFormat) -> Result<String> {
let mut buffer = Vec::new();
self.write_to_writer(&mut buffer, format)?;
String::from_utf8(buffer)
.map_err(|e| Error::new(&format!("Invalid UTF-8 in RDF output: {}", e)))
}
pub fn write_to_file_auto<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let path = path.as_ref();
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(|s| s.to_ascii_lowercase())
.unwrap_or_default();
let format = match ext.as_str() {
"ttl" | "turtle" => RdfFormat::Turtle,
"nt" | "ntriples" => RdfFormat::NTriples,
"rdf" | "xml" => RdfFormat::RdfXml,
"trig" => RdfFormat::TriG,
"nq" | "nquads" => RdfFormat::NQuads,
other => {
return Err(Error::new(&format!(
"unsupported RDF format extension: {}",
other
)))
}
};
self.write_to_file(path, format)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::core::Graph;
use oxigraph::io::RdfFormat;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_export_write_to_file() {
let graph = Graph::new().unwrap();
graph
.insert_turtle(
r#"
@prefix ex: <http://example.org/> .
ex:alice a ex:Person ;
ex:name "Alice" .
"#,
)
.unwrap();
let export = GraphExport::new(&graph);
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("output.ttl");
export.write_to_file(&file_path, RdfFormat::Turtle).unwrap();
assert!(file_path.exists());
let content = fs::read_to_string(&file_path).unwrap();
assert!(content.contains("ex:alice") || content.contains("http://example.org/alice"));
assert!(content.contains("ex:Person") || content.contains("http://example.org/Person"));
}
#[test]
fn test_export_write_to_string_turtle() {
let graph = Graph::new().unwrap();
graph
.insert_turtle(
r#"
@prefix ex: <http://example.org/> .
ex:alice a ex:Person .
"#,
)
.unwrap();
let export = GraphExport::new(&graph);
let result = export.write_to_string(RdfFormat::Turtle).unwrap();
assert!(!result.is_empty());
assert!(result.contains("ex:") || result.contains("http://example.org/"));
}
#[test]
fn test_export_write_to_string_ntriples() {
let graph = Graph::new().unwrap();
graph
.insert_turtle(
r#"
@prefix ex: <http://example.org/> .
ex:alice a ex:Person .
"#,
)
.unwrap();
let export = GraphExport::new(&graph);
let result = export.write_to_string(RdfFormat::NTriples).unwrap();
assert!(!result.is_empty());
assert!(result.contains("http://example.org/alice"));
}
#[test]
fn test_export_write_to_string_rdfxml() {
let graph = Graph::new().unwrap();
graph
.insert_turtle(
r#"
@prefix ex: <http://example.org/> .
ex:alice a ex:Person .
"#,
)
.unwrap();
let export = GraphExport::new(&graph);
let result = export.write_to_string(RdfFormat::RdfXml).unwrap();
assert!(!result.is_empty());
assert!(result.contains('<') && result.contains('>'));
}
#[test]
fn test_export_write_to_file_auto_turtle() {
let graph = Graph::new().unwrap();
graph
.insert_turtle(
r#"
@prefix ex: <http://example.org/> .
ex:alice a ex:Person .
"#,
)
.unwrap();
let export = GraphExport::new(&graph);
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("output.ttl");
export.write_to_file_auto(&file_path).unwrap();
assert!(file_path.exists());
let content = fs::read_to_string(&file_path).unwrap();
assert!(!content.is_empty());
}
#[test]
fn test_export_write_to_file_auto_ntriples() {
let graph = Graph::new().unwrap();
graph
.insert_turtle(
r#"
@prefix ex: <http://example.org/> .
ex:alice a ex:Person .
"#,
)
.unwrap();
let export = GraphExport::new(&graph);
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("output.nt");
export.write_to_file_auto(&file_path).unwrap();
assert!(file_path.exists());
let content = fs::read_to_string(&file_path).unwrap();
assert!(!content.is_empty());
}
#[test]
fn test_export_write_to_file_auto_unsupported_format() {
let graph = Graph::new().unwrap();
let export = GraphExport::new(&graph);
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("output.unknown");
let result = export.write_to_file_auto(&file_path);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("unsupported"));
}
#[test]
fn test_export_format_specific_serialization() {
let graph = Graph::new().unwrap();
graph
.insert_turtle(
r#"
@prefix ex: <http://example.org/> .
ex:alice a ex:Person .
"#,
)
.unwrap();
let export = GraphExport::new(&graph);
let turtle = export.write_to_string(RdfFormat::Turtle).unwrap();
let ntriples = export.write_to_string(RdfFormat::NTriples).unwrap();
let rdfxml = export.write_to_string(RdfFormat::RdfXml).unwrap();
let trig = export.write_to_string(RdfFormat::TriG).unwrap();
let nquads = export.write_to_string(RdfFormat::NQuads).unwrap();
assert!(!turtle.is_empty());
assert!(!ntriples.is_empty());
assert!(!rdfxml.is_empty());
assert!(!trig.is_empty());
assert!(!nquads.is_empty());
assert!(turtle != ntriples || turtle != rdfxml || ntriples != rdfxml);
}
#[test]
fn test_export_turtle_format_preserves_prefixes() {
let graph = Graph::new().unwrap();
graph
.insert_turtle(
r#"
@prefix ex: <http://example.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
ex:alice a ex:Person ;
ex:name "Alice" .
"#,
)
.unwrap();
let export = GraphExport::new(&graph);
let turtle = export.write_to_string(RdfFormat::Turtle).unwrap();
assert!(turtle.contains("ex:") || turtle.contains("http://example.org/"));
assert!(turtle.contains("alice") || turtle.contains("Alice"));
}
#[test]
fn test_export_ntriples_format_uses_full_iris() {
let graph = Graph::new().unwrap();
graph
.insert_turtle(
r#"
@prefix ex: <http://example.org/> .
ex:alice a ex:Person .
"#,
)
.unwrap();
let export = GraphExport::new(&graph);
let ntriples = export.write_to_string(RdfFormat::NTriples).unwrap();
assert!(ntriples.contains("http://example.org/alice"));
assert!(ntriples.contains("http://example.org/Person"));
}
#[test]
fn test_export_rdfxml_format_has_xml_structure() {
let graph = Graph::new().unwrap();
graph
.insert_turtle(
r#"
@prefix ex: <http://example.org/> .
ex:alice a ex:Person .
"#,
)
.unwrap();
let export = GraphExport::new(&graph);
let rdfxml = export.write_to_string(RdfFormat::RdfXml).unwrap();
assert!(rdfxml.contains('<') && rdfxml.contains('>'));
assert!(rdfxml.contains("rdf:") || rdfxml.contains("RDF"));
}
#[test]
fn test_export_trig_format_supports_named_graphs() {
let graph = Graph::new().unwrap();
graph
.insert_turtle_in(
r#"
@prefix ex: <http://example.org/> .
ex:alice a ex:Person .
"#,
"http://example.org/graph1",
)
.unwrap();
let export = GraphExport::new(&graph);
let trig = export.write_to_string(RdfFormat::TriG).unwrap();
assert!(!trig.is_empty());
assert!(trig.contains("http://example.org/graph1") || trig.contains("alice"));
}
#[test]
fn test_export_nquads_format_includes_graph_context() {
let graph = Graph::new().unwrap();
graph
.insert_turtle_in(
r#"
@prefix ex: <http://example.org/> .
ex:alice a ex:Person .
"#,
"http://example.org/graph1",
)
.unwrap();
let export = GraphExport::new(&graph);
let nquads = export.write_to_string(RdfFormat::NQuads).unwrap();
assert!(!nquads.is_empty());
assert!(nquads.contains("http://example.org/graph1") || nquads.contains("alice"));
}
#[test]
fn test_export_empty_graph() {
let graph = Graph::new().unwrap();
let export = GraphExport::new(&graph);
let result = export.write_to_string(RdfFormat::Turtle).unwrap();
assert!(result.is_empty() || !result.is_empty());
}
}