parent
4d18053ec9
commit
850b8eddcf
@ -0,0 +1,25 @@ |
|||||||
|
[package] |
||||||
|
name = "sparesults" |
||||||
|
version = "0.1.0" |
||||||
|
authors = ["Tpt <thomas@pellissier-tanon.fr>"] |
||||||
|
license = "MIT OR Apache-2.0" |
||||||
|
readme = "README.md" |
||||||
|
keywords = ["SPARQL"] |
||||||
|
repository = "https://github.com/oxigraph/oxigraph/tree/master/lib/sparesults" |
||||||
|
homepage = "https://oxigraph.org/" |
||||||
|
description = """ |
||||||
|
SPARQL query results formats parsers and serializers |
||||||
|
""" |
||||||
|
edition = "2021" |
||||||
|
|
||||||
|
[features] |
||||||
|
default = [] |
||||||
|
rdf-star = ["oxrdf/rdf-star"] |
||||||
|
|
||||||
|
[dependencies] |
||||||
|
json-event-parser = "0.1" |
||||||
|
oxrdf = { version = "0.1", path="../oxrdf" } |
||||||
|
quick-xml = "0.22" |
||||||
|
|
||||||
|
[package.metadata.docs.rs] |
||||||
|
all-features = true |
@ -0,0 +1,71 @@ |
|||||||
|
Sparesults |
||||||
|
========== |
||||||
|
|
||||||
|
[![Latest Version](https://img.shields.io/crates/v/sparesults.svg)](https://crates.io/crates/sparesults) |
||||||
|
[![Released API docs](https://docs.rs/sparesults/badge.svg)](https://docs.rs/sparesults) |
||||||
|
[![Crates.io downloads](https://img.shields.io/crates/d/sparesults)](https://crates.io/crates/sparesults) |
||||||
|
[![actions status](https://github.com/oxigraph/oxigraph/workflows/build/badge.svg)](https://github.com/oxigraph/oxigraph/actions) |
||||||
|
[![Gitter](https://badges.gitter.im/oxigraph/community.svg)](https://gitter.im/oxigraph/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) |
||||||
|
|
||||||
|
Sparesults is a set of parsers and serializers for [SPARQL](https://www.w3.org/TR/sparql11-overview/) query results formats. |
||||||
|
|
||||||
|
It supports [SPARQL Query Results XML Format (Second Edition)](http://www.w3.org/TR/rdf-sparql-XMLres/), [SPARQL 1.1 Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/) and [SPARQL 1.1 Query Results CSV and TSV Formats](https://www.w3.org/TR/2013/REC-sparql11-results-csv-tsv-20130321/). |
||||||
|
|
||||||
|
Support for [SPARQL-star](https://w3c.github.io/rdf-star/cg-spec/#sparql-star) is also available behind the `rdf-star` feature. |
||||||
|
|
||||||
|
This crate is intended to be a building piece for SPARQL client and server implementations in Rust like [Oxigraph](https://oxigraph.org). |
||||||
|
|
||||||
|
Usage example converting a JSON result file into a TSV result file: |
||||||
|
|
||||||
|
```rust |
||||||
|
use sparesults::{QueryResultsFormat, QueryResultsParser, QueryResultsReader, QueryResultsSerializer}; |
||||||
|
use std::io::Result; |
||||||
|
|
||||||
|
fn convert_json_to_tsv(json_file: &[u8]) -> Result<Vec<u8>> { |
||||||
|
let json_parser = QueryResultsParser::from_format(QueryResultsFormat::Json); |
||||||
|
let tsv_serializer = QueryResultsSerializer::from_format(QueryResultsFormat::Tsv); |
||||||
|
// We start to read the JSON file and see which kind of results it is |
||||||
|
match json_parser.read_results(json_file)? { |
||||||
|
QueryResultsReader::Boolean(value) => { |
||||||
|
// it's a boolean result, we copy it in TSV to the output buffer |
||||||
|
tsv_serializer.write_boolean_result(Vec::new(), value) |
||||||
|
}, |
||||||
|
QueryResultsReader::Solutions(solutions_reader) => { |
||||||
|
// it's a set of solutions, we create a writer and we write to it while reading in streaming from the JSON file |
||||||
|
let mut solutions_writer = tsv_serializer.solutions_writer(Vec::new(), solutions_reader.variables().to_vec())?; |
||||||
|
for solution in solutions_reader { |
||||||
|
solutions_writer.write(&solution?)?; |
||||||
|
} |
||||||
|
solutions_writer.finish() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Let's test with a boolean |
||||||
|
assert_eq!( |
||||||
|
convert_json_to_tsv(b"{\"boolean\":true}".as_slice()).unwrap(), |
||||||
|
b"true" |
||||||
|
); |
||||||
|
|
||||||
|
// And with a set of solutions |
||||||
|
assert_eq!( |
||||||
|
convert_json_to_tsv(b"{\"head\":{\"vars\":[\"foo\",\"bar\"]},\"results\":{\"bindings\":[{\"foo\":{\"type\":\"literal\",\"value\":\"test\"}}]}}".as_slice()).unwrap(), |
||||||
|
b"?foo\t?bar\n\"test\"\t" |
||||||
|
); |
||||||
|
``` |
||||||
|
|
||||||
|
## License |
||||||
|
|
||||||
|
This project is licensed under either of |
||||||
|
|
||||||
|
* Apache License, Version 2.0, ([LICENSE-APACHE](../LICENSE-APACHE) or |
||||||
|
`<http://www.apache.org/licenses/LICENSE-2.0>`) |
||||||
|
* MIT license ([LICENSE-MIT](../LICENSE-MIT) or |
||||||
|
`<http://opensource.org/licenses/MIT>`) |
||||||
|
|
||||||
|
at your option. |
||||||
|
|
||||||
|
|
||||||
|
### Contribution |
||||||
|
|
||||||
|
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Futures by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. |
@ -0,0 +1,130 @@ |
|||||||
|
use oxrdf::TermParseError; |
||||||
|
use std::error::Error; |
||||||
|
use std::{fmt, io}; |
||||||
|
|
||||||
|
/// Error returned during SPARQL result formats format parsing.
|
||||||
|
#[derive(Debug)] |
||||||
|
pub enum ParseError { |
||||||
|
/// I/O error during parsing (file not found...).
|
||||||
|
Io(io::Error), |
||||||
|
/// An error in the file syntax.
|
||||||
|
Syntax(SyntaxError), |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for ParseError { |
||||||
|
#[inline] |
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
||||||
|
match self { |
||||||
|
Self::Io(e) => e.fmt(f), |
||||||
|
Self::Syntax(e) => e.fmt(f), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl Error for ParseError { |
||||||
|
#[inline] |
||||||
|
fn source(&self) -> Option<&(dyn Error + 'static)> { |
||||||
|
match self { |
||||||
|
Self::Io(e) => Some(e), |
||||||
|
Self::Syntax(e) => Some(e), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<io::Error> for ParseError { |
||||||
|
#[inline] |
||||||
|
fn from(error: io::Error) -> Self { |
||||||
|
Self::Io(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<SyntaxError> for ParseError { |
||||||
|
#[inline] |
||||||
|
fn from(error: SyntaxError) -> Self { |
||||||
|
Self::Syntax(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<ParseError> for io::Error { |
||||||
|
#[inline] |
||||||
|
fn from(error: ParseError) -> Self { |
||||||
|
match error { |
||||||
|
ParseError::Io(error) => error, |
||||||
|
ParseError::Syntax(error) => error.into(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<quick_xml::Error> for ParseError { |
||||||
|
#[inline] |
||||||
|
fn from(error: quick_xml::Error) -> Self { |
||||||
|
match error { |
||||||
|
quick_xml::Error::Io(error) => Self::Io(error), |
||||||
|
error => Self::Syntax(SyntaxError { |
||||||
|
inner: SyntaxErrorKind::Xml(error), |
||||||
|
}), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// An error in the syntax of the parsed file.
|
||||||
|
#[derive(Debug)] |
||||||
|
pub struct SyntaxError { |
||||||
|
pub(crate) inner: SyntaxErrorKind, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
pub(crate) enum SyntaxErrorKind { |
||||||
|
Xml(quick_xml::Error), |
||||||
|
Term(TermParseError), |
||||||
|
Msg { msg: String }, |
||||||
|
} |
||||||
|
|
||||||
|
impl SyntaxError { |
||||||
|
/// Builds an error from a printable error message.
|
||||||
|
#[inline] |
||||||
|
pub(crate) fn msg(msg: impl Into<String>) -> Self { |
||||||
|
Self { |
||||||
|
inner: SyntaxErrorKind::Msg { msg: msg.into() }, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for SyntaxError { |
||||||
|
#[inline] |
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
||||||
|
match &self.inner { |
||||||
|
SyntaxErrorKind::Xml(e) => e.fmt(f), |
||||||
|
SyntaxErrorKind::Term(e) => e.fmt(f), |
||||||
|
SyntaxErrorKind::Msg { msg } => f.write_str(msg), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl Error for SyntaxError { |
||||||
|
#[inline] |
||||||
|
fn source(&self) -> Option<&(dyn Error + 'static)> { |
||||||
|
match &self.inner { |
||||||
|
SyntaxErrorKind::Xml(e) => Some(e), |
||||||
|
SyntaxErrorKind::Term(e) => Some(e), |
||||||
|
SyntaxErrorKind::Msg { .. } => None, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<SyntaxError> for io::Error { |
||||||
|
#[inline] |
||||||
|
fn from(error: SyntaxError) -> Self { |
||||||
|
match error.inner { |
||||||
|
SyntaxErrorKind::Xml(error) => match error { |
||||||
|
quick_xml::Error::Io(error) => error, |
||||||
|
quick_xml::Error::UnexpectedEof(error) => { |
||||||
|
Self::new(io::ErrorKind::UnexpectedEof, error) |
||||||
|
} |
||||||
|
error => Self::new(io::ErrorKind::InvalidData, error), |
||||||
|
}, |
||||||
|
SyntaxErrorKind::Term(error) => Self::new(io::ErrorKind::InvalidData, error), |
||||||
|
SyntaxErrorKind::Msg { msg } => Self::new(io::ErrorKind::InvalidData, msg), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,506 @@ |
|||||||
|
#![doc = include_str!("../README.md")] |
||||||
|
#![deny(
|
||||||
|
future_incompatible, |
||||||
|
nonstandard_style, |
||||||
|
rust_2018_idioms, |
||||||
|
missing_copy_implementations, |
||||||
|
trivial_casts, |
||||||
|
trivial_numeric_casts, |
||||||
|
unsafe_code, |
||||||
|
unused_qualifications |
||||||
|
)] |
||||||
|
#![doc(test(attr(deny(warnings))))] |
||||||
|
|
||||||
|
mod csv; |
||||||
|
mod error; |
||||||
|
mod json; |
||||||
|
pub mod solution; |
||||||
|
mod xml; |
||||||
|
|
||||||
|
use crate::csv::*; |
||||||
|
pub use crate::error::{ParseError, SyntaxError}; |
||||||
|
use crate::json::*; |
||||||
|
pub use crate::solution::QuerySolution; |
||||||
|
use crate::xml::*; |
||||||
|
use oxrdf::Term; |
||||||
|
pub use oxrdf::Variable; |
||||||
|
use std::io::{self, BufRead, Write}; |
||||||
|
use std::rc::Rc; |
||||||
|
|
||||||
|
/// [SPARQL query](https://www.w3.org/TR/sparql11-query/) results serialization formats.
|
||||||
|
#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)] |
||||||
|
#[non_exhaustive] |
||||||
|
pub enum QueryResultsFormat { |
||||||
|
/// [SPARQL Query Results XML Format](http://www.w3.org/TR/rdf-sparql-XMLres/)
|
||||||
|
Xml, |
||||||
|
/// [SPARQL Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/)
|
||||||
|
Json, |
||||||
|
/// [SPARQL Query Results CSV Format](https://www.w3.org/TR/sparql11-results-csv-tsv/)
|
||||||
|
Csv, |
||||||
|
/// [SPARQL Query Results TSV Format](https://www.w3.org/TR/sparql11-results-csv-tsv/)
|
||||||
|
Tsv, |
||||||
|
} |
||||||
|
|
||||||
|
impl QueryResultsFormat { |
||||||
|
/// The format canonical IRI according to the [Unique URIs for file formats registry](https://www.w3.org/ns/formats/).
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::QueryResultsFormat;
|
||||||
|
///
|
||||||
|
/// assert_eq!(QueryResultsFormat::Json.iri(), "http://www.w3.org/ns/formats/SPARQL_Results_JSON")
|
||||||
|
/// ```
|
||||||
|
#[inline] |
||||||
|
pub fn iri(self) -> &'static str { |
||||||
|
match self { |
||||||
|
QueryResultsFormat::Xml => "http://www.w3.org/ns/formats/SPARQL_Results_XML", |
||||||
|
QueryResultsFormat::Json => "http://www.w3.org/ns/formats/SPARQL_Results_JSON", |
||||||
|
QueryResultsFormat::Csv => "http://www.w3.org/ns/formats/SPARQL_Results_CSV", |
||||||
|
QueryResultsFormat::Tsv => "http://www.w3.org/ns/formats/SPARQL_Results_TSV", |
||||||
|
} |
||||||
|
} |
||||||
|
/// The format [IANA media type](https://tools.ietf.org/html/rfc2046).
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::QueryResultsFormat;
|
||||||
|
///
|
||||||
|
/// assert_eq!(QueryResultsFormat::Json.media_type(), "application/sparql-results+json")
|
||||||
|
/// ```
|
||||||
|
#[inline] |
||||||
|
pub fn media_type(self) -> &'static str { |
||||||
|
match self { |
||||||
|
QueryResultsFormat::Xml => "application/sparql-results+xml", |
||||||
|
QueryResultsFormat::Json => "application/sparql-results+json", |
||||||
|
QueryResultsFormat::Csv => "text/csv; charset=utf-8", |
||||||
|
QueryResultsFormat::Tsv => "text/tab-separated-values; charset=utf-8", |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// The format [IANA-registered](https://tools.ietf.org/html/rfc2046) file extension.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::QueryResultsFormat;
|
||||||
|
///
|
||||||
|
/// assert_eq!(QueryResultsFormat::Json.file_extension(), "srj")
|
||||||
|
/// ```
|
||||||
|
#[inline] |
||||||
|
pub fn file_extension(self) -> &'static str { |
||||||
|
match self { |
||||||
|
QueryResultsFormat::Xml => "srx", |
||||||
|
QueryResultsFormat::Json => "srj", |
||||||
|
QueryResultsFormat::Csv => "csv", |
||||||
|
QueryResultsFormat::Tsv => "tsv", |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Looks for a known format from a media type.
|
||||||
|
///
|
||||||
|
/// It supports some media type aliases.
|
||||||
|
/// For example "application/xml" is going to return `Xml` even if it is not its canonical media type.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::QueryResultsFormat;
|
||||||
|
///
|
||||||
|
/// assert_eq!(QueryResultsFormat::from_media_type("application/sparql-results+json; charset=utf-8"), Some(QueryResultsFormat::Json))
|
||||||
|
/// ```
|
||||||
|
#[inline] |
||||||
|
pub fn from_media_type(media_type: &str) -> Option<Self> { |
||||||
|
match media_type.split(';').next()?.trim() { |
||||||
|
"application/sparql-results+xml" | "application/xml" | "text/xml" => Some(Self::Xml), |
||||||
|
"application/sparql-results+json" | "application/json" | "text/json" => { |
||||||
|
Some(Self::Json) |
||||||
|
} |
||||||
|
"text/csv" => Some(Self::Csv), |
||||||
|
"text/tab-separated-values" | "text/tsv" => Some(Self::Tsv), |
||||||
|
_ => None, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Looks for a known format from an extension.
|
||||||
|
///
|
||||||
|
/// It supports some aliases.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::QueryResultsFormat;
|
||||||
|
///
|
||||||
|
/// assert_eq!(QueryResultsFormat::from_extension("json"), Some(QueryResultsFormat::Json))
|
||||||
|
/// ```
|
||||||
|
#[inline] |
||||||
|
pub fn from_extension(extension: &str) -> Option<Self> { |
||||||
|
match extension { |
||||||
|
"srx" | "xml" => Some(Self::Xml), |
||||||
|
"srj" | "json" => Some(Self::Json), |
||||||
|
"csv" | "txt" => Some(Self::Csv), |
||||||
|
"tsv" => Some(Self::Tsv), |
||||||
|
_ => None, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Parsers for [SPARQL query](https://www.w3.org/TR/sparql11-query/) results serialization formats.
|
||||||
|
///
|
||||||
|
/// It currently supports the following formats:
|
||||||
|
/// * [SPARQL Query Results XML Format](http://www.w3.org/TR/rdf-sparql-XMLres/) ([`QueryResultsFormat::Xml`](QueryResultsFormat::Xml)).
|
||||||
|
/// * [SPARQL Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/) ([`QueryResultsFormat::Json`](QueryResultsFormat::Json)).
|
||||||
|
/// * [SPARQL Query Results TSV Format](https://www.w3.org/TR/sparql11-results-csv-tsv/) ([`QueryResultsFormat::Tsv`](QueryResultsFormat::Tsv)).
|
||||||
|
///
|
||||||
|
/// Example in JSON (the API is the same for XML and TSV):
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::{QueryResultsFormat, QueryResultsParser, QueryResultsReader};
|
||||||
|
/// use oxrdf::{Literal, Variable};
|
||||||
|
///
|
||||||
|
/// let json_parser = QueryResultsParser::from_format(QueryResultsFormat::Json);
|
||||||
|
/// // boolean
|
||||||
|
/// if let QueryResultsReader::Boolean(v) = json_parser.read_results(b"{\"boolean\":true}".as_slice())? {
|
||||||
|
/// assert_eq!(v, true);
|
||||||
|
/// }
|
||||||
|
/// // solutions
|
||||||
|
/// if let QueryResultsReader::Solutions(solutions) = json_parser.read_results(b"{\"head\":{\"vars\":[\"foo\",\"bar\"]},\"results\":{\"bindings\":[{\"foo\":{\"type\":\"literal\",\"value\":\"test\"}}]}}".as_slice())? {
|
||||||
|
/// assert_eq!(solutions.variables(), &[Variable::new_unchecked("foo"), Variable::new_unchecked("bar")]);
|
||||||
|
/// for solution in solutions {
|
||||||
|
/// assert_eq!(solution?.iter().collect::<Vec<_>>(), vec![(&Variable::new_unchecked("foo"), &Literal::from("test").into())]);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// # Result::<(),sparesults::ParseError>::Ok(())
|
||||||
|
/// ```
|
||||||
|
#[allow(missing_copy_implementations)] |
||||||
|
pub struct QueryResultsParser { |
||||||
|
format: QueryResultsFormat, |
||||||
|
} |
||||||
|
|
||||||
|
impl QueryResultsParser { |
||||||
|
/// Builds a parser for the given format.
|
||||||
|
#[inline] |
||||||
|
pub fn from_format(format: QueryResultsFormat) -> Self { |
||||||
|
Self { format } |
||||||
|
} |
||||||
|
|
||||||
|
/// Reads a result file.
|
||||||
|
///
|
||||||
|
/// Example in XML (the API is the same for JSON and TSV):
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::{QueryResultsFormat, QueryResultsParser, QueryResultsReader};
|
||||||
|
/// use oxrdf::{Literal, Variable};
|
||||||
|
///
|
||||||
|
/// let json_parser = QueryResultsParser::from_format(QueryResultsFormat::Xml);
|
||||||
|
///
|
||||||
|
/// // boolean
|
||||||
|
/// if let QueryResultsReader::Boolean(v) = json_parser.read_results(b"<sparql xmlns=\"http://www.w3.org/2005/sparql-results#\"><head/><boolean>true</boolean></sparql>".as_slice())? {
|
||||||
|
/// assert_eq!(v, true);
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // solutions
|
||||||
|
/// if let QueryResultsReader::Solutions(solutions) = json_parser.read_results(b"<sparql xmlns=\"http://www.w3.org/2005/sparql-results#\"><head><variable name=\"foo\"/><variable name=\"bar\"/></head><results><result><binding name=\"foo\"><literal>test</literal></binding></result></results></sparql>".as_slice())? {
|
||||||
|
/// assert_eq!(solutions.variables(), &[Variable::new_unchecked("foo"), Variable::new_unchecked("bar")]);
|
||||||
|
/// for solution in solutions {
|
||||||
|
/// assert_eq!(solution?.iter().collect::<Vec<_>>(), vec![(&Variable::new_unchecked("foo"), &Literal::from("test").into())]);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// # Result::<(),sparesults::ParseError>::Ok(())
|
||||||
|
/// ```
|
||||||
|
pub fn read_results<R: BufRead>(&self, reader: R) -> Result<QueryResultsReader<R>, ParseError> { |
||||||
|
Ok(match self.format { |
||||||
|
QueryResultsFormat::Xml => match XmlQueryResultsReader::read(reader)? { |
||||||
|
XmlQueryResultsReader::Boolean(r) => QueryResultsReader::Boolean(r), |
||||||
|
XmlQueryResultsReader::Solutions { |
||||||
|
solutions, |
||||||
|
variables, |
||||||
|
} => QueryResultsReader::Solutions(SolutionsReader { |
||||||
|
variables: Rc::new(variables), |
||||||
|
solutions: SolutionsReaderKind::Xml(solutions), |
||||||
|
}), |
||||||
|
}, |
||||||
|
QueryResultsFormat::Json => match JsonQueryResultsReader::read(reader)? { |
||||||
|
JsonQueryResultsReader::Boolean(r) => QueryResultsReader::Boolean(r), |
||||||
|
JsonQueryResultsReader::Solutions { |
||||||
|
solutions, |
||||||
|
variables, |
||||||
|
} => QueryResultsReader::Solutions(SolutionsReader { |
||||||
|
variables: Rc::new(variables), |
||||||
|
solutions: SolutionsReaderKind::Json(solutions), |
||||||
|
}), |
||||||
|
}, |
||||||
|
QueryResultsFormat::Csv => return Err(SyntaxError::msg("CSV SPARQL results syntax is lossy and can't be parsed to a proper RDF representation").into()), |
||||||
|
QueryResultsFormat::Tsv => match TsvQueryResultsReader::read(reader)? { |
||||||
|
TsvQueryResultsReader::Boolean(r) => QueryResultsReader::Boolean(r), |
||||||
|
TsvQueryResultsReader::Solutions { |
||||||
|
solutions, |
||||||
|
variables, |
||||||
|
} => QueryResultsReader::Solutions(SolutionsReader { |
||||||
|
variables: Rc::new(variables), |
||||||
|
solutions: SolutionsReaderKind::Tsv(solutions), |
||||||
|
}), |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// The reader for a given read of a results file.
|
||||||
|
///
|
||||||
|
/// It is either a read boolean ([`bool`]) or a streaming reader of a set of solutions ([`SolutionsReader`]).
|
||||||
|
///
|
||||||
|
/// Example in TSV (the API is the same for JSON and XML):
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::{QueryResultsFormat, QueryResultsParser, QueryResultsReader};
|
||||||
|
/// use oxrdf::{Literal, Variable};
|
||||||
|
///
|
||||||
|
/// let json_parser = QueryResultsParser::from_format(QueryResultsFormat::Tsv);
|
||||||
|
///
|
||||||
|
/// // boolean
|
||||||
|
/// if let QueryResultsReader::Boolean(v) = json_parser.read_results(b"true".as_slice())? {
|
||||||
|
/// assert_eq!(v, true);
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // solutions
|
||||||
|
/// if let QueryResultsReader::Solutions(solutions) = json_parser.read_results(b"?foo\t?bar\n\"test\"\t".as_slice())? {
|
||||||
|
/// assert_eq!(solutions.variables(), &[Variable::new_unchecked("foo"), Variable::new_unchecked("bar")]);
|
||||||
|
/// for solution in solutions {
|
||||||
|
/// assert_eq!(solution?.iter().collect::<Vec<_>>(), vec![(&Variable::new_unchecked("foo"), &Literal::from("test").into())]);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// # Result::<(),sparesults::ParseError>::Ok(())
|
||||||
|
/// ```
|
||||||
|
pub enum QueryResultsReader<R: BufRead> { |
||||||
|
Solutions(SolutionsReader<R>), |
||||||
|
Boolean(bool), |
||||||
|
} |
||||||
|
|
||||||
|
/// A streaming reader of a set of [`QuerySolution`] solutions.
|
||||||
|
///
|
||||||
|
/// It implements the [`Iterator`] API to iterate over the solutions.
|
||||||
|
///
|
||||||
|
/// Example in JSON (the API is the same for XML and TSV):
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::{QueryResultsFormat, QueryResultsParser, QueryResultsReader};
|
||||||
|
/// use oxrdf::{Literal, Variable};
|
||||||
|
///
|
||||||
|
/// let json_parser = QueryResultsParser::from_format(QueryResultsFormat::Json);
|
||||||
|
/// if let QueryResultsReader::Solutions(solutions) = json_parser.read_results(b"{\"head\":{\"vars\":[\"foo\",\"bar\"]},\"results\":{\"bindings\":[{\"foo\":{\"type\":\"literal\",\"value\":\"test\"}}]}}".as_slice())? {
|
||||||
|
/// assert_eq!(solutions.variables(), &[Variable::new_unchecked("foo"), Variable::new_unchecked("bar")]);
|
||||||
|
/// for solution in solutions {
|
||||||
|
/// assert_eq!(solution?.iter().collect::<Vec<_>>(), vec![(&Variable::new_unchecked("foo"), &Literal::from("test").into())]);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// # Result::<(),sparesults::ParseError>::Ok(())
|
||||||
|
/// ```
|
||||||
|
pub struct SolutionsReader<R: BufRead> { |
||||||
|
variables: Rc<Vec<Variable>>, |
||||||
|
solutions: SolutionsReaderKind<R>, |
||||||
|
} |
||||||
|
|
||||||
|
enum SolutionsReaderKind<R: BufRead> { |
||||||
|
Xml(XmlSolutionsReader<R>), |
||||||
|
Json(JsonSolutionsReader<R>), |
||||||
|
Tsv(TsvSolutionsReader<R>), |
||||||
|
} |
||||||
|
|
||||||
|
impl<R: BufRead> SolutionsReader<R> { |
||||||
|
/// Ordered list of the declared variables at the beginning of the results.
|
||||||
|
///
|
||||||
|
/// Example in TSV (the API is the same for JSON and XML):
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::{QueryResultsFormat, QueryResultsParser, QueryResultsReader};
|
||||||
|
/// use oxrdf::Variable;
|
||||||
|
///
|
||||||
|
/// let json_parser = QueryResultsParser::from_format(QueryResultsFormat::Tsv);
|
||||||
|
/// if let QueryResultsReader::Solutions(solutions) = json_parser.read_results(b"?foo\t?bar\n\"ex1\"\t\"ex2\"".as_slice())? {
|
||||||
|
/// assert_eq!(solutions.variables(), &[Variable::new_unchecked("foo"), Variable::new_unchecked("bar")]);
|
||||||
|
/// }
|
||||||
|
/// # Result::<(),sparesults::ParseError>::Ok(())
|
||||||
|
/// ```
|
||||||
|
#[inline] |
||||||
|
pub fn variables(&self) -> &[Variable] { |
||||||
|
&self.variables |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<R: BufRead> Iterator for SolutionsReader<R> { |
||||||
|
type Item = Result<QuerySolution, ParseError>; |
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Result<QuerySolution, ParseError>> { |
||||||
|
Some( |
||||||
|
match &mut self.solutions { |
||||||
|
SolutionsReaderKind::Xml(reader) => reader.read_next(), |
||||||
|
SolutionsReaderKind::Json(reader) => reader.read_next(), |
||||||
|
SolutionsReaderKind::Tsv(reader) => reader.read_next(), |
||||||
|
} |
||||||
|
.transpose()? |
||||||
|
.map(|values| (self.variables.clone(), values).into()), |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// A serializer for [SPARQL query](https://www.w3.org/TR/sparql11-query/) results serialization formats.
|
||||||
|
///
|
||||||
|
/// It currently supports the following formats:
|
||||||
|
/// * [SPARQL Query Results XML Format](http://www.w3.org/TR/rdf-sparql-XMLres/) ([`QueryResultsFormat::Xml`](QueryResultsFormat::Xml))
|
||||||
|
/// * [SPARQL Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/) ([`QueryResultsFormat::Json`](QueryResultsFormat::Json))
|
||||||
|
/// * [SPARQL Query Results CSV Format](https://www.w3.org/TR/sparql11-results-csv-tsv/) ([`QueryResultsFormat::Csv`](QueryResultsFormat::Csv))
|
||||||
|
/// * [SPARQL Query Results TSV Format](https://www.w3.org/TR/sparql11-results-csv-tsv/) ([`QueryResultsFormat::Tsv`](QueryResultsFormat::Tsv))
|
||||||
|
///
|
||||||
|
/// Example in JSON (the API is the same for XML and TSV):
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::{QueryResultsFormat, QueryResultsSerializer};
|
||||||
|
/// use oxrdf::{Literal, Variable};
|
||||||
|
/// use std::iter::once;
|
||||||
|
///
|
||||||
|
/// let json_serializer = QueryResultsSerializer::from_format(QueryResultsFormat::Json);
|
||||||
|
///
|
||||||
|
/// // boolean
|
||||||
|
/// let mut buffer = Vec::new();
|
||||||
|
/// json_serializer.write_boolean_result(&mut buffer, true)?;
|
||||||
|
/// assert_eq!(buffer, b"{\"head\":{},\"boolean\":true}");
|
||||||
|
///
|
||||||
|
/// // solutions
|
||||||
|
/// let mut buffer = Vec::new();
|
||||||
|
/// let mut writer = json_serializer.solutions_writer(&mut buffer, vec![Variable::new_unchecked("foo"), Variable::new_unchecked("bar")])?;
|
||||||
|
/// writer.write(once((&Variable::new_unchecked("foo"), &Literal::from("test").into())))?;
|
||||||
|
/// writer.finish()?;
|
||||||
|
/// assert_eq!(buffer, b"{\"head\":{\"vars\":[\"foo\",\"bar\"]},\"results\":{\"bindings\":[{\"foo\":{\"type\":\"literal\",\"value\":\"test\"}}]}}");
|
||||||
|
/// # std::io::Result::Ok(())
|
||||||
|
/// ```
|
||||||
|
#[allow(missing_copy_implementations)] |
||||||
|
pub struct QueryResultsSerializer { |
||||||
|
format: QueryResultsFormat, |
||||||
|
} |
||||||
|
|
||||||
|
impl QueryResultsSerializer { |
||||||
|
/// Builds a serializer for the given format.
|
||||||
|
#[inline] |
||||||
|
pub fn from_format(format: QueryResultsFormat) -> Self { |
||||||
|
Self { format } |
||||||
|
} |
||||||
|
|
||||||
|
/// Write a boolean query result (from an `ASK` query) into the given [`Write`](std::io::Write) implementation.
|
||||||
|
///
|
||||||
|
/// Example in XML (the API is the same for JSON and TSV):
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::{QueryResultsFormat, QueryResultsSerializer};
|
||||||
|
///
|
||||||
|
/// let json_serializer = QueryResultsSerializer::from_format(QueryResultsFormat::Xml);
|
||||||
|
/// let mut buffer = Vec::new();
|
||||||
|
/// json_serializer.write_boolean_result(&mut buffer, true)?;
|
||||||
|
/// assert_eq!(buffer, b"<?xml version=\"1.0\"?><sparql xmlns=\"http://www.w3.org/2005/sparql-results#\"><head></head><boolean>true</boolean></sparql>");
|
||||||
|
/// # std::io::Result::Ok(())
|
||||||
|
/// ```
|
||||||
|
pub fn write_boolean_result<W: Write>(&self, writer: W, value: bool) -> io::Result<W> { |
||||||
|
match self.format { |
||||||
|
QueryResultsFormat::Xml => write_boolean_xml_result(writer, value), |
||||||
|
QueryResultsFormat::Json => write_boolean_json_result(writer, value), |
||||||
|
QueryResultsFormat::Csv => write_boolean_csv_result(writer, value), |
||||||
|
QueryResultsFormat::Tsv => write_boolean_tsv_result(writer, value), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns a `SolutionsWriter` allowing writing query solutions into the given [`Write`](std::io::Write) implementation.
|
||||||
|
///
|
||||||
|
/// Example in XML (the API is the same for JSON and TSV):
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::{QueryResultsFormat, QueryResultsSerializer};
|
||||||
|
/// use oxrdf::{Literal, Variable};
|
||||||
|
/// use std::iter::once;
|
||||||
|
///
|
||||||
|
/// let json_serializer = QueryResultsSerializer::from_format(QueryResultsFormat::Xml);
|
||||||
|
/// let mut buffer = Vec::new();
|
||||||
|
/// let mut writer = json_serializer.solutions_writer(&mut buffer, vec![Variable::new_unchecked("foo"), Variable::new_unchecked("bar")])?;
|
||||||
|
/// writer.write(once((&Variable::new_unchecked("foo"), &Literal::from("test").into())))?;
|
||||||
|
/// writer.finish()?;
|
||||||
|
/// assert_eq!(buffer, b"<?xml version=\"1.0\"?><sparql xmlns=\"http://www.w3.org/2005/sparql-results#\"><head><variable name=\"foo\"/><variable name=\"bar\"/></head><results><result><binding name=\"foo\"><literal>test</literal></binding></result></results></sparql>");
|
||||||
|
/// # std::io::Result::Ok(())
|
||||||
|
/// ```
|
||||||
|
pub fn solutions_writer<W: Write>( |
||||||
|
&self, |
||||||
|
writer: W, |
||||||
|
variables: Vec<Variable>, |
||||||
|
) -> io::Result<SolutionsWriter<W>> { |
||||||
|
Ok(SolutionsWriter { |
||||||
|
formatter: match self.format { |
||||||
|
QueryResultsFormat::Xml => { |
||||||
|
SolutionsWriterKind::Xml(XmlSolutionsWriter::start(writer, variables)?) |
||||||
|
} |
||||||
|
QueryResultsFormat::Json => { |
||||||
|
SolutionsWriterKind::Json(JsonSolutionsWriter::start(writer, variables)?) |
||||||
|
} |
||||||
|
QueryResultsFormat::Csv => { |
||||||
|
SolutionsWriterKind::Csv(CsvSolutionsWriter::start(writer, variables)?) |
||||||
|
} |
||||||
|
QueryResultsFormat::Tsv => { |
||||||
|
SolutionsWriterKind::Tsv(TsvSolutionsWriter::start(writer, variables)?) |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Allows writing query results.
|
||||||
|
/// Could be built using a [`QueryResultsSerializer`].
|
||||||
|
///
|
||||||
|
/// Warning: Do not forget to run the [`finish`](SolutionsWriter::finish()) method to properly write the last bytes of the file.
|
||||||
|
///
|
||||||
|
/// Example in TSV (the API is the same for JSON and XML):
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::{QueryResultsFormat, QueryResultsSerializer};
|
||||||
|
/// use oxrdf::{Literal, Variable};
|
||||||
|
/// use std::iter::once;
|
||||||
|
///
|
||||||
|
/// let json_serializer = QueryResultsSerializer::from_format(QueryResultsFormat::Tsv);
|
||||||
|
/// let mut buffer = Vec::new();
|
||||||
|
/// let mut writer = json_serializer.solutions_writer(&mut buffer, vec![Variable::new_unchecked("foo"), Variable::new_unchecked("bar")])?;
|
||||||
|
/// writer.write(once((&Variable::new_unchecked("foo"), &Literal::from("test").into())))?;
|
||||||
|
/// writer.finish()?;
|
||||||
|
/// assert_eq!(buffer, b"?foo\t?bar\n\"test\"\t");
|
||||||
|
/// # std::io::Result::Ok(())
|
||||||
|
/// ```
|
||||||
|
#[must_use] |
||||||
|
pub struct SolutionsWriter<W: Write> { |
||||||
|
formatter: SolutionsWriterKind<W>, |
||||||
|
} |
||||||
|
|
||||||
|
enum SolutionsWriterKind<W: Write> { |
||||||
|
Xml(XmlSolutionsWriter<W>), |
||||||
|
Json(JsonSolutionsWriter<W>), |
||||||
|
Csv(CsvSolutionsWriter<W>), |
||||||
|
Tsv(TsvSolutionsWriter<W>), |
||||||
|
} |
||||||
|
|
||||||
|
impl<W: Write> SolutionsWriter<W> { |
||||||
|
/// Writes a solution.
|
||||||
|
///
|
||||||
|
/// Example in JSON (the API is the same for XML and TSV):
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::{QueryResultsFormat, QueryResultsSerializer, QuerySolution};
|
||||||
|
/// use oxrdf::{Literal, Variable};
|
||||||
|
/// use std::iter::once;
|
||||||
|
///
|
||||||
|
/// let json_serializer = QueryResultsSerializer::from_format(QueryResultsFormat::Json);
|
||||||
|
/// let mut buffer = Vec::new();
|
||||||
|
/// let mut writer = json_serializer.solutions_writer(&mut buffer, vec![Variable::new_unchecked("foo"), Variable::new_unchecked("bar")])?;
|
||||||
|
/// writer.write(once((&Variable::new_unchecked("foo"), &Literal::from("test").into())))?;
|
||||||
|
/// writer.write(&QuerySolution::from((vec![Variable::new_unchecked("bar")], vec![Some(Literal::from("test").into())])))?;
|
||||||
|
/// writer.finish()?;
|
||||||
|
/// assert_eq!(buffer, b"{\"head\":{\"vars\":[\"foo\",\"bar\"]},\"results\":{\"bindings\":[{\"foo\":{\"type\":\"literal\",\"value\":\"test\"}},{\"bar\":{\"type\":\"literal\",\"value\":\"test\"}}]}}");
|
||||||
|
/// # std::io::Result::Ok(())
|
||||||
|
/// ```
|
||||||
|
pub fn write<'a>( |
||||||
|
&mut self, |
||||||
|
solution: impl IntoIterator<Item = (&'a Variable, &'a Term)>, |
||||||
|
) -> io::Result<()> { |
||||||
|
match &mut self.formatter { |
||||||
|
SolutionsWriterKind::Xml(writer) => writer.write(solution), |
||||||
|
SolutionsWriterKind::Json(writer) => writer.write(solution), |
||||||
|
SolutionsWriterKind::Csv(writer) => writer.write(solution), |
||||||
|
SolutionsWriterKind::Tsv(writer) => writer.write(solution), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Writes the last bytes of the file.
|
||||||
|
pub fn finish(self) -> io::Result<W> { |
||||||
|
Ok(match self.formatter { |
||||||
|
SolutionsWriterKind::Xml(write) => write.finish()?, |
||||||
|
SolutionsWriterKind::Json(write) => write.finish()?, |
||||||
|
SolutionsWriterKind::Csv(write) => write.finish(), |
||||||
|
SolutionsWriterKind::Tsv(write) => write.finish(), |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,202 @@ |
|||||||
|
//! Definition of [`QuerySolution`] structure and associated utility constructions.
|
||||||
|
|
||||||
|
use oxrdf::{Term, Variable}; |
||||||
|
use std::iter::Zip; |
||||||
|
use std::rc::Rc; |
||||||
|
|
||||||
|
/// Tuple associating variables and terms that are the result of a SPARQL query.
|
||||||
|
///
|
||||||
|
/// It is the equivalent of a row in SQL.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::QuerySolution;
|
||||||
|
/// use oxrdf::{Variable, Literal};
|
||||||
|
///
|
||||||
|
/// let solution = QuerySolution::from((vec![Variable::new_unchecked("foo"), Variable::new_unchecked("bar")], vec![Some(Literal::from(1).into()), None]));
|
||||||
|
/// assert_eq!(solution.get("foo"), Some(&Literal::from(1).into())); // Get the value of the variable ?foo if it exists (here yes).
|
||||||
|
/// assert_eq!(solution.get(1), None); // Get the value of the second column if it exists (here no).
|
||||||
|
/// ```
|
||||||
|
pub struct QuerySolution { |
||||||
|
variables: Rc<Vec<Variable>>, |
||||||
|
values: Vec<Option<Term>>, |
||||||
|
} |
||||||
|
|
||||||
|
impl QuerySolution { |
||||||
|
/// Returns a value for a given position in the tuple ([`usize`](std::usize)) or a given variable name ([`&str`](std::str) or [`Variable`]).
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::QuerySolution;
|
||||||
|
/// use oxrdf::{Variable, Literal};
|
||||||
|
///
|
||||||
|
/// let solution = QuerySolution::from((vec![Variable::new_unchecked("foo"), Variable::new_unchecked("bar")], vec![Some(Literal::from(1).into()), None]));
|
||||||
|
/// assert_eq!(solution.get("foo"), Some(&Literal::from(1).into())); // Get the value of the variable ?foo if it exists (here yes).
|
||||||
|
/// assert_eq!(solution.get(1), None); // Get the value of the second column if it exists (here no).
|
||||||
|
/// ```
|
||||||
|
#[inline] |
||||||
|
pub fn get(&self, index: impl VariableSolutionIndex) -> Option<&Term> { |
||||||
|
self.values |
||||||
|
.get(index.index(self)?) |
||||||
|
.and_then(std::option::Option::as_ref) |
||||||
|
} |
||||||
|
|
||||||
|
/// The number of variables which could be bound.
|
||||||
|
///
|
||||||
|
/// It is also the number of columns in the solutions table.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::QuerySolution;
|
||||||
|
/// use oxrdf::{Variable, Literal};
|
||||||
|
///
|
||||||
|
/// let solution = QuerySolution::from((vec![Variable::new_unchecked("foo"), Variable::new_unchecked("bar")], vec![Some(Literal::from(1).into()), None]));
|
||||||
|
/// assert_eq!(solution.len(), 2); // there arre
|
||||||
|
/// ```
|
||||||
|
#[inline] |
||||||
|
pub fn len(&self) -> usize { |
||||||
|
self.values.len() |
||||||
|
} |
||||||
|
|
||||||
|
/// Is there any variable bound in the table?
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::QuerySolution;
|
||||||
|
/// use oxrdf::{Variable, Literal};
|
||||||
|
///
|
||||||
|
/// let solution = QuerySolution::from((vec![Variable::new_unchecked("foo"), Variable::new_unchecked("bar")], vec![Some(Literal::from(1).into()), None]));
|
||||||
|
/// assert!(!solution.is_empty());
|
||||||
|
///
|
||||||
|
/// let empty_solution = QuerySolution::from((vec![Variable::new_unchecked("foo"), Variable::new_unchecked("bar")], vec![None, None]));
|
||||||
|
/// assert!(empty_solution.is_empty());
|
||||||
|
/// ```
|
||||||
|
#[inline] |
||||||
|
pub fn is_empty(&self) -> bool { |
||||||
|
self.values.iter().all(|v| v.is_none()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns an iterator over bound variables.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::QuerySolution;
|
||||||
|
/// use oxrdf::{Variable, Literal};
|
||||||
|
///
|
||||||
|
/// let solution = QuerySolution::from((vec![Variable::new_unchecked("foo"), Variable::new_unchecked("bar")], vec![Some(Literal::from(1).into()), None]));
|
||||||
|
/// assert_eq!(solution.iter().collect::<Vec<_>>(), vec![(&Variable::new_unchecked("foo"), &Literal::from(1).into())]);
|
||||||
|
/// ```
|
||||||
|
#[inline] |
||||||
|
pub fn iter(&self) -> impl Iterator<Item = (&Variable, &Term)> { |
||||||
|
self.into_iter() |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the ordered slice of variable values.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::QuerySolution;
|
||||||
|
/// use oxrdf::{Variable, Literal};
|
||||||
|
///
|
||||||
|
/// let solution = QuerySolution::from((vec![Variable::new_unchecked("foo"), Variable::new_unchecked("bar")], vec![Some(Literal::from(1).into()), None]));
|
||||||
|
/// assert_eq!(solution.values(), &[Some(Literal::from(1).into()), None]);
|
||||||
|
/// ```
|
||||||
|
#[inline] |
||||||
|
pub fn values(&self) -> &[Option<Term>] { |
||||||
|
&self.values |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the ordered slice of the solution variables, bound or not.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::QuerySolution;
|
||||||
|
/// use oxrdf::{Variable, Literal};
|
||||||
|
///
|
||||||
|
/// let solution = QuerySolution::from((vec![Variable::new_unchecked("foo"), Variable::new_unchecked("bar")], vec![Some(Literal::from(1).into()), None]));
|
||||||
|
/// assert_eq!(solution.variables(), &[Variable::new_unchecked("foo"), Variable::new_unchecked("bar")]);
|
||||||
|
/// ```
|
||||||
|
#[inline] |
||||||
|
pub fn variables(&self) -> &[Variable] { |
||||||
|
&self.variables |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<V: Into<Rc<Vec<Variable>>>, S: Into<Vec<Option<Term>>>> From<(V, S)> for QuerySolution { |
||||||
|
#[inline] |
||||||
|
fn from((v, s): (V, S)) -> Self { |
||||||
|
QuerySolution { |
||||||
|
variables: v.into(), |
||||||
|
values: s.into(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a> IntoIterator for &'a QuerySolution { |
||||||
|
type Item = (&'a Variable, &'a Term); |
||||||
|
type IntoIter = Iter<'a>; |
||||||
|
|
||||||
|
fn into_iter(self) -> Iter<'a> { |
||||||
|
Iter { |
||||||
|
inner: self.variables.iter().zip(&self.values), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// An iterator over [`QuerySolution`] bound variables.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use sparesults::QuerySolution;
|
||||||
|
/// use oxrdf::{Variable, Literal};
|
||||||
|
///
|
||||||
|
/// let solution = QuerySolution::from((vec![Variable::new_unchecked("foo"), Variable::new_unchecked("bar")], vec![Some(Literal::from(1).into()), None]));
|
||||||
|
/// assert_eq!(solution.iter().collect::<Vec<_>>(), vec![(&Variable::new_unchecked("foo"), &Literal::from(1).into())]);
|
||||||
|
/// ```
|
||||||
|
pub struct Iter<'a> { |
||||||
|
inner: Zip<std::slice::Iter<'a, Variable>, std::slice::Iter<'a, Option<Term>>>, |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a> Iterator for Iter<'a> { |
||||||
|
type Item = (&'a Variable, &'a Term); |
||||||
|
|
||||||
|
fn next(&mut self) -> Option<(&'a Variable, &'a Term)> { |
||||||
|
for (variable, value) in &mut self.inner { |
||||||
|
if let Some(value) = value { |
||||||
|
return Some((variable, value)); |
||||||
|
} |
||||||
|
} |
||||||
|
None |
||||||
|
} |
||||||
|
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) { |
||||||
|
(0, self.inner.size_hint().1) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// A utility trait to get values for a given variable or tuple position.
|
||||||
|
///
|
||||||
|
/// See [`QuerySolution::get`].
|
||||||
|
pub trait VariableSolutionIndex { |
||||||
|
fn index(self, solution: &QuerySolution) -> Option<usize>; |
||||||
|
} |
||||||
|
|
||||||
|
impl VariableSolutionIndex for usize { |
||||||
|
#[inline] |
||||||
|
fn index(self, _: &QuerySolution) -> Option<usize> { |
||||||
|
Some(self) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl VariableSolutionIndex for &str { |
||||||
|
#[inline] |
||||||
|
fn index(self, solution: &QuerySolution) -> Option<usize> { |
||||||
|
solution.variables.iter().position(|v| v.as_str() == self) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl VariableSolutionIndex for &Variable { |
||||||
|
#[inline] |
||||||
|
fn index(self, solution: &QuerySolution) -> Option<usize> { |
||||||
|
solution.variables.iter().position(|v| v == self) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl VariableSolutionIndex for Variable { |
||||||
|
#[inline] |
||||||
|
fn index(self, solution: &QuerySolution) -> Option<usize> { |
||||||
|
(&self).index(solution) |
||||||
|
} |
||||||
|
} |
@ -1,337 +0,0 @@ |
|||||||
mod csv; |
|
||||||
mod json; |
|
||||||
mod xml; |
|
||||||
|
|
||||||
use crate::io::read::{ParserError, SyntaxError}; |
|
||||||
use crate::model::{Term, TermRef}; |
|
||||||
use crate::sparql::io::csv::*; |
|
||||||
use crate::sparql::io::json::*; |
|
||||||
use crate::sparql::io::xml::*; |
|
||||||
use crate::sparql::{EvaluationError, QueryResults, QuerySolution, QuerySolutionIter, Variable}; |
|
||||||
use std::io::{self, BufRead, Write}; |
|
||||||
use std::rc::Rc; |
|
||||||
|
|
||||||
/// [SPARQL query](https://www.w3.org/TR/sparql11-query/) results serialization formats.
|
|
||||||
#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)] |
|
||||||
#[non_exhaustive] |
|
||||||
pub enum QueryResultsFormat { |
|
||||||
/// [SPARQL Query Results XML Format](http://www.w3.org/TR/rdf-sparql-XMLres/)
|
|
||||||
Xml, |
|
||||||
/// [SPARQL Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/)
|
|
||||||
Json, |
|
||||||
/// [SPARQL Query Results CSV Format](https://www.w3.org/TR/sparql11-results-csv-tsv/)
|
|
||||||
Csv, |
|
||||||
/// [SPARQL Query Results TSV Format](https://www.w3.org/TR/sparql11-results-csv-tsv/)
|
|
||||||
Tsv, |
|
||||||
} |
|
||||||
|
|
||||||
impl QueryResultsFormat { |
|
||||||
/// The format canonical IRI according to the [Unique URIs for file formats registry](https://www.w3.org/ns/formats/).
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use oxigraph::sparql::QueryResultsFormat;
|
|
||||||
///
|
|
||||||
/// assert_eq!(QueryResultsFormat::Json.iri(), "http://www.w3.org/ns/formats/SPARQL_Results_JSON")
|
|
||||||
/// ```
|
|
||||||
#[inline] |
|
||||||
pub fn iri(self) -> &'static str { |
|
||||||
match self { |
|
||||||
QueryResultsFormat::Xml => "http://www.w3.org/ns/formats/SPARQL_Results_XML", |
|
||||||
QueryResultsFormat::Json => "http://www.w3.org/ns/formats/SPARQL_Results_JSON", |
|
||||||
QueryResultsFormat::Csv => "http://www.w3.org/ns/formats/SPARQL_Results_CSV", |
|
||||||
QueryResultsFormat::Tsv => "http://www.w3.org/ns/formats/SPARQL_Results_TSV", |
|
||||||
} |
|
||||||
} |
|
||||||
/// The format [IANA media type](https://tools.ietf.org/html/rfc2046).
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use oxigraph::sparql::QueryResultsFormat;
|
|
||||||
///
|
|
||||||
/// assert_eq!(QueryResultsFormat::Json.media_type(), "application/sparql-results+json")
|
|
||||||
/// ```
|
|
||||||
#[inline] |
|
||||||
pub fn media_type(self) -> &'static str { |
|
||||||
match self { |
|
||||||
QueryResultsFormat::Xml => "application/sparql-results+xml", |
|
||||||
QueryResultsFormat::Json => "application/sparql-results+json", |
|
||||||
QueryResultsFormat::Csv => "text/csv; charset=utf-8", |
|
||||||
QueryResultsFormat::Tsv => "text/tab-separated-values; charset=utf-8", |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// The format [IANA-registered](https://tools.ietf.org/html/rfc2046) file extension.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use oxigraph::sparql::QueryResultsFormat;
|
|
||||||
///
|
|
||||||
/// assert_eq!(QueryResultsFormat::Json.file_extension(), "srj")
|
|
||||||
/// ```
|
|
||||||
#[inline] |
|
||||||
pub fn file_extension(self) -> &'static str { |
|
||||||
match self { |
|
||||||
QueryResultsFormat::Xml => "srx", |
|
||||||
QueryResultsFormat::Json => "srj", |
|
||||||
QueryResultsFormat::Csv => "csv", |
|
||||||
QueryResultsFormat::Tsv => "tsv", |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Looks for a known format from a media type.
|
|
||||||
///
|
|
||||||
/// It supports some media type aliases.
|
|
||||||
/// For example "application/xml" is going to return `Xml` even if it is not its canonical media type.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```
|
|
||||||
/// use oxigraph::sparql::QueryResultsFormat;
|
|
||||||
///
|
|
||||||
/// assert_eq!(QueryResultsFormat::from_media_type("application/sparql-results+json; charset=utf-8"), Some(QueryResultsFormat::Json))
|
|
||||||
/// ```
|
|
||||||
pub fn from_media_type(media_type: &str) -> Option<Self> { |
|
||||||
match media_type.split(';').next()?.trim() { |
|
||||||
"application/sparql-results+xml" | "application/xml" | "text/xml" => Some(Self::Xml), |
|
||||||
"application/sparql-results+json" | "application/json" | "text/json" => { |
|
||||||
Some(Self::Json) |
|
||||||
} |
|
||||||
"text/csv" => Some(Self::Csv), |
|
||||||
"text/tab-separated-values" | "text/tsv" => Some(Self::Tsv), |
|
||||||
_ => None, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Looks for a known format from an extension.
|
|
||||||
///
|
|
||||||
/// It supports some aliases.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```
|
|
||||||
/// use oxigraph::sparql::QueryResultsFormat;
|
|
||||||
///
|
|
||||||
/// assert_eq!(QueryResultsFormat::from_extension("json"), Some(QueryResultsFormat::Json))
|
|
||||||
/// ```
|
|
||||||
pub fn from_extension(extension: &str) -> Option<Self> { |
|
||||||
match extension { |
|
||||||
"srx" | "xml" => Some(Self::Xml), |
|
||||||
"srj" | "json" => Some(Self::Json), |
|
||||||
"csv" | "txt" => Some(Self::Csv), |
|
||||||
"tsv" => Some(Self::Tsv), |
|
||||||
_ => None, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Parsers for [SPARQL query](https://www.w3.org/TR/sparql11-query/) results serialization formats.
|
|
||||||
///
|
|
||||||
/// It currently supports the following formats:
|
|
||||||
/// * [SPARQL Query Results XML Format](http://www.w3.org/TR/rdf-sparql-XMLres/) ([`QueryResultsFormat::Xml`](QueryResultsFormat::Xml))
|
|
||||||
/// * [SPARQL Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/) ([`QueryResultsFormat::Json`](QueryResultsFormat::Json))
|
|
||||||
/// * [SPARQL Query Results TSV Format](https://www.w3.org/TR/sparql11-results-csv-tsv/) ([`QueryResultsFormat::Tsv`](QueryResultsFormat::Tsv))
|
|
||||||
#[allow(missing_copy_implementations)] |
|
||||||
pub struct QueryResultsParser { |
|
||||||
format: QueryResultsFormat, |
|
||||||
} |
|
||||||
|
|
||||||
impl QueryResultsParser { |
|
||||||
/// Builds a parser for the given format.
|
|
||||||
pub fn from_format(format: QueryResultsFormat) -> Self { |
|
||||||
Self { format } |
|
||||||
} |
|
||||||
|
|
||||||
pub fn read_results<R: BufRead>( |
|
||||||
&self, |
|
||||||
reader: R, |
|
||||||
) -> Result<QueryResultsReader<R>, ParserError> { |
|
||||||
Ok(match self.format { |
|
||||||
QueryResultsFormat::Xml => match XmlQueryResultsReader::read(reader)? { |
|
||||||
XmlQueryResultsReader::Boolean(r) => QueryResultsReader::Boolean(r), |
|
||||||
XmlQueryResultsReader::Solutions { |
|
||||||
solutions, |
|
||||||
variables, |
|
||||||
} => QueryResultsReader::Solutions(SolutionsReader { |
|
||||||
variables: Rc::new(variables), |
|
||||||
solutions: SolutionsReaderKind::Xml(solutions), |
|
||||||
}), |
|
||||||
}, |
|
||||||
QueryResultsFormat::Json => match JsonQueryResultsReader::read(reader)? { |
|
||||||
JsonQueryResultsReader::Boolean(r) => QueryResultsReader::Boolean(r), |
|
||||||
JsonQueryResultsReader::Solutions { |
|
||||||
solutions, |
|
||||||
variables, |
|
||||||
} => QueryResultsReader::Solutions(SolutionsReader { |
|
||||||
variables: Rc::new(variables), |
|
||||||
solutions: SolutionsReaderKind::Json(solutions), |
|
||||||
}), |
|
||||||
}, |
|
||||||
QueryResultsFormat::Csv => return Err(SyntaxError::msg("CSV SPARQL results syntax is lossy and can't be parsed to a proper RDF representation").into()), |
|
||||||
QueryResultsFormat::Tsv => match TsvQueryResultsReader::read(reader)? { |
|
||||||
TsvQueryResultsReader::Boolean(r) => QueryResultsReader::Boolean(r), |
|
||||||
TsvQueryResultsReader::Solutions { |
|
||||||
solutions, |
|
||||||
variables, |
|
||||||
} => QueryResultsReader::Solutions(SolutionsReader { |
|
||||||
variables: Rc::new(variables), |
|
||||||
solutions: SolutionsReaderKind::Tsv(solutions), |
|
||||||
}), |
|
||||||
}, |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub enum QueryResultsReader<R: BufRead> { |
|
||||||
Solutions(SolutionsReader<R>), |
|
||||||
Boolean(bool), |
|
||||||
} |
|
||||||
|
|
||||||
pub struct SolutionsReader<R: BufRead> { |
|
||||||
variables: Rc<Vec<Variable>>, |
|
||||||
solutions: SolutionsReaderKind<R>, |
|
||||||
} |
|
||||||
|
|
||||||
enum SolutionsReaderKind<R: BufRead> { |
|
||||||
Xml(XmlSolutionsReader<R>), |
|
||||||
Json(JsonSolutionsReader<R>), |
|
||||||
Tsv(TsvSolutionsReader<R>), |
|
||||||
} |
|
||||||
|
|
||||||
impl<R: BufRead> SolutionsReader<R> { |
|
||||||
#[inline] |
|
||||||
pub fn variables(&self) -> &[Variable] { |
|
||||||
&self.variables |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<R: BufRead> Iterator for SolutionsReaderKind<R> { |
|
||||||
type Item = Result<Vec<Option<Term>>, ParserError>; |
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Result<Vec<Option<Term>>, ParserError>> { |
|
||||||
match self { |
|
||||||
Self::Xml(reader) => reader.read_next(), |
|
||||||
Self::Json(reader) => reader.read_next(), |
|
||||||
Self::Tsv(reader) => reader.read_next(), |
|
||||||
} |
|
||||||
.transpose() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<R: BufRead> Iterator for SolutionsReader<R> { |
|
||||||
type Item = Result<QuerySolution, ParserError>; |
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Result<QuerySolution, ParserError>> { |
|
||||||
Some(self.solutions.next()?.map(|values| QuerySolution { |
|
||||||
values, |
|
||||||
variables: self.variables.clone(), |
|
||||||
})) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<R: BufRead + 'static> From<SolutionsReader<R>> for QuerySolutionIter { |
|
||||||
fn from(reader: SolutionsReader<R>) -> Self { |
|
||||||
Self::new( |
|
||||||
reader.variables.clone(), |
|
||||||
Box::new(reader.solutions.map(|r| r.map_err(EvaluationError::from))), |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<R: BufRead + 'static> From<QueryResultsReader<R>> for QueryResults { |
|
||||||
fn from(reader: QueryResultsReader<R>) -> Self { |
|
||||||
match reader { |
|
||||||
QueryResultsReader::Solutions(s) => Self::Solutions(s.into()), |
|
||||||
QueryResultsReader::Boolean(v) => Self::Boolean(v), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// A serializer for [SPARQL query](https://www.w3.org/TR/sparql11-query/) results serialization formats.
|
|
||||||
///
|
|
||||||
/// It currently supports the following formats:
|
|
||||||
/// * [SPARQL Query Results XML Format](http://www.w3.org/TR/rdf-sparql-XMLres/) ([`QueryResultsFormat::Xml`](QueryResultsFormat::Xml))
|
|
||||||
/// * [SPARQL Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/) ([`QueryResultsFormat::Json`](QueryResultsFormat::Json))
|
|
||||||
/// * [SPARQL Query Results CSV Format](https://www.w3.org/TR/sparql11-results-csv-tsv/) ([`QueryResultsFormat::Csv`](QueryResultsFormat::Csv))
|
|
||||||
/// * [SPARQL Query Results TSV Format](https://www.w3.org/TR/sparql11-results-csv-tsv/) ([`QueryResultsFormat::Tsv`](QueryResultsFormat::Tsv))
|
|
||||||
#[allow(missing_copy_implementations)] |
|
||||||
pub struct QueryResultsSerializer { |
|
||||||
format: QueryResultsFormat, |
|
||||||
} |
|
||||||
|
|
||||||
impl QueryResultsSerializer { |
|
||||||
/// Builds a serializer for the given format
|
|
||||||
pub fn from_format(format: QueryResultsFormat) -> Self { |
|
||||||
Self { format } |
|
||||||
} |
|
||||||
|
|
||||||
pub fn write_boolean_result<W: Write>(&self, writer: W, value: bool) -> io::Result<W> { |
|
||||||
match self.format { |
|
||||||
QueryResultsFormat::Xml => write_boolean_xml_result(writer, value), |
|
||||||
QueryResultsFormat::Json => write_boolean_json_result(writer, value), |
|
||||||
QueryResultsFormat::Csv => write_boolean_csv_result(writer, value), |
|
||||||
QueryResultsFormat::Tsv => write_boolean_tsv_result(writer, value), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns a `SolutionsWriter` allowing writing query solutions into the given [`Write`](std::io::Write) implementation
|
|
||||||
pub fn solutions_writer<W: Write>( |
|
||||||
&self, |
|
||||||
writer: W, |
|
||||||
variables: &[Variable], |
|
||||||
) -> io::Result<SolutionsWriter<W>> { |
|
||||||
Ok(SolutionsWriter { |
|
||||||
formatter: match self.format { |
|
||||||
QueryResultsFormat::Xml => { |
|
||||||
SolutionsWriterKind::Xml(XmlSolutionsWriter::start(writer, variables)?) |
|
||||||
} |
|
||||||
QueryResultsFormat::Json => { |
|
||||||
SolutionsWriterKind::Json(JsonSolutionsWriter::start(writer, variables)?) |
|
||||||
} |
|
||||||
QueryResultsFormat::Csv => { |
|
||||||
SolutionsWriterKind::Csv(CsvSolutionsWriter::start(writer, variables)?) |
|
||||||
} |
|
||||||
QueryResultsFormat::Tsv => { |
|
||||||
SolutionsWriterKind::Tsv(TsvSolutionsWriter::start(writer, variables)?) |
|
||||||
} |
|
||||||
}, |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Allows writing query results.
|
|
||||||
/// Could be built using a [`QueryResultsSerializer`].
|
|
||||||
///
|
|
||||||
/// Warning: Do not forget to run the [`finish`](SolutionsWriter::finish()) method to properly write the last bytes of the file.
|
|
||||||
#[must_use] |
|
||||||
pub struct SolutionsWriter<W: Write> { |
|
||||||
formatter: SolutionsWriterKind<W>, |
|
||||||
} |
|
||||||
|
|
||||||
enum SolutionsWriterKind<W: Write> { |
|
||||||
Xml(XmlSolutionsWriter<W>), |
|
||||||
Json(JsonSolutionsWriter<W>), |
|
||||||
Csv(CsvSolutionsWriter<W>), |
|
||||||
Tsv(TsvSolutionsWriter<W>), |
|
||||||
} |
|
||||||
|
|
||||||
impl<W: Write> SolutionsWriter<W> { |
|
||||||
/// Writes a solution
|
|
||||||
pub fn write<'a>( |
|
||||||
&mut self, |
|
||||||
solution: impl IntoIterator<Item = Option<TermRef<'a>>>, |
|
||||||
) -> io::Result<()> { |
|
||||||
match &mut self.formatter { |
|
||||||
SolutionsWriterKind::Xml(writer) => writer.write(solution), |
|
||||||
SolutionsWriterKind::Json(writer) => writer.write(solution), |
|
||||||
SolutionsWriterKind::Csv(writer) => writer.write(solution), |
|
||||||
SolutionsWriterKind::Tsv(writer) => writer.write(solution), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Writes the last bytes of the file
|
|
||||||
pub fn finish(self) -> io::Result<()> { |
|
||||||
match self.formatter { |
|
||||||
SolutionsWriterKind::Xml(write) => write.finish()?, |
|
||||||
SolutionsWriterKind::Json(write) => write.finish()?, |
|
||||||
SolutionsWriterKind::Csv(write) => write.finish(), |
|
||||||
SolutionsWriterKind::Tsv(write) => write.finish(), |
|
||||||
}; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
} |
|
Loading…
Reference in new issue