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