use crate::error::{TextPosition, TurtleParseError, TurtleResult, TurtleSyntaxError};
use crate::formats::turtle::TurtleParser;
use crate::toolkit::{Parser, Serializer};
use oxirs_core::model::{BlankNode, GraphName, Literal, NamedNode, Quad, Triple};
use std::collections::HashMap;
use std::io::{BufRead, BufReader, Read, Write};
#[derive(Debug, Clone)]
pub struct TriGParser {
pub lenient: bool,
pub base_iri: Option<String>,
pub prefixes: HashMap<String, String>,
}
impl Default for TriGParser {
fn default() -> Self {
Self::new()
}
}
impl TriGParser {
pub fn new() -> Self {
let mut prefixes = HashMap::new();
prefixes.insert(
"rdf".to_string(),
"http://www.w3.org/1999/02/22-rdf-syntax-ns#".to_string(),
);
prefixes.insert(
"rdfs".to_string(),
"http://www.w3.org/2000/01/rdf-schema#".to_string(),
);
prefixes.insert(
"xsd".to_string(),
"http://www.w3.org/2001/XMLSchema#".to_string(),
);
Self {
lenient: false,
base_iri: None,
prefixes,
}
}
fn parse_trig_content<R: BufRead>(&self, reader: R) -> TurtleResult<Vec<Quad>> {
let mut quads = Vec::new();
let mut current_graph = GraphName::DefaultGraph;
let mut graph_depth = 0; let mut prefixes = self.prefixes.clone();
let mut base_iri = self.base_iri.clone();
let content = {
let mut buffer = String::new();
let mut reader = reader;
reader.read_to_string(&mut buffer).map_err(|e| {
TurtleParseError::Io(std::io::Error::new(std::io::ErrorKind::Other, e))
})?;
buffer
};
let lines: Vec<&str> = content.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i].trim();
if line.is_empty() || line.starts_with('#') {
i += 1;
continue;
}
if line.starts_with("@prefix") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let prefix = parts[1].trim_end_matches(':');
let iri = parts[2]
.trim_start_matches('<')
.trim_end_matches('>')
.trim_end_matches('.');
let resolved_iri = Self::resolve_iri(iri, &base_iri);
prefixes.insert(prefix.to_string(), resolved_iri);
}
i += 1;
continue;
}
if line.starts_with("@base") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let iri = parts[1]
.trim_start_matches('<')
.trim_end_matches('>')
.trim_end_matches('.');
base_iri = Some(iri.to_string());
}
i += 1;
continue;
}
if line.contains('{') && !line.ends_with('.') {
let graph_part = line.split('{').next().unwrap_or("").trim();
if graph_part.starts_with("GRAPH") {
let graph_iri = graph_part.strip_prefix("GRAPH").unwrap_or("").trim();
current_graph = self
.parse_graph_name_with_prefixes_and_base(graph_iri, &prefixes, &base_iri)?;
} else if !graph_part.is_empty() {
current_graph = self.parse_graph_name_with_prefixes_and_base(
graph_part, &prefixes, &base_iri,
)?;
} else {
current_graph = GraphName::DefaultGraph;
}
graph_depth += 1;
i += 1;
continue;
}
if line.trim() == "}" {
if graph_depth == 0 {
return Err(TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: "Unexpected closing brace '}'".to_string(),
position: TextPosition::default(),
}));
}
graph_depth -= 1;
current_graph = GraphName::DefaultGraph;
i += 1;
continue;
}
if !line.is_empty()
&& !line.starts_with('@')
&& !line.contains('{')
&& line.trim() != "}"
{
let line_without_comment = if let Some(comment_pos) = line.find('#') {
line[..comment_pos].trim_end()
} else {
line
};
let mut statement = line_without_comment.to_string();
let count_triple_quotes =
|s: &str| s.matches("\"\"\"").count() + s.matches("'''").count();
while i + 1 < lines.len() {
let inside_multiline = count_triple_quotes(&statement) % 2 == 1;
if !inside_multiline && statement.trim_end().ends_with('.') {
break;
}
i += 1;
let next_line = lines[i].trim();
if next_line == "}" {
i -= 1; break;
}
if !next_line.is_empty() && !next_line.starts_with('#') {
let next_line_without_comment =
if let Some(comment_pos) = next_line.find('#') {
next_line[..comment_pos].trim_end()
} else {
next_line
};
if !next_line_without_comment.is_empty() {
statement.push('\n');
statement.push_str(next_line_without_comment);
}
}
}
if statement.trim_end().ends_with('.') {
match self.parse_triple_with_turtle(&statement, &prefixes, &base_iri) {
Ok(triples) => {
for triple in triples {
let quad = Quad::new(
triple.subject().clone(),
triple.predicate().clone(),
triple.object().clone(),
current_graph.clone(),
);
quads.push(quad);
}
}
Err(_e) if self.lenient => {
}
Err(e) => return Err(e),
}
} else if !statement.trim().is_empty() && !self.lenient {
return Err(TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Statement must end with '.': {}", statement.trim()),
position: TextPosition::default(),
}));
}
}
i += 1;
}
if graph_depth > 0 && !self.lenient {
return Err(TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: "Unclosed graph: missing closing brace '}'".to_string(),
position: TextPosition::default(),
}));
}
Ok(quads)
}
fn parse_triple_with_turtle(
&self,
content: &str,
prefixes: &HashMap<String, String>,
base_iri: &Option<String>,
) -> TurtleResult<Vec<Triple>> {
let mut document = String::new();
if let Some(base) = base_iri {
document.push_str(&format!("@base <{}> .\n", base));
}
for (prefix, iri) in prefixes {
document.push_str(&format!("@prefix {}: <{}> .\n", prefix, iri));
}
document.push_str(content);
let mut turtle_parser = TurtleParser::new();
if self.lenient {
turtle_parser.lenient = true;
}
turtle_parser.parse_document(&document)
}
#[allow(dead_code)]
fn parse_simple_triple(&self, line: &str) -> TurtleResult<Triple> {
self.parse_simple_triple_with_prefixes(line, &self.prefixes)
}
#[allow(dead_code)]
fn parse_simple_triple_with_prefixes(
&self,
line: &str,
prefixes: &HashMap<String, String>,
) -> TurtleResult<Triple> {
let line = line.trim_end_matches('.').trim();
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 3 {
return Err(TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: "Invalid triple syntax".to_string(),
position: TextPosition::start(),
}));
}
let subject = self.parse_term_as_subject_with_prefixes(parts[0], prefixes)?;
let predicate = self.parse_term_as_predicate_with_prefixes(parts[1], prefixes)?;
let object = self.parse_term_as_object_with_prefixes(&parts[2..].join(" "), prefixes)?;
Ok(Triple::new(subject, predicate, object))
}
#[allow(dead_code)]
fn parse_graph_name(&self, graph_str: &str) -> TurtleResult<GraphName> {
self.parse_graph_name_with_prefixes(graph_str, &self.prefixes)
}
fn parse_graph_name_with_prefixes(
&self,
graph_str: &str,
prefixes: &HashMap<String, String>,
) -> TurtleResult<GraphName> {
self.parse_graph_name_with_prefixes_and_base(graph_str, prefixes, &None)
}
fn parse_graph_name_with_prefixes_and_base(
&self,
graph_str: &str,
prefixes: &HashMap<String, String>,
base_iri: &Option<String>,
) -> TurtleResult<GraphName> {
let graph_str = graph_str.trim();
if graph_str.starts_with('<') && graph_str.ends_with('>') {
let iri = graph_str.trim_start_matches('<').trim_end_matches('>');
let resolved_iri = Self::resolve_iri(iri, base_iri);
let named_node = NamedNode::new(&resolved_iri).map_err(|e| {
TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Invalid IRI: {e}"),
position: TextPosition::start(),
})
})?;
Ok(GraphName::NamedNode(named_node))
} else if graph_str.starts_with("_:") {
let label = graph_str.trim_start_matches("_:");
let blank_node = BlankNode::new(label).map_err(|e| {
TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Invalid blank node: {e}"),
position: TextPosition::start(),
})
})?;
Ok(GraphName::BlankNode(blank_node))
} else if graph_str.contains(':') {
let expanded = Self::expand_prefixed_name_static(graph_str, prefixes)?;
let named_node = NamedNode::new(&expanded).map_err(|e| {
TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Invalid IRI: {e}"),
position: TextPosition::start(),
})
})?;
Ok(GraphName::NamedNode(named_node))
} else {
Err(TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Invalid graph name: {graph_str}"),
position: TextPosition::start(),
}))
}
}
fn resolve_iri(iri: &str, base_iri: &Option<String>) -> String {
if iri.contains("://")
|| iri.starts_with("http:")
|| iri.starts_with("https:")
|| iri.starts_with("urn:")
{
return iri.to_string();
}
if let Some(base) = base_iri {
if base.ends_with('/') || base.ends_with('#') {
format!("{}{}", base, iri)
} else {
format!("{}/{}", base, iri)
}
} else {
iri.to_string()
}
}
#[allow(dead_code)]
fn expand_prefixed_name(&self, prefixed: &str) -> TurtleResult<String> {
Self::expand_prefixed_name_static(prefixed, &self.prefixes)
}
fn expand_prefixed_name_static(
prefixed: &str,
prefixes: &HashMap<String, String>,
) -> TurtleResult<String> {
if let Some(colon_pos) = prefixed.find(':') {
let prefix = &prefixed[..colon_pos];
let local = &prefixed[colon_pos + 1..];
if let Some(namespace) = prefixes.get(prefix) {
Ok(format!("{namespace}{local}"))
} else {
Err(TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Unknown prefix: {prefix}"),
position: TextPosition::start(),
}))
}
} else {
Err(TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Invalid prefixed name: {prefixed}"),
position: TextPosition::start(),
}))
}
}
#[allow(dead_code)]
fn parse_term_as_subject(&self, term: &str) -> TurtleResult<oxirs_core::model::Subject> {
self.parse_term_as_subject_with_prefixes(term, &self.prefixes)
}
#[allow(dead_code)]
fn parse_term_as_subject_with_prefixes(
&self,
term: &str,
prefixes: &HashMap<String, String>,
) -> TurtleResult<oxirs_core::model::Subject> {
use oxirs_core::model::{BlankNode, Subject};
if term.starts_with('<') && term.ends_with('>') {
let iri = term.trim_start_matches('<').trim_end_matches('>');
let named_node = NamedNode::new(iri).map_err(|e| {
TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Invalid IRI: {e}"),
position: TextPosition::start(),
})
})?;
Ok(Subject::NamedNode(named_node))
} else if let Some(stripped) = term.strip_prefix("_:") {
let blank_node = BlankNode::new(stripped).map_err(|e| {
TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Invalid blank node: {e}"),
position: TextPosition::start(),
})
})?;
Ok(Subject::BlankNode(blank_node))
} else if term.contains(':') {
let expanded = Self::expand_prefixed_name_static(term, prefixes)?;
let named_node = NamedNode::new(&expanded).map_err(|e| {
TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Invalid IRI: {e}"),
position: TextPosition::start(),
})
})?;
Ok(Subject::NamedNode(named_node))
} else {
Err(TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Invalid subject: {term}"),
position: TextPosition::start(),
}))
}
}
#[allow(dead_code)]
fn parse_term_as_predicate(&self, term: &str) -> TurtleResult<oxirs_core::model::Predicate> {
self.parse_term_as_predicate_with_prefixes(term, &self.prefixes)
}
#[allow(dead_code)]
fn parse_term_as_predicate_with_prefixes(
&self,
term: &str,
prefixes: &HashMap<String, String>,
) -> TurtleResult<oxirs_core::model::Predicate> {
use oxirs_core::model::Predicate;
if term == "a" {
let rdf_type = NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")
.expect("valid IRI");
Ok(Predicate::NamedNode(rdf_type))
} else if term.starts_with('<') && term.ends_with('>') {
let iri = term.trim_start_matches('<').trim_end_matches('>');
let named_node = NamedNode::new(iri).map_err(|e| {
TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Invalid IRI: {e}"),
position: TextPosition::start(),
})
})?;
Ok(Predicate::NamedNode(named_node))
} else if term.contains(':') {
let expanded = Self::expand_prefixed_name_static(term, prefixes)?;
let named_node = NamedNode::new(&expanded).map_err(|e| {
TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Invalid IRI: {e}"),
position: TextPosition::start(),
})
})?;
Ok(Predicate::NamedNode(named_node))
} else {
Err(TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Invalid predicate: {term}"),
position: TextPosition::start(),
}))
}
}
#[allow(dead_code)]
fn parse_term_as_object(&self, term: &str) -> TurtleResult<oxirs_core::model::Object> {
self.parse_term_as_object_with_prefixes(term, &self.prefixes)
}
#[allow(dead_code)]
fn parse_term_as_object_with_prefixes(
&self,
term: &str,
prefixes: &HashMap<String, String>,
) -> TurtleResult<oxirs_core::model::Object> {
use oxirs_core::model::{BlankNode, Literal, Object};
let term = term.trim();
if term.starts_with('"') && term.ends_with('"') {
let content = &term[1..term.len() - 1];
let literal = Literal::new_simple_literal(content);
Ok(Object::Literal(literal))
} else if term.starts_with('<') && term.ends_with('>') {
let iri = term.trim_start_matches('<').trim_end_matches('>');
let named_node = NamedNode::new(iri).map_err(|e| {
TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Invalid IRI: {e}"),
position: TextPosition::start(),
})
})?;
Ok(Object::NamedNode(named_node))
} else if let Some(stripped) = term.strip_prefix("_:") {
let blank_node = BlankNode::new(stripped).map_err(|e| {
TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Invalid blank node: {e}"),
position: TextPosition::start(),
})
})?;
Ok(Object::BlankNode(blank_node))
} else if term.contains(':') {
let expanded = Self::expand_prefixed_name_static(term, prefixes)?;
let named_node = NamedNode::new(&expanded).map_err(|e| {
TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: format!("Invalid IRI: {e}"),
position: TextPosition::start(),
})
})?;
Ok(Object::NamedNode(named_node))
} else {
let literal = Literal::new_simple_literal(term);
Ok(Object::Literal(literal))
}
}
}
impl Parser<Quad> for TriGParser {
fn parse<R: Read>(&self, reader: R) -> TurtleResult<Vec<Quad>> {
let buf_reader = BufReader::new(reader);
self.parse_trig_content(buf_reader)
}
fn for_reader<R: BufRead>(&self, reader: R) -> Box<dyn Iterator<Item = TurtleResult<Quad>>> {
match self.parse_trig_content(reader) {
Ok(quads) => Box::new(quads.into_iter().map(Ok)),
Err(e) => Box::new(std::iter::once(Err(e))),
}
}
}
#[derive(Debug, Clone)]
pub struct TriGSerializer {
pub base_iri: Option<String>,
pub prefixes: HashMap<String, String>,
}
impl Default for TriGSerializer {
fn default() -> Self {
Self::new()
}
}
impl TriGSerializer {
pub fn new() -> Self {
let mut prefixes = HashMap::new();
prefixes.insert(
"rdf".to_string(),
"http://www.w3.org/1999/02/22-rdf-syntax-ns#".to_string(),
);
prefixes.insert(
"rdfs".to_string(),
"http://www.w3.org/2000/01/rdf-schema#".to_string(),
);
prefixes.insert(
"xsd".to_string(),
"http://www.w3.org/2001/XMLSchema#".to_string(),
);
Self {
base_iri: None,
prefixes,
}
}
fn serialize_quad<W: Write>(&self, quad: &Quad, writer: &mut W) -> TurtleResult<()> {
self.serialize_subject(quad.subject(), writer)?;
write!(writer, " ").map_err(TurtleParseError::Io)?;
self.serialize_predicate(quad.predicate(), writer)?;
write!(writer, " ").map_err(TurtleParseError::Io)?;
self.serialize_object(quad.object(), writer)?;
Ok(())
}
fn serialize_subject<W: Write>(
&self,
subject: &oxirs_core::model::Subject,
writer: &mut W,
) -> TurtleResult<()> {
use oxirs_core::model::Subject;
match subject {
Subject::NamedNode(node) => {
self.serialize_named_node(node, writer)?;
}
Subject::BlankNode(node) => {
write!(writer, "_:{}", node.as_str()).map_err(TurtleParseError::Io)?;
}
Subject::QuotedTriple(triple) => {
write!(writer, "<< ").map_err(TurtleParseError::Io)?;
self.serialize_subject(triple.subject(), writer)?;
write!(writer, " ").map_err(TurtleParseError::Io)?;
self.serialize_predicate(triple.predicate(), writer)?;
write!(writer, " ").map_err(TurtleParseError::Io)?;
self.serialize_object(triple.object(), writer)?;
write!(writer, " >>").map_err(TurtleParseError::Io)?;
}
Subject::Variable(var) => {
write!(writer, "?{}", var.name()).map_err(TurtleParseError::Io)?;
}
}
Ok(())
}
fn serialize_predicate<W: Write>(
&self,
predicate: &oxirs_core::model::Predicate,
writer: &mut W,
) -> TurtleResult<()> {
use oxirs_core::model::Predicate;
match predicate {
Predicate::NamedNode(node) => {
if node.as_str() == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" {
write!(writer, "a").map_err(TurtleParseError::Io)?;
} else {
self.serialize_named_node(node, writer)?;
}
}
Predicate::Variable(var) => {
write!(writer, "?{}", var.name()).map_err(TurtleParseError::Io)?;
}
}
Ok(())
}
fn serialize_object<W: Write>(
&self,
object: &oxirs_core::model::Object,
writer: &mut W,
) -> TurtleResult<()> {
use oxirs_core::model::Object;
match object {
Object::NamedNode(node) => {
self.serialize_named_node(node, writer)?;
}
Object::BlankNode(node) => {
write!(writer, "_:{}", node.as_str()).map_err(TurtleParseError::Io)?;
}
Object::Literal(literal) => {
self.serialize_literal(literal, writer)?;
}
Object::QuotedTriple(triple) => {
write!(writer, "<< ").map_err(TurtleParseError::Io)?;
self.serialize_subject(triple.subject(), writer)?;
write!(writer, " ").map_err(TurtleParseError::Io)?;
self.serialize_predicate(triple.predicate(), writer)?;
write!(writer, " ").map_err(TurtleParseError::Io)?;
self.serialize_object(triple.object(), writer)?;
write!(writer, " >>").map_err(TurtleParseError::Io)?;
}
Object::Variable(var) => {
write!(writer, "?{}", var.name()).map_err(TurtleParseError::Io)?;
}
}
Ok(())
}
fn serialize_named_node<W: Write>(&self, node: &NamedNode, writer: &mut W) -> TurtleResult<()> {
let iri = node.as_str();
for (prefix, namespace) in &self.prefixes {
if iri.starts_with(namespace) {
let local = &iri[namespace.len()..];
write!(writer, "{prefix}:{local}").map_err(TurtleParseError::Io)?;
return Ok(());
}
}
write!(writer, "<{iri}>").map_err(TurtleParseError::Io)?;
Ok(())
}
fn serialize_literal<W: Write>(&self, literal: &Literal, writer: &mut W) -> TurtleResult<()> {
let value = literal.value();
let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
write!(writer, "\"{escaped}\"").map_err(TurtleParseError::Io)?;
if let Some(lang) = literal.language() {
write!(writer, "@{lang}").map_err(TurtleParseError::Io)?;
}
else if literal.datatype().as_str() != "http://www.w3.org/2001/XMLSchema#string" {
write!(writer, "^^").map_err(TurtleParseError::Io)?;
self.serialize_named_node(&literal.datatype().into_owned(), writer)?;
}
Ok(())
}
fn group_quads_by_graph<'a>(
&self,
quads: &'a [Quad],
) -> std::collections::BTreeMap<GraphName, Vec<&'a Quad>> {
let mut grouped = std::collections::BTreeMap::new();
for quad in quads {
grouped
.entry(quad.graph_name().clone())
.or_insert_with(Vec::new)
.push(quad);
}
grouped
}
}
impl Serializer<Quad> for TriGSerializer {
fn serialize<W: Write>(&self, quads: &[Quad], mut writer: W) -> TurtleResult<()> {
for (prefix, namespace) in &self.prefixes {
writeln!(writer, "@prefix {prefix}: <{namespace}> .").map_err(TurtleParseError::Io)?;
}
if !self.prefixes.is_empty() {
writeln!(writer).map_err(TurtleParseError::Io)?;
}
let grouped = self.group_quads_by_graph(quads);
for (graph_name, graph_quads) in grouped {
match graph_name {
GraphName::DefaultGraph => {
for quad in graph_quads {
self.serialize_quad(quad, &mut writer)?;
writeln!(writer, " .").map_err(TurtleParseError::Io)?;
}
}
GraphName::NamedNode(node) => {
self.serialize_named_node(&node, &mut writer)?;
writeln!(writer, " {{").map_err(TurtleParseError::Io)?;
for quad in graph_quads {
write!(writer, " ").map_err(TurtleParseError::Io)?;
self.serialize_quad(quad, &mut writer)?;
writeln!(writer, " .").map_err(TurtleParseError::Io)?;
}
writeln!(writer, "}}").map_err(TurtleParseError::Io)?;
}
GraphName::BlankNode(node) => {
writeln!(writer, "_:{} {{", node.as_str()).map_err(TurtleParseError::Io)?;
for quad in graph_quads {
write!(writer, " ").map_err(TurtleParseError::Io)?;
self.serialize_quad(quad, &mut writer)?;
writeln!(writer, " .").map_err(TurtleParseError::Io)?;
}
writeln!(writer, "}}").map_err(TurtleParseError::Io)?;
}
GraphName::Variable(_) => {
return Err(TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: "Cannot serialize variable graph names".to_string(),
position: TextPosition::start(),
}));
}
}
writeln!(writer).map_err(TurtleParseError::Io)?;
}
Ok(())
}
fn serialize_item<W: Write>(&self, quad: &Quad, mut writer: W) -> TurtleResult<()> {
match quad.graph_name() {
GraphName::DefaultGraph => {
self.serialize_quad(quad, &mut writer)?;
writeln!(writer, " .").map_err(TurtleParseError::Io)?;
}
GraphName::NamedNode(node) => {
write!(writer, "GRAPH ").map_err(TurtleParseError::Io)?;
self.serialize_named_node(node, &mut writer)?;
write!(writer, " {{ ").map_err(TurtleParseError::Io)?;
self.serialize_quad(quad, &mut writer)?;
writeln!(writer, " . }}").map_err(TurtleParseError::Io)?;
}
GraphName::BlankNode(node) => {
write!(writer, "_:{} {{ ", node.as_str()).map_err(TurtleParseError::Io)?;
self.serialize_quad(quad, &mut writer)?;
writeln!(writer, " . }}").map_err(TurtleParseError::Io)?;
}
GraphName::Variable(_) => {
return Err(TurtleParseError::Syntax(TurtleSyntaxError::Generic {
message: "Cannot serialize variable graph names".to_string(),
position: TextPosition::start(),
}));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxirs_core::model::Object;
use std::io::Cursor;
#[test]
fn test_parse_default_graph_triples() {
let parser = TriGParser::new();
let input = r#"
@prefix ex: <http://example.org/> .
ex:alice ex:name "Alice" .
ex:bob ex:name "Bob" .
"#;
let quads = parser
.parse(Cursor::new(input))
.expect("trig parsing should succeed");
assert_eq!(quads.len(), 2);
for quad in &quads {
assert!(
quad.graph_name().is_default_graph(),
"triples outside graph block should be in default graph"
);
}
}
#[test]
fn test_parse_named_graph_block() {
let parser = TriGParser::new();
let input = r#"
@prefix ex: <http://example.org/> .
ex:graph1 {
ex:alice ex:knows ex:bob .
}
"#;
let quads = parser
.parse(Cursor::new(input))
.expect("trig parsing should succeed");
assert!(!quads.is_empty(), "named graph should produce quads");
let in_named_graph = quads
.iter()
.filter(|q| !q.graph_name().is_default_graph())
.count();
assert!(in_named_graph > 0, "some quads should be in named graph");
}
#[test]
fn test_parse_multiple_named_graphs() {
let parser = TriGParser::new();
let input = r#"
@prefix ex: <http://example.org/> .
ex:graph1 {
ex:alice ex:name "Alice" .
}
ex:graph2 {
ex:bob ex:name "Bob" .
}
"#;
let quads = parser
.parse(Cursor::new(input))
.expect("trig parsing should succeed");
assert!(
quads.len() >= 2,
"multiple graphs should produce multiple quads"
);
}
#[test]
fn test_parse_graph_keyword_syntax() {
let parser = TriGParser::new();
let input = r#"
@prefix ex: <http://example.org/> .
GRAPH ex:graph1 {
ex:s ex:p ex:o .
}
"#;
let quads = parser
.parse(Cursor::new(input))
.expect("trig GRAPH keyword syntax should succeed");
assert!(
!quads.is_empty(),
"GRAPH keyword syntax should produce quads"
);
}
#[test]
fn test_parse_mixed_default_and_named_graphs() {
let parser = TriGParser::new();
let input = r#"
@prefix ex: <http://example.org/> .
ex:alice ex:type ex:Person .
ex:relationships {
ex:alice ex:knows ex:bob .
}
ex:charlie ex:type ex:Person .
"#;
let quads = parser
.parse(Cursor::new(input))
.expect("trig parsing should succeed");
let default_count = quads
.iter()
.filter(|q| q.graph_name().is_default_graph())
.count();
assert!(default_count >= 2, "should have default graph triples");
}
#[test]
fn test_parse_prefix_declarations() {
let parser = TriGParser::new();
let input = r#"
@prefix ex: <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
ex:graph1 {
ex:alice foaf:name "Alice" .
}
"#;
let quads = parser
.parse(Cursor::new(input))
.expect("trig parsing with prefixes should succeed");
assert!(!quads.is_empty());
}
#[test]
fn test_parse_empty_document() {
let parser = TriGParser::new();
let input = "# only comments\n";
let quads = parser
.parse(Cursor::new(input))
.expect("trig parsing empty document should succeed");
assert!(quads.is_empty(), "empty document produces no quads");
}
#[test]
fn test_for_reader_iterator() {
let parser = TriGParser::new();
let input = r#"
@prefix ex: <http://example.org/> .
ex:s ex:p "o" .
"#;
let quads: Vec<_> = parser
.for_reader(Cursor::new(input))
.collect::<Result<Vec<_>, _>>()
.expect("for_reader should succeed");
assert_eq!(quads.len(), 1);
}
#[test]
fn test_serialize_quads_default_graph() {
let serializer = TriGSerializer::new();
let quad = Quad::new(
NamedNode::new("http://example.org/s").expect("valid IRI"),
NamedNode::new("http://example.org/p").expect("valid IRI"),
NamedNode::new("http://example.org/o").expect("valid IRI"),
GraphName::DefaultGraph,
);
let mut output = Vec::new();
serializer
.serialize(&[quad], &mut output)
.expect("trig serialization should succeed");
let out_str = String::from_utf8(output).expect("valid UTF-8");
assert!(out_str.contains("<http://example.org/s>"));
}
#[test]
fn test_serialize_quads_named_graph() {
let serializer = TriGSerializer::new();
let quad = Quad::new(
NamedNode::new("http://example.org/s").expect("valid IRI"),
NamedNode::new("http://example.org/p").expect("valid IRI"),
NamedNode::new("http://example.org/o").expect("valid IRI"),
GraphName::NamedNode(NamedNode::new("http://example.org/g").expect("valid IRI")),
);
let mut output = Vec::new();
serializer
.serialize(&[quad], &mut output)
.expect("trig serialization should succeed");
let out_str = String::from_utf8(output).expect("valid UTF-8");
assert!(out_str.contains("<http://example.org/g>"));
}
#[test]
fn test_parse_literal_object_in_graph() {
let parser = TriGParser::new();
let input = r#"
@prefix ex: <http://example.org/> .
ex:data {
ex:alice ex:age "30" .
}
"#;
let quads = parser
.parse(Cursor::new(input))
.expect("trig parsing should succeed");
assert!(!quads.is_empty());
let has_literal = quads
.iter()
.any(|q| matches!(q.object(), Object::Literal(_)));
assert!(has_literal, "should have quad with literal object");
}
#[test]
fn test_parser_default_is_non_lenient() {
let parser = TriGParser::new();
assert!(!parser.lenient, "default parser should not be lenient");
}
#[test]
fn test_parse_base_iri_declaration() {
let parser = TriGParser::new();
let input = r#"
@base <http://example.org/> .
@prefix ex: <http://example.org/> .
ex:g {
<alice> <knows> <bob> .
}
"#;
let quads = parser
.parse(Cursor::new(input))
.expect("trig parsing with base IRI should succeed");
assert!(!quads.is_empty());
}
#[test]
fn test_parse_multiple_triples_in_single_graph() {
let parser = TriGParser::new();
let input = r#"
@prefix ex: <http://example.org/> .
ex:graph1 {
ex:s1 ex:p ex:o1 .
ex:s2 ex:p ex:o2 .
ex:s3 ex:p ex:o3 .
}
"#;
let quads = parser
.parse(Cursor::new(input))
.expect("trig parsing should succeed");
assert!(quads.len() >= 3, "single graph with 3 triples");
}
}