Moves out of Oxigraph SPARQL results parser and serializers

pull/190/head
Tpt 3 years ago
parent 4d18053ec9
commit 850b8eddcf
  1. 104
      Cargo.lock
  2. 1
      Cargo.toml
  3. 3
      lib/Cargo.toml
  4. 25
      lib/sparesults/Cargo.toml
  5. 71
      lib/sparesults/README.md
  6. 68
      lib/sparesults/src/csv.rs
  7. 130
      lib/sparesults/src/error.rs
  8. 262
      lib/sparesults/src/json.rs
  9. 506
      lib/sparesults/src/lib.rs
  10. 202
      lib/sparesults/src/solution.rs
  11. 43
      lib/sparesults/src/xml.rs
  12. 37
      lib/src/io/read.rs
  13. 18
      lib/src/sparql/error.rs
  14. 2
      lib/src/sparql/eval.rs
  15. 337
      lib/src/sparql/io/mod.rs
  16. 3
      lib/src/sparql/mod.rs
  17. 150
      lib/src/sparql/model.rs
  18. 7
      python/src/sparql.rs

104
Cargo.lock generated

@ -112,9 +112,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.8.0" version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
[[package]] [[package]]
name = "cast" name = "cast"
@ -177,9 +177,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "3.0.0" version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d17bf219fcd37199b9a29e00ba65dfb8cd5b2688b7297ec14ff829c40ac50ca9" checksum = "f6f34b09b9ee8c7c7b400fe2f8df39cafc9538b03d6ba7f4ae13e4cb90bfbb7d"
dependencies = [ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
@ -194,9 +194,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "3.0.0" version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1b9752c030a14235a0bd5ef3ad60a1dcac8468c30921327fc8af36b20c790b9" checksum = "41a0645a430ec9136d2d701e54a95d557de12649a9dd7109ced3187e648ac824"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro-error", "proc-macro-error",
@ -278,9 +278,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.1" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"crossbeam-utils", "crossbeam-utils",
@ -299,9 +299,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-epoch" name = "crossbeam-epoch"
version = "0.9.5" version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" checksum = "97242a70df9b89a65d0b6df3c4bf5b9ce03c5b7309019777fbde37e7537f8762"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"crossbeam-utils", "crossbeam-utils",
@ -312,9 +312,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.5" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"lazy_static", "lazy_static",
@ -391,6 +391,15 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "fastrand"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2"
dependencies = [
"instant",
]
[[package]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.3.2" version = "0.3.2"
@ -418,9 +427,9 @@ dependencies = [
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.4" version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
dependencies = [ dependencies = [
"typenum", "typenum",
"version_check", "version_check",
@ -471,12 +480,9 @@ dependencies = [
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.3.3" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
@ -518,9 +524,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.7.0" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown",
@ -835,7 +841,6 @@ dependencies = [
"getrandom", "getrandom",
"hex", "hex",
"js-sys", "js-sys",
"json-event-parser",
"lasso", "lasso",
"lazy_static", "lazy_static",
"libc", "libc",
@ -847,7 +852,6 @@ dependencies = [
"oxiri", "oxiri",
"oxrdf", "oxrdf",
"oxrocksdb-sys", "oxrocksdb-sys",
"quick-xml",
"rand", "rand",
"regex", "regex",
"rio_api", "rio_api",
@ -857,6 +861,7 @@ dependencies = [
"sha2", "sha2",
"siphasher", "siphasher",
"sophia_api", "sophia_api",
"sparesults",
"spargebra", "spargebra",
"wasm-bindgen-test", "wasm-bindgen-test",
"zstd", "zstd",
@ -877,7 +882,7 @@ dependencies = [
name = "oxigraph_server" name = "oxigraph_server"
version = "0.3.0-dev" version = "0.3.0-dev"
dependencies = [ dependencies = [
"clap 3.0.0", "clap 3.0.5",
"oxhttp", "oxhttp",
"oxigraph", "oxigraph",
"oxiri", "oxiri",
@ -890,7 +895,7 @@ name = "oxigraph_testsuite"
version = "0.3.0-dev" version = "0.3.0-dev"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap 3.0.0", "clap 3.0.5",
"criterion", "criterion",
"oxigraph", "oxigraph",
"text-diff", "text-diff",
@ -1448,9 +1453,9 @@ checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.132" version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008" checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a"
[[package]] [[package]]
name = "serde_cbor" name = "serde_cbor"
@ -1464,9 +1469,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.132" version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276" checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1475,9 +1480,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.73" version = "1.0.74"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5" checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142"
dependencies = [ dependencies = [
"itoa 1.0.1", "itoa 1.0.1",
"ryu", "ryu",
@ -1497,9 +1502,9 @@ dependencies = [
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.0" version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900d964dd36bb15bcf2f2b35694c072feab74969a54f2bbeec7a2d725d2bdcb6" checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures",
@ -1550,6 +1555,15 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "sparesults"
version = "0.1.0"
dependencies = [
"json-event-parser",
"oxrdf",
"quick-xml",
]
[[package]] [[package]]
name = "spargebra" name = "spargebra"
version = "0.1.0" version = "0.1.0"
@ -1581,9 +1595,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.84" version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b" checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1592,13 +1606,13 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.2.0" version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand",
"libc", "libc",
"rand",
"redox_syscall", "redox_syscall",
"remove_dir_all", "remove_dir_all",
"winapi 0.3.9", "winapi 0.3.9",
@ -1724,12 +1738,6 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "unicode-segmentation"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.9" version = "0.1.9"
@ -1967,18 +1975,18 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "0.9.1+zstd.1.5.1" version = "0.9.2+zstd.1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "538b8347df9257b7fbce37677ef7535c00a3c7bf1f81023cc328ed7fe4b41de8" checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54"
dependencies = [ dependencies = [
"zstd-safe", "zstd-safe",
] ]
[[package]] [[package]]
name = "zstd-safe" name = "zstd-safe"
version = "4.1.2+zstd.1.5.1" version = "4.1.3+zstd.1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb4cfe2f6e6d35c5d27ecd9d256c4b6f7933c4895654917460ec56c29336cc1" checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79"
dependencies = [ dependencies = [
"libc", "libc",
"zstd-sys", "zstd-sys",

@ -4,6 +4,7 @@ members = [
"lib", "lib",
"lib/oxrdf", "lib/oxrdf",
"lib/spargebra", "lib/spargebra",
"lib/sparesults",
"python", "python",
"rocksdb-sys", "rocksdb-sys",
"server", "server",

@ -22,7 +22,6 @@ sophia = ["sophia_api", "oxrdf/sophia_api"]
http_client = ["oxhttp", "oxhttp/rustls"] http_client = ["oxhttp", "oxhttp/rustls"]
[dependencies] [dependencies]
quick-xml = "0.22"
rand = "0.8" rand = "0.8"
md-5 = "0.10" md-5 = "0.10"
sha-1 = "0.10" sha-1 = "0.10"
@ -40,10 +39,10 @@ siphasher = "0.3"
lasso = {version="0.6", features=["multi-threaded", "inline-more"]} lasso = {version="0.6", features=["multi-threaded", "inline-more"]}
lazy_static = "1" lazy_static = "1"
sophia_api = { version = "0.7", optional = true } sophia_api = { version = "0.7", optional = true }
json-event-parser = "0.1"
num_cpus = "1" num_cpus = "1"
oxrdf = { version = "0.1", path="oxrdf", features = ["rdf-star"] } oxrdf = { version = "0.1", path="oxrdf", features = ["rdf-star"] }
spargebra = { version = "0.1", path="spargebra", features = ["rdf-star"] } spargebra = { version = "0.1", path="spargebra", features = ["rdf-star"] }
sparesults = { version = "0.1", path="sparesults", features = ["rdf-star"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
libc = "0.2" libc = "0.2"

@ -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.

@ -1,8 +1,8 @@
//! Implementation of [SPARQL 1.1 Query Results CSV and TSV Formats](https://www.w3.org/TR/sparql11-results-csv-tsv/) //! Implementation of [SPARQL 1.1 Query Results CSV and TSV Formats](https://www.w3.org/TR/sparql11-results-csv-tsv/)
use crate::io::read::{ParserError, SyntaxError}; use crate::error::{ParseError, SyntaxError, SyntaxErrorKind};
use crate::model::{vocab::xsd, *};
use oxrdf::Variable; use oxrdf::Variable;
use oxrdf::{vocab::xsd, *};
use std::io::{self, BufRead, Write}; use std::io::{self, BufRead, Write};
use std::str::FromStr; use std::str::FromStr;
@ -13,12 +13,13 @@ pub fn write_boolean_csv_result<W: Write>(mut sink: W, value: bool) -> io::Resul
pub struct CsvSolutionsWriter<W: Write> { pub struct CsvSolutionsWriter<W: Write> {
sink: W, sink: W,
variables: Vec<Variable>,
} }
impl<W: Write> CsvSolutionsWriter<W> { impl<W: Write> CsvSolutionsWriter<W> {
pub fn start(mut sink: W, variables: &[Variable]) -> io::Result<Self> { pub fn start(mut sink: W, variables: Vec<Variable>) -> io::Result<Self> {
let mut start_vars = true; let mut start_vars = true;
for variable in variables { for variable in &variables {
if start_vars { if start_vars {
start_vars = false; start_vars = false;
} else { } else {
@ -26,16 +27,22 @@ impl<W: Write> CsvSolutionsWriter<W> {
} }
sink.write_all(variable.as_str().as_bytes())?; sink.write_all(variable.as_str().as_bytes())?;
} }
Ok(Self { sink }) Ok(Self { sink, variables })
} }
pub fn write<'a>( pub fn write<'a>(
&mut self, &mut self,
solution: impl IntoIterator<Item = Option<TermRef<'a>>>, solution: impl IntoIterator<Item = (&'a Variable, &'a Term)>,
) -> io::Result<()> { ) -> io::Result<()> {
let mut values = vec![None; self.variables.len()];
for (variable, value) in solution {
if let Some(position) = self.variables.iter().position(|v| v == variable) {
values[position] = Some(value);
}
}
self.sink.write_all(b"\r\n")?; self.sink.write_all(b"\r\n")?;
let mut start_binding = true; let mut start_binding = true;
for value in solution { for value in values {
if start_binding { if start_binding {
start_binding = false; start_binding = false;
} else { } else {
@ -61,6 +68,7 @@ fn write_csv_term<'a>(term: impl Into<TermRef<'a>>, sink: &mut impl Write) -> io
sink.write_all(bnode.as_str().as_bytes()) sink.write_all(bnode.as_str().as_bytes())
} }
TermRef::Literal(literal) => write_escaped_csv_string(literal.value(), sink), TermRef::Literal(literal) => write_escaped_csv_string(literal.value(), sink),
#[cfg(feature = "rdf-star")]
TermRef::Triple(triple) => { TermRef::Triple(triple) => {
write_csv_term(&triple.subject, sink)?; write_csv_term(&triple.subject, sink)?;
sink.write_all(b" ")?; sink.write_all(b" ")?;
@ -94,12 +102,13 @@ pub fn write_boolean_tsv_result<W: Write>(mut sink: W, value: bool) -> io::Resul
pub struct TsvSolutionsWriter<W: Write> { pub struct TsvSolutionsWriter<W: Write> {
sink: W, sink: W,
variables: Vec<Variable>,
} }
impl<W: Write> TsvSolutionsWriter<W> { impl<W: Write> TsvSolutionsWriter<W> {
pub fn start(mut sink: W, variables: &[Variable]) -> io::Result<Self> { pub fn start(mut sink: W, variables: Vec<Variable>) -> io::Result<Self> {
let mut start_vars = true; let mut start_vars = true;
for variable in variables { for variable in &variables {
if start_vars { if start_vars {
start_vars = false; start_vars = false;
} else { } else {
@ -108,16 +117,22 @@ impl<W: Write> TsvSolutionsWriter<W> {
sink.write_all(b"?")?; sink.write_all(b"?")?;
sink.write_all(variable.as_str().as_bytes())?; sink.write_all(variable.as_str().as_bytes())?;
} }
Ok(Self { sink }) Ok(Self { sink, variables })
} }
pub fn write<'a>( pub fn write<'a>(
&mut self, &mut self,
solution: impl IntoIterator<Item = Option<TermRef<'a>>>, solution: impl IntoIterator<Item = (&'a Variable, &'a Term)>,
) -> io::Result<()> { ) -> io::Result<()> {
let mut values = vec![None; self.variables.len()];
for (variable, value) in solution {
if let Some(position) = self.variables.iter().position(|v| v == variable) {
values[position] = Some(value);
}
}
self.sink.write_all(b"\n")?; self.sink.write_all(b"\n")?;
let mut start_binding = true; let mut start_binding = true;
for value in solution { for value in values {
if start_binding { if start_binding {
start_binding = false; start_binding = false;
} else { } else {
@ -155,6 +170,7 @@ fn write_tsv_term<'a>(term: impl Into<TermRef<'a>>, sink: &mut impl Write) -> io
} }
_ => sink.write_all(literal.to_string().as_bytes()), _ => sink.write_all(literal.to_string().as_bytes()),
}, },
#[cfg(feature = "rdf-star")]
TermRef::Triple(triple) => { TermRef::Triple(triple) => {
sink.write_all(b"<<")?; sink.write_all(b"<<")?;
write_tsv_term(&triple.subject, sink)?; write_tsv_term(&triple.subject, sink)?;
@ -177,7 +193,7 @@ pub enum TsvQueryResultsReader<R: BufRead> {
} }
impl<R: BufRead> TsvQueryResultsReader<R> { impl<R: BufRead> TsvQueryResultsReader<R> {
pub fn read(mut source: R) -> Result<Self, ParserError> { pub fn read(mut source: R) -> Result<Self, ParseError> {
let mut buffer = String::new(); let mut buffer = String::new();
// We read the header // We read the header
@ -209,7 +225,7 @@ pub struct TsvSolutionsReader<R: BufRead> {
} }
impl<R: BufRead> TsvSolutionsReader<R> { impl<R: BufRead> TsvSolutionsReader<R> {
pub fn read_next(&mut self) -> Result<Option<Vec<Option<Term>>>, ParserError> { pub fn read_next(&mut self) -> Result<Option<Vec<Option<Term>>>, ParseError> {
self.buffer.clear(); self.buffer.clear();
if self.source.read_line(&mut self.buffer)? == 0 { if self.source.read_line(&mut self.buffer)? == 0 {
return Ok(None); return Ok(None);
@ -222,12 +238,12 @@ impl<R: BufRead> TsvSolutionsReader<R> {
if v.is_empty() { if v.is_empty() {
Ok(None) Ok(None)
} else { } else {
Ok(Some( Ok(Some(Term::from_str(v).map_err(|e| SyntaxError {
Term::from_str(v).map_err(|e| SyntaxError::msg(e.to_string()))?, inner: SyntaxErrorKind::Term(e),
)) })?))
} }
}) })
.collect::<Result<_, ParserError>>()?, .collect::<Result<_, ParseError>>()?,
)) ))
} }
} }
@ -235,6 +251,8 @@ impl<R: BufRead> TsvSolutionsReader<R> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::QuerySolution;
use std::rc::Rc;
use std::str; use std::str;
fn build_example() -> (Vec<Variable>, Vec<Vec<Option<Term>>>) { fn build_example() -> (Vec<Variable>, Vec<Vec<Option<Term>>>) {
@ -283,9 +301,10 @@ mod tests {
#[test] #[test]
fn test_csv_serialization() -> io::Result<()> { fn test_csv_serialization() -> io::Result<()> {
let (variables, solutions) = build_example(); let (variables, solutions) = build_example();
let mut writer = CsvSolutionsWriter::start(Vec::new(), &variables)?; let mut writer = CsvSolutionsWriter::start(Vec::new(), variables.clone())?;
for solution in &solutions { let variables = Rc::new(variables);
writer.write(solution.iter().map(|t| t.as_ref().map(|t| t.as_ref())))?; for solution in solutions {
writer.write(QuerySolution::from((variables.clone(), solution)).iter())?;
} }
let result = writer.finish(); let result = writer.finish();
assert_eq!(str::from_utf8(&result).unwrap(), "x,literal\r\nhttp://example/x,String\r\nhttp://example/x,\"String-with-dquote\"\"\"\r\n_:b0,Blank node\r\n,Missing 'x'\r\n,\r\nhttp://example/x,\r\n_:b1,String-with-lang\r\n_:b1,123"); assert_eq!(str::from_utf8(&result).unwrap(), "x,literal\r\nhttp://example/x,String\r\nhttp://example/x,\"String-with-dquote\"\"\"\r\n_:b0,Blank node\r\n,Missing 'x'\r\n,\r\nhttp://example/x,\r\n_:b1,String-with-lang\r\n_:b1,123");
@ -295,9 +314,10 @@ mod tests {
#[test] #[test]
fn test_tsv_serialization() -> io::Result<()> { fn test_tsv_serialization() -> io::Result<()> {
let (variables, solutions) = build_example(); let (variables, solutions) = build_example();
let mut writer = TsvSolutionsWriter::start(Vec::new(), &variables)?; let mut writer = TsvSolutionsWriter::start(Vec::new(), variables.clone())?;
for solution in &solutions { let variables = Rc::new(variables);
writer.write(solution.iter().map(|t| t.as_ref().map(|t| t.as_ref())))?; for solution in solutions {
writer.write(QuerySolution::from((variables.clone(), solution)).iter())?;
} }
let result = writer.finish(); let result = writer.finish();
assert_eq!(str::from_utf8(&result).unwrap(), "?x\t?literal\n<http://example/x>\t\"String\"\n<http://example/x>\t\"String-with-dquote\\\"\"\n_:b0\t\"Blank node\"\n\t\"Missing 'x'\"\n\t\n<http://example/x>\t\n_:b1\t\"String-with-lang\"@en\n_:b1\t123"); assert_eq!(str::from_utf8(&result).unwrap(), "?x\t?literal\n<http://example/x>\t\"String\"\n<http://example/x>\t\"String-with-dquote\\\"\"\n_:b0\t\"Blank node\"\n\t\"Missing 'x'\"\n\t\n<http://example/x>\t\n_:b1\t\"String-with-lang\"@en\n_:b1\t123");

@ -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),
}
}
}

@ -1,11 +1,10 @@
//! Implementation of [SPARQL Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/) //! Implementation of [SPARQL Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/)
use crate::io::read::{ParserError, SyntaxError}; use crate::error::{ParseError, SyntaxError};
use crate::model::vocab::rdf;
use crate::model::*;
use crate::sparql::error::EvaluationError;
use json_event_parser::{JsonEvent, JsonReader, JsonWriter}; use json_event_parser::{JsonEvent, JsonReader, JsonWriter};
use oxrdf::vocab::rdf;
use oxrdf::Variable; use oxrdf::Variable;
use oxrdf::*;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::io::{self, BufRead, Write}; use std::io::{self, BufRead, Write};
@ -23,18 +22,17 @@ pub fn write_boolean_json_result<W: Write>(sink: W, value: bool) -> io::Result<W
pub struct JsonSolutionsWriter<W: Write> { pub struct JsonSolutionsWriter<W: Write> {
writer: JsonWriter<W>, writer: JsonWriter<W>,
variables: Vec<Variable>,
} }
impl<W: Write> JsonSolutionsWriter<W> { impl<W: Write> JsonSolutionsWriter<W> {
pub fn start(sink: W, variables: &[Variable]) -> io::Result<Self> { pub fn start(sink: W, variables: Vec<Variable>) -> io::Result<Self> {
let mut writer = JsonWriter::from_writer(sink); let mut writer = JsonWriter::from_writer(sink);
writer.write_event(JsonEvent::StartObject)?; writer.write_event(JsonEvent::StartObject)?;
writer.write_event(JsonEvent::ObjectKey("head"))?; writer.write_event(JsonEvent::ObjectKey("head"))?;
writer.write_event(JsonEvent::StartObject)?; writer.write_event(JsonEvent::StartObject)?;
writer.write_event(JsonEvent::ObjectKey("vars"))?; writer.write_event(JsonEvent::ObjectKey("vars"))?;
writer.write_event(JsonEvent::StartArray)?; writer.write_event(JsonEvent::StartArray)?;
for variable in variables { for variable in &variables {
writer.write_event(JsonEvent::String(variable.as_str()))?; writer.write_event(JsonEvent::String(variable.as_str()))?;
} }
writer.write_event(JsonEvent::EndArray)?; writer.write_event(JsonEvent::EndArray)?;
@ -43,23 +41,18 @@ impl<W: Write> JsonSolutionsWriter<W> {
writer.write_event(JsonEvent::StartObject)?; writer.write_event(JsonEvent::StartObject)?;
writer.write_event(JsonEvent::ObjectKey("bindings"))?; writer.write_event(JsonEvent::ObjectKey("bindings"))?;
writer.write_event(JsonEvent::StartArray)?; writer.write_event(JsonEvent::StartArray)?;
Ok(Self { Ok(Self { writer })
writer,
variables: variables.to_vec(),
})
} }
pub fn write<'a>( pub fn write<'a>(
&mut self, &mut self,
solution: impl IntoIterator<Item = Option<TermRef<'a>>>, solution: impl IntoIterator<Item = (&'a Variable, &'a Term)>,
) -> io::Result<()> { ) -> io::Result<()> {
self.writer.write_event(JsonEvent::StartObject)?; self.writer.write_event(JsonEvent::StartObject)?;
for (value, variable) in solution.into_iter().zip(&self.variables) { for (variable, value) in solution {
if let Some(value) = value {
self.writer self.writer
.write_event(JsonEvent::ObjectKey(variable.as_str()))?; .write_event(JsonEvent::ObjectKey(variable.as_str()))?;
write_json_term(value, &mut self.writer)?; write_json_term(value.as_ref(), &mut self.writer)?;
}
} }
self.writer.write_event(JsonEvent::EndObject)?; self.writer.write_event(JsonEvent::EndObject)?;
Ok(()) Ok(())
@ -73,10 +66,7 @@ impl<W: Write> JsonSolutionsWriter<W> {
} }
} }
fn write_json_term( fn write_json_term(term: TermRef<'_>, writer: &mut JsonWriter<impl Write>) -> io::Result<()> {
term: TermRef<'_>,
writer: &mut JsonWriter<impl Write>,
) -> Result<(), EvaluationError> {
match term { match term {
TermRef::NamedNode(uri) => { TermRef::NamedNode(uri) => {
writer.write_event(JsonEvent::StartObject)?; writer.write_event(JsonEvent::StartObject)?;
@ -109,6 +99,7 @@ fn write_json_term(
} }
writer.write_event(JsonEvent::EndObject)?; writer.write_event(JsonEvent::EndObject)?;
} }
#[cfg(feature = "rdf-star")]
TermRef::Triple(triple) => { TermRef::Triple(triple) => {
writer.write_event(JsonEvent::StartObject)?; writer.write_event(JsonEvent::StartObject)?;
writer.write_event(JsonEvent::ObjectKey("type"))?; writer.write_event(JsonEvent::ObjectKey("type"))?;
@ -137,7 +128,7 @@ pub enum JsonQueryResultsReader<R: BufRead> {
} }
impl<R: BufRead> JsonQueryResultsReader<R> { impl<R: BufRead> JsonQueryResultsReader<R> {
pub fn read(source: R) -> Result<Self, ParserError> { pub fn read(source: R) -> Result<Self, ParseError> {
let mut reader = JsonReader::from_reader(source); let mut reader = JsonReader::from_reader(source);
let mut buffer = Vec::default(); let mut buffer = Vec::default();
let mut variables = None; let mut variables = None;
@ -234,7 +225,7 @@ pub struct JsonSolutionsReader<R: BufRead> {
} }
impl<R: BufRead> JsonSolutionsReader<R> { impl<R: BufRead> JsonSolutionsReader<R> {
pub fn read_next(&mut self) -> Result<Option<Vec<Option<Term>>>, ParserError> { pub fn read_next(&mut self) -> Result<Option<Vec<Option<Term>>>, ParseError> {
let mut new_bindings = vec![None; self.mapping.len()]; let mut new_bindings = vec![None; self.mapping.len()];
loop { loop {
match self.reader.read_event(&mut self.buffer)? { match self.reader.read_event(&mut self.buffer)? {
@ -255,11 +246,12 @@ impl<R: BufRead> JsonSolutionsReader<R> {
} }
} }
fn read_value(&mut self) -> Result<Term, ParserError> { fn read_value(&mut self) -> Result<Term, ParseError> {
enum Type { enum Type {
Uri, Uri,
BNode, BNode,
Literal, Literal,
#[cfg(feature = "rdf-star")]
Triple, Triple,
} }
#[derive(Eq, PartialEq)] #[derive(Eq, PartialEq)]
@ -312,6 +304,7 @@ impl<R: BufRead> JsonSolutionsReader<R> {
"uri" => t = Some(Type::Uri), "uri" => t = Some(Type::Uri),
"bnode" => t = Some(Type::BNode), "bnode" => t = Some(Type::BNode),
"literal" => t = Some(Type::Literal), "literal" => t = Some(Type::Literal),
#[cfg(feature = "rdf-star")]
"triple" => t = Some(Type::Triple), "triple" => t = Some(Type::Triple),
_ => { _ => {
return Err(SyntaxError::msg(format!( return Err(SyntaxError::msg(format!(
@ -393,6 +386,7 @@ impl<R: BufRead> JsonSolutionsReader<R> {
} }
.into()) .into())
} }
#[cfg(feature = "rdf-star")]
Some(Type::Triple) => Ok(Triple::new( Some(Type::Triple) => Ok(Triple::new(
match subject.ok_or_else(|| { match subject.ok_or_else(|| {
SyntaxError::msg( SyntaxError::msg(
@ -441,7 +435,7 @@ impl<R: BufRead> JsonSolutionsReader<R> {
fn read_head<R: BufRead>( fn read_head<R: BufRead>(
reader: &mut JsonReader<R>, reader: &mut JsonReader<R>,
buffer: &mut Vec<u8>, buffer: &mut Vec<u8>,
) -> Result<Vec<String>, ParserError> { ) -> Result<Vec<String>, ParseError> {
if reader.read_event(buffer)? != JsonEvent::StartObject { if reader.read_event(buffer)? != JsonEvent::StartObject {
return Err(SyntaxError::msg("head should be an object").into()); return Err(SyntaxError::msg("head should be an object").into());
} }
@ -468,7 +462,7 @@ fn read_head<R: BufRead>(
fn read_string_array<R: BufRead>( fn read_string_array<R: BufRead>(
reader: &mut JsonReader<R>, reader: &mut JsonReader<R>,
buffer: &mut Vec<u8>, buffer: &mut Vec<u8>,
) -> Result<Vec<String>, ParserError> { ) -> Result<Vec<String>, ParseError> {
if reader.read_event(buffer)? != JsonEvent::StartArray { if reader.read_event(buffer)? != JsonEvent::StartArray {
return Err(SyntaxError::msg("Variable list should be an array").into()); return Err(SyntaxError::msg("Variable list should be an array").into());
} }
@ -483,221 +477,3 @@ fn read_string_array<R: BufRead>(
} }
} }
} }
struct ResultsIterator<R: BufRead> {
reader: JsonReader<R>,
buffer: Vec<u8>,
mapping: BTreeMap<String, usize>,
}
impl<R: BufRead> Iterator for ResultsIterator<R> {
type Item = Result<Vec<Option<Term>>, EvaluationError>;
fn next(&mut self) -> Option<Result<Vec<Option<Term>>, EvaluationError>> {
self.read_next().map_err(EvaluationError::from).transpose()
}
}
impl<R: BufRead> ResultsIterator<R> {
fn read_next(&mut self) -> Result<Option<Vec<Option<Term>>>, ParserError> {
let mut new_bindings = vec![None; self.mapping.len()];
loop {
match self.reader.read_event(&mut self.buffer)? {
JsonEvent::StartObject => (),
JsonEvent::EndObject => return Ok(Some(new_bindings)),
JsonEvent::EndArray | JsonEvent::Eof => return Ok(None),
JsonEvent::ObjectKey(key) => {
let k = *self.mapping.get(key).ok_or_else(|| {
SyntaxError::msg(format!(
"The variable {} has not been defined in the header",
key
))
})?;
new_bindings[k] = Some(self.read_value()?)
}
_ => return Err(SyntaxError::msg("Invalid result serialization").into()),
}
}
}
fn read_value(&mut self) -> Result<Term, ParserError> {
enum Type {
Uri,
BNode,
Literal,
Triple,
}
#[derive(Eq, PartialEq)]
enum State {
Type,
Value,
Lang,
Datatype,
}
let mut state = None;
let mut t = None;
let mut value = None;
let mut lang = None;
let mut datatype = None;
let mut subject = None;
let mut predicate = None;
let mut object = None;
if self.reader.read_event(&mut self.buffer)? != JsonEvent::StartObject {
return Err(SyntaxError::msg("Term serializations should be an object").into());
}
loop {
match self.reader.read_event(&mut self.buffer)? {
JsonEvent::ObjectKey(key) => match key {
"type" => state = Some(State::Type),
"value" => state = Some(State::Value),
"xml:lang" => state = Some(State::Lang),
"datatype" => state = Some(State::Datatype),
"subject" => subject = Some(self.read_value()?),
"predicate" => predicate = Some(self.read_value()?),
"object" => object = Some(self.read_value()?),
_ => {
return Err(SyntaxError::msg(format!(
"Unexpected key in term serialization: '{}'",
key
))
.into())
}
},
JsonEvent::StartObject => {
if state != Some(State::Value) {
return Err(SyntaxError::msg(
"Unexpected nested object in term serialization",
)
.into());
}
}
JsonEvent::String(s) => match state {
Some(State::Type) => {
match s {
"uri" => t = Some(Type::Uri),
"bnode" => t = Some(Type::BNode),
"literal" => t = Some(Type::Literal),
"triple" => t = Some(Type::Triple),
_ => {
return Err(SyntaxError::msg(format!(
"Unexpected term type: '{}'",
s
))
.into())
}
};
state = None;
}
Some(State::Value) => {
value = Some(s.to_owned());
state = None;
}
Some(State::Lang) => {
lang = Some(s.to_owned());
state = None;
}
Some(State::Datatype) => {
datatype = Some(NamedNode::new(s).map_err(|e| {
SyntaxError::msg(format!("Invalid datatype value: {}", e))
})?);
state = None;
}
_ => (), // impossible
},
JsonEvent::EndObject => {
if let Some(s) = state {
if s == State::Value {
state = None; //End of triple
} else {
return Err(SyntaxError::msg(
"Term description values should be string",
)
.into());
}
} else {
return match t {
None => Err(SyntaxError::msg(
"Term serialization should have a 'type' key",
)
.into()),
Some(Type::Uri) => Ok(NamedNode::new(value.ok_or_else(|| {
SyntaxError::msg("uri serialization should have a 'value' key")
})?)
.map_err(|e| SyntaxError::msg(format!("Invalid uri value: {}", e)))?
.into()),
Some(Type::BNode) => Ok(BlankNode::new(value.ok_or_else(|| {
SyntaxError::msg("bnode serialization should have a 'value' key")
})?)
.map_err(|e| SyntaxError::msg(format!("Invalid bnode value: {}", e)))?
.into()),
Some(Type::Literal) => {
let value = value.ok_or_else(|| {
SyntaxError::msg(
"literal serialization should have a 'value' key",
)
})?;
Ok(match lang {
Some(lang) => {
if let Some(datatype) = datatype {
if datatype.as_ref() != rdf::LANG_STRING {
return Err(SyntaxError::msg(format!(
"xml:lang value '{}' provided with the datatype {}",
lang, datatype
)).into())
}
}
Literal::new_language_tagged_literal(value, &lang).map_err(|e| {
SyntaxError::msg(format!("Invalid xml:lang value '{}': {}", lang, e))
})?
}
None => if let Some(datatype) = datatype {
Literal::new_typed_literal(value, datatype)
} else {
Literal::new_simple_literal(value)
}
}
.into())
}
Some(Type::Triple) => Ok(Triple::new(
match subject.ok_or_else(|| {
SyntaxError::msg(
"triple serialization should have a 'subject' key",
)
})? {
Term::NamedNode(subject) => subject.into(),
Term::BlankNode(subject) => subject.into(),
Term::Triple(subject) => Subject::Triple(subject),
Term::Literal(_) => {
return Err(SyntaxError::msg(
"The 'subject' value should not be a literal",
)
.into())
}
},
match predicate.ok_or_else(|| {
SyntaxError::msg(
"triple serialization should have a 'predicate' key",
)
})? {
Term::NamedNode(predicate) => predicate,
_ => {
return Err(SyntaxError::msg(
"The 'predicate' value should be a uri",
)
.into())
}
},
object.ok_or_else(|| {
SyntaxError::msg(
"triple serialization should have a 'object' key",
)
})?,
)
.into()),
};
}
}
_ => return Err(SyntaxError::msg("Invalid term serialization").into()),
}
}
}
}

@ -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,9 +1,9 @@
//! Implementation of [SPARQL Query Results XML Format](http://www.w3.org/TR/rdf-sparql-XMLres/) //! Implementation of [SPARQL Query Results XML Format](http://www.w3.org/TR/rdf-sparql-XMLres/)
use crate::io::read::{ParserError, SyntaxError}; use crate::error::{ParseError, SyntaxError};
use crate::model::vocab::rdf; use oxrdf::vocab::rdf;
use crate::model::*;
use oxrdf::Variable; use oxrdf::Variable;
use oxrdf::*;
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event}; use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
use quick_xml::Reader; use quick_xml::Reader;
use quick_xml::Writer; use quick_xml::Writer;
@ -35,57 +35,51 @@ fn do_write_boolean_xml_result<W: Write>(sink: W, value: bool) -> Result<W, quic
pub struct XmlSolutionsWriter<W: Write> { pub struct XmlSolutionsWriter<W: Write> {
writer: Writer<W>, writer: Writer<W>,
variables: Vec<Variable>,
} }
impl<W: Write> XmlSolutionsWriter<W> { impl<W: Write> XmlSolutionsWriter<W> {
pub fn start(sink: W, variables: &[Variable]) -> io::Result<Self> { pub fn start(sink: W, variables: Vec<Variable>) -> io::Result<Self> {
Self::do_start(sink, variables).map_err(map_xml_error) Self::do_start(sink, variables).map_err(map_xml_error)
} }
fn do_start(sink: W, variables: &[Variable]) -> Result<Self, quick_xml::Error> { fn do_start(sink: W, variables: Vec<Variable>) -> Result<Self, quick_xml::Error> {
let mut writer = Writer::new(sink); let mut writer = Writer::new(sink);
writer.write_event(Event::Decl(BytesDecl::new(b"1.0", None, None)))?; writer.write_event(Event::Decl(BytesDecl::new(b"1.0", None, None)))?;
let mut sparql_open = BytesStart::borrowed_name(b"sparql"); let mut sparql_open = BytesStart::borrowed_name(b"sparql");
sparql_open.push_attribute(("xmlns", "http://www.w3.org/2005/sparql-results#")); sparql_open.push_attribute(("xmlns", "http://www.w3.org/2005/sparql-results#"));
writer.write_event(Event::Start(sparql_open))?; writer.write_event(Event::Start(sparql_open))?;
writer.write_event(Event::Start(BytesStart::borrowed_name(b"head")))?; writer.write_event(Event::Start(BytesStart::borrowed_name(b"head")))?;
for variable in variables { for variable in &variables {
let mut variable_tag = BytesStart::borrowed_name(b"variable"); let mut variable_tag = BytesStart::borrowed_name(b"variable");
variable_tag.push_attribute(("name", variable.as_str())); variable_tag.push_attribute(("name", variable.as_str()));
writer.write_event(Event::Empty(variable_tag))?; writer.write_event(Event::Empty(variable_tag))?;
} }
writer.write_event(Event::End(BytesEnd::borrowed(b"head")))?; writer.write_event(Event::End(BytesEnd::borrowed(b"head")))?;
writer.write_event(Event::Start(BytesStart::borrowed_name(b"results")))?; writer.write_event(Event::Start(BytesStart::borrowed_name(b"results")))?;
Ok(Self { Ok(Self { writer })
writer,
variables: variables.to_vec(),
})
} }
pub fn write<'a>( pub fn write<'a>(
&mut self, &mut self,
solution: impl IntoIterator<Item = Option<TermRef<'a>>>, solution: impl IntoIterator<Item = (&'a Variable, &'a Term)>,
) -> io::Result<()> { ) -> io::Result<()> {
self.do_write(solution).map_err(map_xml_error) self.do_write(solution).map_err(map_xml_error)
} }
fn do_write<'a>( fn do_write<'a>(
&mut self, &mut self,
solution: impl IntoIterator<Item = Option<TermRef<'a>>>, solution: impl IntoIterator<Item = (&'a Variable, &'a Term)>,
) -> Result<(), quick_xml::Error> { ) -> Result<(), quick_xml::Error> {
self.writer self.writer
.write_event(Event::Start(BytesStart::borrowed_name(b"result")))?; .write_event(Event::Start(BytesStart::borrowed_name(b"result")))?;
for (value, variable) in solution.into_iter().zip(&self.variables) { for (variable, value) in solution {
if let Some(value) = value {
let mut binding_tag = BytesStart::borrowed_name(b"binding"); let mut binding_tag = BytesStart::borrowed_name(b"binding");
binding_tag.push_attribute(("name", variable.as_str())); binding_tag.push_attribute(("name", variable.as_str()));
self.writer.write_event(Event::Start(binding_tag))?; self.writer.write_event(Event::Start(binding_tag))?;
write_xml_term(value, &mut self.writer)?; write_xml_term(value.as_ref(), &mut self.writer)?;
self.writer self.writer
.write_event(Event::End(BytesEnd::borrowed(b"binding")))?; .write_event(Event::End(BytesEnd::borrowed(b"binding")))?;
} }
}
self.writer self.writer
.write_event(Event::End(BytesEnd::borrowed(b"result"))) .write_event(Event::End(BytesEnd::borrowed(b"result")))
} }
@ -129,6 +123,7 @@ fn write_xml_term(
writer.write_event(Event::Text(BytesText::from_plain_str(literal.value())))?; writer.write_event(Event::Text(BytesText::from_plain_str(literal.value())))?;
writer.write_event(Event::End(BytesEnd::borrowed(b"literal")))?; writer.write_event(Event::End(BytesEnd::borrowed(b"literal")))?;
} }
#[cfg(feature = "rdf-star")]
TermRef::Triple(triple) => { TermRef::Triple(triple) => {
writer.write_event(Event::Start(BytesStart::borrowed_name(b"triple")))?; writer.write_event(Event::Start(BytesStart::borrowed_name(b"triple")))?;
writer.write_event(Event::Start(BytesStart::borrowed_name(b"subject")))?; writer.write_event(Event::Start(BytesStart::borrowed_name(b"subject")))?;
@ -155,7 +150,7 @@ pub enum XmlQueryResultsReader<R: BufRead> {
} }
impl<R: BufRead> XmlQueryResultsReader<R> { impl<R: BufRead> XmlQueryResultsReader<R> {
pub fn read(source: R) -> Result<Self, ParserError> { pub fn read(source: R) -> Result<Self, ParseError> {
enum State { enum State {
Start, Start,
Sparql, Sparql,
@ -301,7 +296,7 @@ pub struct XmlSolutionsReader<R: BufRead> {
} }
impl<R: BufRead> XmlSolutionsReader<R> { impl<R: BufRead> XmlSolutionsReader<R> {
pub fn read_next(&mut self) -> Result<Option<Vec<Option<Term>>>, ParserError> { pub fn read_next(&mut self) -> Result<Option<Vec<Option<Term>>>, ParseError> {
let mut state = State::Start; let mut state = State::Start;
let mut new_bindings = vec![None; self.mapping.len()]; let mut new_bindings = vec![None; self.mapping.len()];
@ -513,6 +508,7 @@ impl<R: BufRead> XmlSolutionsReader<R> {
state = self.stack.pop().unwrap(); state = self.stack.pop().unwrap();
} }
State::Triple => { State::Triple => {
#[cfg(feature = "rdf-star")]
if let (Some(subject), Some(predicate), Some(object)) = ( if let (Some(subject), Some(predicate), Some(object)) = (
self.subject_stack.pop(), self.subject_stack.pop(),
self.predicate_stack.pop(), self.predicate_stack.pop(),
@ -550,6 +546,13 @@ impl<R: BufRead> XmlSolutionsReader<R> {
SyntaxError::msg("A <triple> should contain a <subject>, a <predicate> and an <object>").into() SyntaxError::msg("A <triple> should contain a <subject>, a <predicate> and an <object>").into()
); );
} }
#[cfg(not(feature = "rdf-star"))]
{
return Err(SyntaxError::msg(
"The <triple> tag is only supported with RDF-star",
)
.into());
}
} }
State::End => (), State::End => (),
}, },
@ -564,7 +567,7 @@ fn build_literal(
value: impl Into<String>, value: impl Into<String>,
lang: Option<String>, lang: Option<String>,
datatype: Option<NamedNode>, datatype: Option<NamedNode>,
) -> Result<Literal, ParserError> { ) -> Result<Literal, ParseError> {
match lang { match lang {
Some(lang) => { Some(lang) => {
if let Some(datatype) = datatype { if let Some(datatype) = datatype {

@ -471,18 +471,7 @@ impl From<ParserError> for io::Error {
} }
} }
impl From<quick_xml::Error> for ParserError { /// An error in the syntax of the parsed file.
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)] #[derive(Debug)]
pub struct SyntaxError { pub struct SyntaxError {
pub(crate) inner: SyntaxErrorKind, pub(crate) inner: SyntaxErrorKind,
@ -493,18 +482,7 @@ pub(crate) enum SyntaxErrorKind {
Turtle(TurtleError), Turtle(TurtleError),
RdfXml(RdfXmlError), RdfXml(RdfXmlError),
InvalidBaseIri { iri: String, error: IriParseError }, InvalidBaseIri { iri: String, error: IriParseError },
Xml(quick_xml::Error),
Term(TermParseError), Term(TermParseError),
Msg { msg: String },
}
impl SyntaxError {
/// Builds an error from a printable error message.
pub(crate) fn msg(msg: impl Into<String>) -> Self {
Self {
inner: SyntaxErrorKind::Msg { msg: msg.into() },
}
}
} }
impl fmt::Display for SyntaxError { impl fmt::Display for SyntaxError {
@ -515,9 +493,7 @@ impl fmt::Display for SyntaxError {
SyntaxErrorKind::InvalidBaseIri { iri, error } => { SyntaxErrorKind::InvalidBaseIri { iri, error } => {
write!(f, "Invalid base IRI '{}': {}", iri, error) write!(f, "Invalid base IRI '{}': {}", iri, error)
} }
SyntaxErrorKind::Xml(e) => e.fmt(f),
SyntaxErrorKind::Term(e) => e.fmt(f), SyntaxErrorKind::Term(e) => e.fmt(f),
SyntaxErrorKind::Msg { msg } => f.write_str(msg),
} }
} }
} }
@ -527,9 +503,8 @@ impl Error for SyntaxError {
match &self.inner { match &self.inner {
SyntaxErrorKind::Turtle(e) => Some(e), SyntaxErrorKind::Turtle(e) => Some(e),
SyntaxErrorKind::RdfXml(e) => Some(e), SyntaxErrorKind::RdfXml(e) => Some(e),
SyntaxErrorKind::Xml(e) => Some(e),
SyntaxErrorKind::Term(e) => Some(e), SyntaxErrorKind::Term(e) => Some(e),
SyntaxErrorKind::InvalidBaseIri { .. } | SyntaxErrorKind::Msg { .. } => None, SyntaxErrorKind::InvalidBaseIri { .. } => None,
} }
} }
} }
@ -543,15 +518,7 @@ impl From<SyntaxError> for io::Error {
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
format!("Invalid IRI '{}': {}", iri, error), format!("Invalid IRI '{}': {}", iri, error),
), ),
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::Term(error) => Self::new(io::ErrorKind::InvalidData, error),
SyntaxErrorKind::Msg { msg } => Self::new(io::ErrorKind::InvalidData, msg),
} }
} }
} }

@ -1,5 +1,4 @@
use crate::io::read::ParserError; use crate::io::read::ParserError;
use crate::sparql::ParseError;
use crate::storage::StorageError; use crate::storage::StorageError;
use std::convert::Infallible; use std::convert::Infallible;
use std::error; use std::error;
@ -11,11 +10,13 @@ use std::io;
#[non_exhaustive] #[non_exhaustive]
pub enum EvaluationError { pub enum EvaluationError {
/// An error in SPARQL parsing /// An error in SPARQL parsing
Parsing(ParseError), Parsing(spargebra::ParseError),
/// An error from the storage /// An error from the storage
Storage(StorageError), Storage(StorageError),
/// An error while parsing an external RDF file /// An error while parsing an external RDF file
ExternalParser(ParserError), ExternalParser(ParserError),
/// An error while parsing an external result file (likely from a federated query)
ResultsParsing(sparesults::ParseError),
/// An error returned during store IOs or during results write /// An error returned during store IOs or during results write
Io(io::Error), Io(io::Error),
/// An error returned during the query evaluation itself /// An error returned during the query evaluation itself
@ -39,6 +40,7 @@ impl fmt::Display for EvaluationError {
Self::Parsing(error) => error.fmt(f), Self::Parsing(error) => error.fmt(f),
Self::Storage(error) => error.fmt(f), Self::Storage(error) => error.fmt(f),
Self::ExternalParser(error) => error.fmt(f), Self::ExternalParser(error) => error.fmt(f),
Self::ResultsParsing(error) => error.fmt(f),
Self::Io(error) => error.fmt(f), Self::Io(error) => error.fmt(f),
Self::Query(error) => error.fmt(f), Self::Query(error) => error.fmt(f),
} }
@ -60,6 +62,7 @@ impl error::Error for EvaluationError {
Self::Parsing(e) => Some(e), Self::Parsing(e) => Some(e),
Self::Storage(e) => Some(e), Self::Storage(e) => Some(e),
Self::ExternalParser(e) => Some(e), Self::ExternalParser(e) => Some(e),
Self::ResultsParsing(e) => Some(e),
Self::Io(e) => Some(e), Self::Io(e) => Some(e),
Self::Query(e) => Some(e), Self::Query(e) => Some(e),
} }
@ -97,8 +100,8 @@ impl From<Infallible> for EvaluationError {
} }
} }
impl From<ParseError> for EvaluationError { impl From<spargebra::ParseError> for EvaluationError {
fn from(error: ParseError) -> Self { fn from(error: spargebra::ParseError) -> Self {
Self::Parsing(error) Self::Parsing(error)
} }
} }
@ -121,11 +124,18 @@ impl From<ParserError> for EvaluationError {
} }
} }
impl From<sparesults::ParseError> for EvaluationError {
fn from(error: sparesults::ParseError) -> Self {
Self::ResultsParsing(error)
}
}
impl From<EvaluationError> for io::Error { impl From<EvaluationError> for io::Error {
fn from(error: EvaluationError) -> Self { fn from(error: EvaluationError) -> Self {
match error { match error {
EvaluationError::Parsing(error) => Self::new(io::ErrorKind::InvalidData, error), EvaluationError::Parsing(error) => Self::new(io::ErrorKind::InvalidData, error),
EvaluationError::ExternalParser(error) => error.into(), EvaluationError::ExternalParser(error) => error.into(),
EvaluationError::ResultsParsing(error) => error.into(),
EvaluationError::Io(error) => error, EvaluationError::Io(error) => error,
EvaluationError::Storage(error) => error.into(), EvaluationError::Storage(error) => error.into(),
EvaluationError::Query(error) => Self::new(io::ErrorKind::Other, error), EvaluationError::Query(error) => Self::new(io::ErrorKind::Other, error),

@ -2330,7 +2330,7 @@ fn encode_bindings(
put_variable_value( put_variable_value(
variable, variable,
&variables, &variables,
dataset.encode_term(term.as_ref()), dataset.encode_term(term),
&mut encoded_terms, &mut encoded_terms,
) )
} }

@ -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(())
}
}

@ -7,7 +7,6 @@ mod dataset;
mod error; mod error;
mod eval; mod eval;
mod http; mod http;
pub mod io;
mod model; mod model;
mod plan; mod plan;
mod plan_builder; mod plan_builder;
@ -19,7 +18,6 @@ pub use crate::sparql::algebra::{Query, Update};
use crate::sparql::dataset::DatasetView; use crate::sparql::dataset::DatasetView;
pub use crate::sparql::error::EvaluationError; pub use crate::sparql::error::EvaluationError;
use crate::sparql::eval::SimpleEvaluator; use crate::sparql::eval::SimpleEvaluator;
pub use crate::sparql::io::QueryResultsFormat;
pub use crate::sparql::model::{QueryResults, QuerySolution, QuerySolutionIter, QueryTripleIter}; pub use crate::sparql::model::{QueryResults, QuerySolution, QuerySolutionIter, QueryTripleIter};
use crate::sparql::plan_builder::PlanBuilder; use crate::sparql::plan_builder::PlanBuilder;
pub use crate::sparql::service::ServiceHandler; pub use crate::sparql::service::ServiceHandler;
@ -27,6 +25,7 @@ use crate::sparql::service::{EmptyServiceHandler, ErrorConversionServiceHandler}
pub(crate) use crate::sparql::update::evaluate_update; pub(crate) use crate::sparql::update::evaluate_update;
use crate::storage::Storage; use crate::storage::Storage;
pub use oxrdf::{Variable, VariableNameParseError}; pub use oxrdf::{Variable, VariableNameParseError};
pub use sparesults::QueryResultsFormat;
pub use spargebra::ParseError; pub use spargebra::ParseError;
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;

@ -2,8 +2,12 @@ use crate::io::GraphFormat;
use crate::io::GraphSerializer; use crate::io::GraphSerializer;
use crate::model::*; use crate::model::*;
use crate::sparql::error::EvaluationError; use crate::sparql::error::EvaluationError;
use crate::sparql::io::{QueryResultsFormat, QueryResultsParser, QueryResultsSerializer};
use oxrdf::Variable; use oxrdf::Variable;
pub use sparesults::QuerySolution;
use sparesults::{
QueryResultsFormat, QueryResultsParser, QueryResultsReader, QueryResultsSerializer,
SolutionsReader,
};
use std::io::{self, BufRead, Write}; use std::io::{self, BufRead, Write};
use std::rc::Rc; use std::rc::Rc;
@ -54,32 +58,25 @@ impl QueryResults {
serializer.write_boolean_result(writer, value)?; serializer.write_boolean_result(writer, value)?;
} }
QueryResults::Solutions(solutions) => { QueryResults::Solutions(solutions) => {
let mut writer = serializer.solutions_writer(writer, solutions.variables())?; let mut writer =
serializer.solutions_writer(writer, solutions.variables().to_vec())?;
for solution in solutions { for solution in solutions {
writer.write( writer.write(&solution?)?;
solution?
.values
.iter()
.map(|t| t.as_ref().map(|t| t.as_ref())),
)?;
} }
writer.finish()?; writer.finish()?;
} }
QueryResults::Graph(triples) => { QueryResults::Graph(triples) => {
let mut writer = serializer.solutions_writer( let s = Variable::new_unchecked("subject");
writer, let p = Variable::new_unchecked("predicate");
&[ let o = Variable::new_unchecked("object");
Variable::new_unchecked("subject"), let mut writer =
Variable::new_unchecked("predicate"), serializer.solutions_writer(writer, vec![s.clone(), p.clone(), o.clone()])?;
Variable::new_unchecked("object"),
],
)?;
for triple in triples { for triple in triples {
let triple = triple?; let triple = triple?;
writer.write([ writer.write([
Some(triple.subject.as_ref().into()), (&s, &triple.subject.into()),
Some(triple.predicate.as_ref().into()), (&p, &triple.predicate.into()),
Some(triple.object.as_ref()), (&o, &triple.object),
])?; ])?;
} }
writer.finish()?; writer.finish()?;
@ -135,6 +132,15 @@ impl From<QuerySolutionIter> for QueryResults {
} }
} }
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),
}
}
}
/// An iterator over [`QuerySolution`]s. /// An iterator over [`QuerySolution`]s.
/// ///
/// ``` /// ```
@ -151,15 +157,18 @@ impl From<QuerySolutionIter> for QueryResults {
/// ``` /// ```
pub struct QuerySolutionIter { pub struct QuerySolutionIter {
variables: Rc<Vec<Variable>>, variables: Rc<Vec<Variable>>,
iter: Box<dyn Iterator<Item = Result<Vec<Option<Term>>, EvaluationError>>>, iter: Box<dyn Iterator<Item = Result<QuerySolution, EvaluationError>>>,
} }
impl QuerySolutionIter { impl QuerySolutionIter {
pub fn new( pub fn new(
variables: Rc<Vec<Variable>>, variables: Rc<Vec<Variable>>,
iter: Box<dyn Iterator<Item = Result<Vec<Option<Term>>, EvaluationError>>>, iter: impl Iterator<Item = Result<Vec<Option<Term>>, EvaluationError>> + 'static,
) -> Self { ) -> Self {
Self { variables, iter } Self {
variables: variables.clone(),
iter: Box::new(iter.map(move |t| t.map(|values| (variables.clone(), values).into()))),
}
} }
/// The variables used in the solutions. /// The variables used in the solutions.
@ -180,15 +189,21 @@ impl QuerySolutionIter {
} }
} }
impl<R: BufRead + 'static> From<SolutionsReader<R>> for QuerySolutionIter {
fn from(reader: SolutionsReader<R>) -> Self {
Self {
variables: Rc::new(reader.variables().to_vec()),
iter: Box::new(reader.map(|t| t.map_err(EvaluationError::from))),
}
}
}
impl Iterator for QuerySolutionIter { impl Iterator for QuerySolutionIter {
type Item = Result<QuerySolution, EvaluationError>; type Item = Result<QuerySolution, EvaluationError>;
#[inline] #[inline]
fn next(&mut self) -> Option<Result<QuerySolution, EvaluationError>> { fn next(&mut self) -> Option<Result<QuerySolution, EvaluationError>> {
Some(self.iter.next()?.map(|values| QuerySolution { self.iter.next()
values,
variables: self.variables.clone(),
}))
} }
#[inline] #[inline]
@ -197,89 +212,6 @@ impl Iterator for QuerySolutionIter {
} }
} }
/// Tuple associating variables and terms that are the result of a SPARQL query.
///
/// It is the equivalent of a row in SQL.
pub struct QuerySolution {
pub(super) values: Vec<Option<Term>>,
pub(super) variables: Rc<Vec<Variable>>,
}
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`])
///
/// ```ignore
/// let foo = solution.get("foo"); // Get the value of the variable ?foo if it exists
/// let first = solution.get(1); // Get the value of the second column if it exists
/// ```
#[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
#[inline]
pub fn len(&self) -> usize {
self.values.len()
}
/// Is this binding empty?
#[inline]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
/// Returns an iterator over bound variables
#[inline]
pub fn iter(&self) -> impl Iterator<Item = (&Variable, &Term)> {
self.values
.iter()
.enumerate()
.filter_map(move |(i, value)| value.as_ref().map(|value| (&self.variables[i], value)))
}
/// Returns an iterator over all values, bound or not
#[inline]
pub fn values(&self) -> impl Iterator<Item = Option<&Term>> {
self.values.iter().map(std::option::Option::as_ref)
}
}
/// A utility trait to get values for a given variable or tuple position
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)
}
}
/// An iterator over the triples that compose a graph solution. /// An iterator over the triples that compose a graph solution.
/// ///
/// ``` /// ```

@ -129,12 +129,7 @@ impl PyQuerySolution {
fn __iter__(&self) -> SolutionValueIter { fn __iter__(&self) -> SolutionValueIter {
SolutionValueIter { SolutionValueIter {
inner: self inner: self.inner.values().to_vec().into_iter(),
.inner
.values()
.map(|v| v.cloned())
.collect::<Vec<_>>()
.into_iter(),
} }
} }
} }

Loading…
Cancel
Save