//! Implementation of [SPARQL 1.1 Query Results CSV and TSV Formats](https://www.w3.org/TR/sparql11-results-csv-tsv/) use crate::model::{vocab::xsd, *}; use crate::sparql::error::EvaluationError; use crate::sparql::model::*; use std::io::{self, Write}; pub fn write_csv_results( results: QueryResults, mut sink: impl Write, ) -> Result<(), EvaluationError> { match results { QueryResults::Boolean(value) => { sink.write_all(if value { b"true" } else { b"false" })?; } QueryResults::Solutions(solutions) => { let mut start_vars = true; for variable in solutions.variables() { if start_vars { start_vars = false; } else { sink.write_all(b",")?; } sink.write_all(variable.as_str().as_bytes())?; } for solution in solutions { let solution = solution?; sink.write_all(b"\r\n")?; let mut start_binding = true; for value in solution.values() { if start_binding { start_binding = false; } else { sink.write_all(b",")?; } if let Some(value) = value { write_csv_term(value, &mut sink)?; } } } } QueryResults::Graph(g) => { sink.write_all(b"subject,predicate,object")?; for t in g { let t = t?; sink.write_all(b"\r\n")?; write_csv_term(&t.subject, &mut sink)?; sink.write_all(b",")?; write_csv_term(&t.predicate, &mut sink)?; sink.write_all(b",")?; write_csv_term(&t.object, &mut sink)?; } } } Ok(()) } fn write_csv_term<'a>(term: impl Into>, mut sink: impl Write) -> io::Result<()> { match term.into() { TermRef::NamedNode(uri) => sink.write_all(uri.as_str().as_bytes()), TermRef::BlankNode(bnode) => { sink.write_all(b"_:")?; sink.write_all(bnode.as_str().as_bytes()) } TermRef::Literal(literal) => write_escaped_csv_string(literal.value(), &mut sink), } } fn write_escaped_csv_string(s: &str, mut sink: impl Write) -> io::Result<()> { if s.bytes().any(|c| match c { b'"' | b',' | b'\n' | b'\r' => true, _ => false, }) { sink.write_all(b"\"")?; for c in s.bytes() { if c == b'\"' { sink.write_all(b"\"\"") } else { sink.write_all(&[c]) }?; } sink.write_all(b"\"") } else { sink.write_all(s.as_bytes()) } } pub fn write_tsv_results( results: QueryResults, mut sink: impl Write, ) -> Result<(), EvaluationError> { match results { QueryResults::Boolean(value) => { sink.write_all(if value { b"true" } else { b"false" })?; } QueryResults::Solutions(solutions) => { let mut start_vars = true; for variable in solutions.variables() { if start_vars { start_vars = false; } else { sink.write_all(b"\t")?; } sink.write_all(b"?")?; sink.write_all(variable.as_str().as_bytes())?; } for solution in solutions { let solution = solution?; sink.write_all(b"\n")?; let mut start_binding = true; for value in solution.values() { if start_binding { start_binding = false; } else { sink.write_all(b"\t")?; } if let Some(value) = value { write_tsv_term(value, &mut sink)?; } } } } QueryResults::Graph(g) => { sink.write_all(b"subject\tpredicate\tobject")?; for t in g { let t = t?; sink.write_all(b"\n")?; write_tsv_term(&t.subject, &mut sink)?; sink.write_all(b"\t")?; write_tsv_term(&t.predicate, &mut sink)?; sink.write_all(b"\t")?; write_tsv_term(&t.object, &mut sink)?; } } } Ok(()) } fn write_tsv_term<'a>(term: impl Into>, mut sink: impl Write) -> io::Result<()> { //TODO: full Turtle serialization match term.into() { TermRef::NamedNode(node) => write!(sink, "<{}>", node.as_str()), TermRef::BlankNode(node) => write!(sink, "_:{}", node.as_str()), TermRef::Literal(literal) => match literal.datatype() { xsd::BOOLEAN => match literal.value() { "true" | "1" => sink.write_all(b"true"), "false" | "0" => sink.write_all(b"false"), _ => sink.write_all(literal.to_string().as_bytes()), }, xsd::INTEGER => { if literal.value().bytes().all(|c| match c { b'0'..=b'9' => true, _ => false, }) { sink.write_all(literal.value().as_bytes()) } else { sink.write_all(literal.to_string().as_bytes()) } } _ => sink.write_all(literal.to_string().as_bytes()), }, } } #[cfg(test)] mod tests { use super::*; use std::rc::Rc; use std::str; fn build_example() -> QueryResults { QuerySolutionIter::new( Rc::new(vec![ Variable::new_unchecked("x"), Variable::new_unchecked("literal"), ]), Box::new( vec![ Ok(vec![ Some(NamedNode::new_unchecked("http://example/x").into()), Some(Literal::new_simple_literal("String").into()), ]), Ok(vec![ Some(NamedNode::new_unchecked("http://example/x").into()), Some(Literal::new_simple_literal("String-with-dquote\"").into()), ]), Ok(vec![ Some(BlankNode::new_unchecked("b0").into()), Some(Literal::new_simple_literal("Blank node").into()), ]), Ok(vec![ None, Some(Literal::new_simple_literal("Missing 'x'").into()), ]), Ok(vec![None, None]), Ok(vec![ Some(NamedNode::new_unchecked("http://example/x").into()), None, ]), Ok(vec![ Some(BlankNode::new_unchecked("b1").into()), Some( Literal::new_language_tagged_literal_unchecked( "String-with-lang", "en", ) .into(), ), ]), Ok(vec![ Some(BlankNode::new_unchecked("b1").into()), Some(Literal::new_typed_literal("123", xsd::INTEGER).into()), ]), ] .into_iter(), ), ) .into() } #[test] fn test_csv_serialization() { let mut sink = Vec::new(); write_csv_results(build_example(), &mut sink).unwrap(); assert_eq!(str::from_utf8(&sink).unwrap(), "x,literal\r\nhttp://example/x,String\r\nhttp://example/x,\"String-with-dquote\"\"\"\r\n_:b0,Blank node\r\n,Missing 'x'\r\n,\r\nhttp://example/x,\r\n_:b1,String-with-lang\r\n_:b1,123"); } #[test] fn test_tsv_serialization() { let mut sink = Vec::new(); write_tsv_results(build_example(), &mut sink).unwrap(); assert_eq!(str::from_utf8(&sink).unwrap(), "?x\t?literal\n\t\"String\"\n\t\"String-with-dquote\\\"\"\n_:b0\t\"Blank node\"\n\t\"Missing 'x'\"\n\t\n\t\n_:b1\t\"String-with-lang\"@en\n_:b1\t123"); } }