diff --git a/Cargo.lock b/Cargo.lock index 657a399a..03c1e481 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,9 +112,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.8.0" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] name = "cast" @@ -177,9 +177,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.0.0" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17bf219fcd37199b9a29e00ba65dfb8cd5b2688b7297ec14ff829c40ac50ca9" +checksum = "f6f34b09b9ee8c7c7b400fe2f8df39cafc9538b03d6ba7f4ae13e4cb90bfbb7d" dependencies = [ "atty", "bitflags", @@ -194,9 +194,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.0.0" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b9752c030a14235a0bd5ef3ad60a1dcac8468c30921327fc8af36b20c790b9" +checksum = "41a0645a430ec9136d2d701e54a95d557de12649a9dd7109ced3187e648ac824" dependencies = [ "heck", "proc-macro-error", @@ -278,9 +278,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" dependencies = [ "cfg-if", "crossbeam-utils", @@ -299,9 +299,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" +checksum = "97242a70df9b89a65d0b6df3c4bf5b9ce03c5b7309019777fbde37e7537f8762" dependencies = [ "cfg-if", "crossbeam-utils", @@ -312,9 +312,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120" dependencies = [ "cfg-if", "lazy_static", @@ -391,6 +391,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "fastrand" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2" +dependencies = [ + "instant", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -418,9 +427,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", "version_check", @@ -471,12 +480,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" [[package]] name = "hermit-abi" @@ -518,9 +524,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" dependencies = [ "autocfg", "hashbrown", @@ -835,7 +841,6 @@ dependencies = [ "getrandom", "hex", "js-sys", - "json-event-parser", "lasso", "lazy_static", "libc", @@ -847,7 +852,6 @@ dependencies = [ "oxiri", "oxrdf", "oxrocksdb-sys", - "quick-xml", "rand", "regex", "rio_api", @@ -857,6 +861,7 @@ dependencies = [ "sha2", "siphasher", "sophia_api", + "sparesults", "spargebra", "wasm-bindgen-test", "zstd", @@ -877,7 +882,7 @@ dependencies = [ name = "oxigraph_server" version = "0.3.0-dev" dependencies = [ - "clap 3.0.0", + "clap 3.0.5", "oxhttp", "oxigraph", "oxiri", @@ -890,7 +895,7 @@ name = "oxigraph_testsuite" version = "0.3.0-dev" dependencies = [ "anyhow", - "clap 3.0.0", + "clap 3.0.5", "criterion", "oxigraph", "text-diff", @@ -1448,9 +1453,9 @@ checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" [[package]] name = "serde" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008" +checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" [[package]] name = "serde_cbor" @@ -1464,9 +1469,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276" +checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" dependencies = [ "proc-macro2", "quote", @@ -1475,9 +1480,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5" +checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142" dependencies = [ "itoa 1.0.1", "ryu", @@ -1497,9 +1502,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900d964dd36bb15bcf2f2b35694c072feab74969a54f2bbeec7a2d725d2bdcb6" +checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec" dependencies = [ "cfg-if", "cpufeatures", @@ -1550,6 +1555,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "sparesults" +version = "0.1.0" +dependencies = [ + "json-event-parser", + "oxrdf", + "quick-xml", +] + [[package]] name = "spargebra" version = "0.1.0" @@ -1581,9 +1595,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.84" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b" +checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" dependencies = [ "proc-macro2", "quote", @@ -1592,13 +1606,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if", + "fastrand", "libc", - "rand", "redox_syscall", "remove_dir_all", "winapi 0.3.9", @@ -1724,12 +1738,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" - [[package]] name = "unicode-width" version = "0.1.9" @@ -1967,18 +1975,18 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] 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" -checksum = "538b8347df9257b7fbce37677ef7535c00a3c7bf1f81023cc328ed7fe4b41de8" +checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54" dependencies = [ "zstd-safe", ] [[package]] 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" -checksum = "9fb4cfe2f6e6d35c5d27ecd9d256c4b6f7933c4895654917460ec56c29336cc1" +checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79" dependencies = [ "libc", "zstd-sys", diff --git a/Cargo.toml b/Cargo.toml index de1151d9..251cc2ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "lib", "lib/oxrdf", "lib/spargebra", + "lib/sparesults", "python", "rocksdb-sys", "server", diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 1c14f1be..6886e3eb 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -22,7 +22,6 @@ sophia = ["sophia_api", "oxrdf/sophia_api"] http_client = ["oxhttp", "oxhttp/rustls"] [dependencies] -quick-xml = "0.22" rand = "0.8" md-5 = "0.10" sha-1 = "0.10" @@ -40,10 +39,10 @@ siphasher = "0.3" lasso = {version="0.6", features=["multi-threaded", "inline-more"]} lazy_static = "1" sophia_api = { version = "0.7", optional = true } -json-event-parser = "0.1" num_cpus = "1" oxrdf = { version = "0.1", path="oxrdf", 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] libc = "0.2" diff --git a/lib/sparesults/Cargo.toml b/lib/sparesults/Cargo.toml new file mode 100644 index 00000000..4841c59d --- /dev/null +++ b/lib/sparesults/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "sparesults" +version = "0.1.0" +authors = ["Tpt "] +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 diff --git a/lib/sparesults/README.md b/lib/sparesults/README.md new file mode 100644 index 00000000..268349c1 --- /dev/null +++ b/lib/sparesults/README.md @@ -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> { + 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 + ``) +* MIT license ([LICENSE-MIT](../LICENSE-MIT) or + ``) + +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. diff --git a/lib/src/sparql/io/csv.rs b/lib/sparesults/src/csv.rs similarity index 80% rename from lib/src/sparql/io/csv.rs rename to lib/sparesults/src/csv.rs index 2c5ef9c4..8a317801 100644 --- a/lib/src/sparql/io/csv.rs +++ b/lib/sparesults/src/csv.rs @@ -1,8 +1,8 @@ //! 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::model::{vocab::xsd, *}; +use crate::error::{ParseError, SyntaxError, SyntaxErrorKind}; use oxrdf::Variable; +use oxrdf::{vocab::xsd, *}; use std::io::{self, BufRead, Write}; use std::str::FromStr; @@ -13,12 +13,13 @@ pub fn write_boolean_csv_result(mut sink: W, value: bool) -> io::Resul pub struct CsvSolutionsWriter { sink: W, + variables: Vec, } impl CsvSolutionsWriter { - pub fn start(mut sink: W, variables: &[Variable]) -> io::Result { + pub fn start(mut sink: W, variables: Vec) -> io::Result { let mut start_vars = true; - for variable in variables { + for variable in &variables { if start_vars { start_vars = false; } else { @@ -26,16 +27,22 @@ impl CsvSolutionsWriter { } sink.write_all(variable.as_str().as_bytes())?; } - Ok(Self { sink }) + Ok(Self { sink, variables }) } pub fn write<'a>( &mut self, - solution: impl IntoIterator>>, + solution: impl IntoIterator, ) -> 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")?; let mut start_binding = true; - for value in solution { + for value in values { if start_binding { start_binding = false; } else { @@ -61,6 +68,7 @@ fn write_csv_term<'a>(term: impl Into>, sink: &mut impl Write) -> io sink.write_all(bnode.as_str().as_bytes()) } TermRef::Literal(literal) => write_escaped_csv_string(literal.value(), sink), + #[cfg(feature = "rdf-star")] TermRef::Triple(triple) => { write_csv_term(&triple.subject, sink)?; sink.write_all(b" ")?; @@ -94,12 +102,13 @@ pub fn write_boolean_tsv_result(mut sink: W, value: bool) -> io::Resul pub struct TsvSolutionsWriter { sink: W, + variables: Vec, } impl TsvSolutionsWriter { - pub fn start(mut sink: W, variables: &[Variable]) -> io::Result { + pub fn start(mut sink: W, variables: Vec) -> io::Result { let mut start_vars = true; - for variable in variables { + for variable in &variables { if start_vars { start_vars = false; } else { @@ -108,16 +117,22 @@ impl TsvSolutionsWriter { sink.write_all(b"?")?; sink.write_all(variable.as_str().as_bytes())?; } - Ok(Self { sink }) + Ok(Self { sink, variables }) } pub fn write<'a>( &mut self, - solution: impl IntoIterator>>, + solution: impl IntoIterator, ) -> 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")?; let mut start_binding = true; - for value in solution { + for value in values { if start_binding { start_binding = false; } else { @@ -155,6 +170,7 @@ fn write_tsv_term<'a>(term: impl Into>, sink: &mut impl Write) -> io } _ => sink.write_all(literal.to_string().as_bytes()), }, + #[cfg(feature = "rdf-star")] TermRef::Triple(triple) => { sink.write_all(b"<<")?; write_tsv_term(&triple.subject, sink)?; @@ -177,7 +193,7 @@ pub enum TsvQueryResultsReader { } impl TsvQueryResultsReader { - pub fn read(mut source: R) -> Result { + pub fn read(mut source: R) -> Result { let mut buffer = String::new(); // We read the header @@ -209,7 +225,7 @@ pub struct TsvSolutionsReader { } impl TsvSolutionsReader { - pub fn read_next(&mut self) -> Result>>, ParserError> { + pub fn read_next(&mut self) -> Result>>, ParseError> { self.buffer.clear(); if self.source.read_line(&mut self.buffer)? == 0 { return Ok(None); @@ -222,12 +238,12 @@ impl TsvSolutionsReader { if v.is_empty() { Ok(None) } else { - Ok(Some( - Term::from_str(v).map_err(|e| SyntaxError::msg(e.to_string()))?, - )) + Ok(Some(Term::from_str(v).map_err(|e| SyntaxError { + inner: SyntaxErrorKind::Term(e), + })?)) } }) - .collect::>()?, + .collect::>()?, )) } } @@ -235,6 +251,8 @@ impl TsvSolutionsReader { #[cfg(test)] mod tests { use super::*; + use crate::QuerySolution; + use std::rc::Rc; use std::str; fn build_example() -> (Vec, Vec>>) { @@ -283,9 +301,10 @@ mod tests { #[test] fn test_csv_serialization() -> io::Result<()> { let (variables, solutions) = build_example(); - let mut writer = CsvSolutionsWriter::start(Vec::new(), &variables)?; - for solution in &solutions { - writer.write(solution.iter().map(|t| t.as_ref().map(|t| t.as_ref())))?; + let mut writer = CsvSolutionsWriter::start(Vec::new(), variables.clone())?; + let variables = Rc::new(variables); + for solution in solutions { + writer.write(QuerySolution::from((variables.clone(), solution)).iter())?; } 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"); @@ -295,9 +314,10 @@ mod tests { #[test] fn test_tsv_serialization() -> io::Result<()> { let (variables, solutions) = build_example(); - let mut writer = TsvSolutionsWriter::start(Vec::new(), &variables)?; - for solution in &solutions { - writer.write(solution.iter().map(|t| t.as_ref().map(|t| t.as_ref())))?; + let mut writer = TsvSolutionsWriter::start(Vec::new(), variables.clone())?; + let variables = Rc::new(variables); + for solution in solutions { + writer.write(QuerySolution::from((variables.clone(), solution)).iter())?; } let result = writer.finish(); assert_eq!(str::from_utf8(&result).unwrap(), "?x\t?literal\n\t\"String\"\n\t\"String-with-dquote\\\"\"\n_:b0\t\"Blank node\"\n\t\"Missing 'x'\"\n\t\n\t\n_:b1\t\"String-with-lang\"@en\n_:b1\t123"); diff --git a/lib/sparesults/src/error.rs b/lib/sparesults/src/error.rs new file mode 100644 index 00000000..15cde64a --- /dev/null +++ b/lib/sparesults/src/error.rs @@ -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 for ParseError { + #[inline] + fn from(error: io::Error) -> Self { + Self::Io(error) + } +} + +impl From for ParseError { + #[inline] + fn from(error: SyntaxError) -> Self { + Self::Syntax(error) + } +} + +impl From for io::Error { + #[inline] + fn from(error: ParseError) -> Self { + match error { + ParseError::Io(error) => error, + ParseError::Syntax(error) => error.into(), + } + } +} + +impl From 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) -> 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 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), + } + } +} diff --git a/lib/src/sparql/io/json.rs b/lib/sparesults/src/json.rs similarity index 64% rename from lib/src/sparql/io/json.rs rename to lib/sparesults/src/json.rs index 25451c79..a80f35b7 100644 --- a/lib/src/sparql/io/json.rs +++ b/lib/sparesults/src/json.rs @@ -1,11 +1,10 @@ //! Implementation of [SPARQL Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/) -use crate::io::read::{ParserError, SyntaxError}; -use crate::model::vocab::rdf; -use crate::model::*; -use crate::sparql::error::EvaluationError; +use crate::error::{ParseError, SyntaxError}; use json_event_parser::{JsonEvent, JsonReader, JsonWriter}; +use oxrdf::vocab::rdf; use oxrdf::Variable; +use oxrdf::*; use std::collections::BTreeMap; use std::io::{self, BufRead, Write}; @@ -23,18 +22,17 @@ pub fn write_boolean_json_result(sink: W, value: bool) -> io::Result { writer: JsonWriter, - variables: Vec, } impl JsonSolutionsWriter { - pub fn start(sink: W, variables: &[Variable]) -> io::Result { + pub fn start(sink: W, variables: Vec) -> io::Result { let mut writer = JsonWriter::from_writer(sink); writer.write_event(JsonEvent::StartObject)?; writer.write_event(JsonEvent::ObjectKey("head"))?; writer.write_event(JsonEvent::StartObject)?; writer.write_event(JsonEvent::ObjectKey("vars"))?; 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::EndArray)?; @@ -43,23 +41,18 @@ impl JsonSolutionsWriter { writer.write_event(JsonEvent::StartObject)?; writer.write_event(JsonEvent::ObjectKey("bindings"))?; writer.write_event(JsonEvent::StartArray)?; - Ok(Self { - writer, - variables: variables.to_vec(), - }) + Ok(Self { writer }) } pub fn write<'a>( &mut self, - solution: impl IntoIterator>>, + solution: impl IntoIterator, ) -> io::Result<()> { self.writer.write_event(JsonEvent::StartObject)?; - for (value, variable) in solution.into_iter().zip(&self.variables) { - if let Some(value) = value { - self.writer - .write_event(JsonEvent::ObjectKey(variable.as_str()))?; - write_json_term(value, &mut self.writer)?; - } + for (variable, value) in solution { + self.writer + .write_event(JsonEvent::ObjectKey(variable.as_str()))?; + write_json_term(value.as_ref(), &mut self.writer)?; } self.writer.write_event(JsonEvent::EndObject)?; Ok(()) @@ -73,10 +66,7 @@ impl JsonSolutionsWriter { } } -fn write_json_term( - term: TermRef<'_>, - writer: &mut JsonWriter, -) -> Result<(), EvaluationError> { +fn write_json_term(term: TermRef<'_>, writer: &mut JsonWriter) -> io::Result<()> { match term { TermRef::NamedNode(uri) => { writer.write_event(JsonEvent::StartObject)?; @@ -109,6 +99,7 @@ fn write_json_term( } writer.write_event(JsonEvent::EndObject)?; } + #[cfg(feature = "rdf-star")] TermRef::Triple(triple) => { writer.write_event(JsonEvent::StartObject)?; writer.write_event(JsonEvent::ObjectKey("type"))?; @@ -137,7 +128,7 @@ pub enum JsonQueryResultsReader { } impl JsonQueryResultsReader { - pub fn read(source: R) -> Result { + pub fn read(source: R) -> Result { let mut reader = JsonReader::from_reader(source); let mut buffer = Vec::default(); let mut variables = None; @@ -234,7 +225,7 @@ pub struct JsonSolutionsReader { } impl JsonSolutionsReader { - pub fn read_next(&mut self) -> Result>>, ParserError> { + pub fn read_next(&mut self) -> Result>>, ParseError> { let mut new_bindings = vec![None; self.mapping.len()]; loop { match self.reader.read_event(&mut self.buffer)? { @@ -255,11 +246,12 @@ impl JsonSolutionsReader { } } - fn read_value(&mut self) -> Result { + fn read_value(&mut self) -> Result { enum Type { Uri, BNode, Literal, + #[cfg(feature = "rdf-star")] Triple, } #[derive(Eq, PartialEq)] @@ -312,6 +304,7 @@ impl JsonSolutionsReader { "uri" => t = Some(Type::Uri), "bnode" => t = Some(Type::BNode), "literal" => t = Some(Type::Literal), + #[cfg(feature = "rdf-star")] "triple" => t = Some(Type::Triple), _ => { return Err(SyntaxError::msg(format!( @@ -393,6 +386,7 @@ impl JsonSolutionsReader { } .into()) } + #[cfg(feature = "rdf-star")] Some(Type::Triple) => Ok(Triple::new( match subject.ok_or_else(|| { SyntaxError::msg( @@ -441,7 +435,7 @@ impl JsonSolutionsReader { fn read_head( reader: &mut JsonReader, buffer: &mut Vec, -) -> Result, ParserError> { +) -> Result, ParseError> { if reader.read_event(buffer)? != JsonEvent::StartObject { return Err(SyntaxError::msg("head should be an object").into()); } @@ -468,7 +462,7 @@ fn read_head( fn read_string_array( reader: &mut JsonReader, buffer: &mut Vec, -) -> Result, ParserError> { +) -> Result, ParseError> { if reader.read_event(buffer)? != JsonEvent::StartArray { return Err(SyntaxError::msg("Variable list should be an array").into()); } @@ -483,221 +477,3 @@ fn read_string_array( } } } - -struct ResultsIterator { - reader: JsonReader, - buffer: Vec, - mapping: BTreeMap, -} - -impl Iterator for ResultsIterator { - type Item = Result>, EvaluationError>; - - fn next(&mut self) -> Option>, EvaluationError>> { - self.read_next().map_err(EvaluationError::from).transpose() - } -} - -impl ResultsIterator { - fn read_next(&mut self) -> Result>>, 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 { - 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()), - } - } - } -} diff --git a/lib/sparesults/src/lib.rs b/lib/sparesults/src/lib.rs new file mode 100644 index 00000000..6ccf0427 --- /dev/null +++ b/lib/sparesults/src/lib.rs @@ -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 { + 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 { + 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![(&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"true".as_slice())? { + /// assert_eq!(v, true); + /// } + /// + /// // solutions + /// if let QueryResultsReader::Solutions(solutions) = json_parser.read_results(b"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![(&Variable::new_unchecked("foo"), &Literal::from("test").into())]); + /// } + /// } + /// # Result::<(),sparesults::ParseError>::Ok(()) + /// ``` + pub fn read_results(&self, reader: R) -> Result, 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![(&Variable::new_unchecked("foo"), &Literal::from("test").into())]); +/// } +/// } +/// # Result::<(),sparesults::ParseError>::Ok(()) +/// ``` +pub enum QueryResultsReader { + Solutions(SolutionsReader), + 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![(&Variable::new_unchecked("foo"), &Literal::from("test").into())]); +/// } +/// } +/// # Result::<(),sparesults::ParseError>::Ok(()) +/// ``` +pub struct SolutionsReader { + variables: Rc>, + solutions: SolutionsReaderKind, +} + +enum SolutionsReaderKind { + Xml(XmlSolutionsReader), + Json(JsonSolutionsReader), + Tsv(TsvSolutionsReader), +} + +impl SolutionsReader { + /// 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 Iterator for SolutionsReader { + type Item = Result; + + fn next(&mut self) -> Option> { + 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"true"); + /// # std::io::Result::Ok(()) + /// ``` + pub fn write_boolean_result(&self, writer: W, value: bool) -> io::Result { + 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"test"); + /// # std::io::Result::Ok(()) + /// ``` + pub fn solutions_writer( + &self, + writer: W, + variables: Vec, + ) -> io::Result> { + 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 { + formatter: SolutionsWriterKind, +} + +enum SolutionsWriterKind { + Xml(XmlSolutionsWriter), + Json(JsonSolutionsWriter), + Csv(CsvSolutionsWriter), + Tsv(TsvSolutionsWriter), +} + +impl SolutionsWriter { + /// 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, + ) -> 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 { + Ok(match self.formatter { + SolutionsWriterKind::Xml(write) => write.finish()?, + SolutionsWriterKind::Json(write) => write.finish()?, + SolutionsWriterKind::Csv(write) => write.finish(), + SolutionsWriterKind::Tsv(write) => write.finish(), + }) + } +} diff --git a/lib/sparesults/src/solution.rs b/lib/sparesults/src/solution.rs new file mode 100644 index 00000000..2b8f1c5a --- /dev/null +++ b/lib/sparesults/src/solution.rs @@ -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>, + values: Vec>, +} + +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![(&Variable::new_unchecked("foo"), &Literal::from(1).into())]); + /// ``` + #[inline] + pub fn iter(&self) -> impl Iterator { + 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] { + &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>>, S: Into>>> 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![(&Variable::new_unchecked("foo"), &Literal::from(1).into())]); +/// ``` +pub struct Iter<'a> { + inner: Zip, std::slice::Iter<'a, Option>>, +} + +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) { + (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; +} + +impl VariableSolutionIndex for usize { + #[inline] + fn index(self, _: &QuerySolution) -> Option { + Some(self) + } +} + +impl VariableSolutionIndex for &str { + #[inline] + fn index(self, solution: &QuerySolution) -> Option { + solution.variables.iter().position(|v| v.as_str() == self) + } +} + +impl VariableSolutionIndex for &Variable { + #[inline] + fn index(self, solution: &QuerySolution) -> Option { + solution.variables.iter().position(|v| v == self) + } +} + +impl VariableSolutionIndex for Variable { + #[inline] + fn index(self, solution: &QuerySolution) -> Option { + (&self).index(solution) + } +} diff --git a/lib/src/sparql/io/xml.rs b/lib/sparesults/src/xml.rs similarity index 94% rename from lib/src/sparql/io/xml.rs rename to lib/sparesults/src/xml.rs index 6125bd2f..e65e9652 100644 --- a/lib/src/sparql/io/xml.rs +++ b/lib/sparesults/src/xml.rs @@ -1,9 +1,9 @@ //! Implementation of [SPARQL Query Results XML Format](http://www.w3.org/TR/rdf-sparql-XMLres/) -use crate::io::read::{ParserError, SyntaxError}; -use crate::model::vocab::rdf; -use crate::model::*; +use crate::error::{ParseError, SyntaxError}; +use oxrdf::vocab::rdf; use oxrdf::Variable; +use oxrdf::*; use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event}; use quick_xml::Reader; use quick_xml::Writer; @@ -35,56 +35,50 @@ fn do_write_boolean_xml_result(sink: W, value: bool) -> Result { writer: Writer, - variables: Vec, } impl XmlSolutionsWriter { - pub fn start(sink: W, variables: &[Variable]) -> io::Result { + pub fn start(sink: W, variables: Vec) -> io::Result { Self::do_start(sink, variables).map_err(map_xml_error) } - fn do_start(sink: W, variables: &[Variable]) -> Result { + fn do_start(sink: W, variables: Vec) -> Result { let mut writer = Writer::new(sink); writer.write_event(Event::Decl(BytesDecl::new(b"1.0", None, None)))?; let mut sparql_open = BytesStart::borrowed_name(b"sparql"); sparql_open.push_attribute(("xmlns", "http://www.w3.org/2005/sparql-results#")); writer.write_event(Event::Start(sparql_open))?; 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"); variable_tag.push_attribute(("name", variable.as_str())); writer.write_event(Event::Empty(variable_tag))?; } writer.write_event(Event::End(BytesEnd::borrowed(b"head")))?; writer.write_event(Event::Start(BytesStart::borrowed_name(b"results")))?; - Ok(Self { - writer, - variables: variables.to_vec(), - }) + Ok(Self { writer }) } pub fn write<'a>( &mut self, - solution: impl IntoIterator>>, + solution: impl IntoIterator, ) -> io::Result<()> { self.do_write(solution).map_err(map_xml_error) } fn do_write<'a>( &mut self, - solution: impl IntoIterator>>, + solution: impl IntoIterator, ) -> Result<(), quick_xml::Error> { self.writer .write_event(Event::Start(BytesStart::borrowed_name(b"result")))?; - for (value, variable) in solution.into_iter().zip(&self.variables) { - if let Some(value) = value { - let mut binding_tag = BytesStart::borrowed_name(b"binding"); - binding_tag.push_attribute(("name", variable.as_str())); - self.writer.write_event(Event::Start(binding_tag))?; - write_xml_term(value, &mut self.writer)?; - self.writer - .write_event(Event::End(BytesEnd::borrowed(b"binding")))?; - } + for (variable, value) in solution { + let mut binding_tag = BytesStart::borrowed_name(b"binding"); + binding_tag.push_attribute(("name", variable.as_str())); + self.writer.write_event(Event::Start(binding_tag))?; + write_xml_term(value.as_ref(), &mut self.writer)?; + self.writer + .write_event(Event::End(BytesEnd::borrowed(b"binding")))?; } self.writer .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::End(BytesEnd::borrowed(b"literal")))?; } + #[cfg(feature = "rdf-star")] TermRef::Triple(triple) => { writer.write_event(Event::Start(BytesStart::borrowed_name(b"triple")))?; writer.write_event(Event::Start(BytesStart::borrowed_name(b"subject")))?; @@ -155,7 +150,7 @@ pub enum XmlQueryResultsReader { } impl XmlQueryResultsReader { - pub fn read(source: R) -> Result { + pub fn read(source: R) -> Result { enum State { Start, Sparql, @@ -301,7 +296,7 @@ pub struct XmlSolutionsReader { } impl XmlSolutionsReader { - pub fn read_next(&mut self) -> Result>>, ParserError> { + pub fn read_next(&mut self) -> Result>>, ParseError> { let mut state = State::Start; let mut new_bindings = vec![None; self.mapping.len()]; @@ -513,6 +508,7 @@ impl XmlSolutionsReader { state = self.stack.pop().unwrap(); } State::Triple => { + #[cfg(feature = "rdf-star")] if let (Some(subject), Some(predicate), Some(object)) = ( self.subject_stack.pop(), self.predicate_stack.pop(), @@ -550,6 +546,13 @@ impl XmlSolutionsReader { SyntaxError::msg("A should contain a , a and an ").into() ); } + #[cfg(not(feature = "rdf-star"))] + { + return Err(SyntaxError::msg( + "The tag is only supported with RDF-star", + ) + .into()); + } } State::End => (), }, @@ -564,7 +567,7 @@ fn build_literal( value: impl Into, lang: Option, datatype: Option, -) -> Result { +) -> Result { match lang { Some(lang) => { if let Some(datatype) = datatype { diff --git a/lib/src/io/read.rs b/lib/src/io/read.rs index c63ef06e..c457a71a 100644 --- a/lib/src/io/read.rs +++ b/lib/src/io/read.rs @@ -471,18 +471,7 @@ impl From for io::Error { } } -impl From for ParserError { - 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 +/// An error in the syntax of the parsed file. #[derive(Debug)] pub struct SyntaxError { pub(crate) inner: SyntaxErrorKind, @@ -493,18 +482,7 @@ pub(crate) enum SyntaxErrorKind { Turtle(TurtleError), RdfXml(RdfXmlError), InvalidBaseIri { iri: String, error: IriParseError }, - Xml(quick_xml::Error), Term(TermParseError), - Msg { msg: String }, -} - -impl SyntaxError { - /// Builds an error from a printable error message. - pub(crate) fn msg(msg: impl Into) -> Self { - Self { - inner: SyntaxErrorKind::Msg { msg: msg.into() }, - } - } } impl fmt::Display for SyntaxError { @@ -515,9 +493,7 @@ impl fmt::Display for SyntaxError { SyntaxErrorKind::InvalidBaseIri { iri, error } => { write!(f, "Invalid base IRI '{}': {}", iri, error) } - SyntaxErrorKind::Xml(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 { SyntaxErrorKind::Turtle(e) => Some(e), SyntaxErrorKind::RdfXml(e) => Some(e), - SyntaxErrorKind::Xml(e) => Some(e), SyntaxErrorKind::Term(e) => Some(e), - SyntaxErrorKind::InvalidBaseIri { .. } | SyntaxErrorKind::Msg { .. } => None, + SyntaxErrorKind::InvalidBaseIri { .. } => None, } } } @@ -543,15 +518,7 @@ impl From for io::Error { io::ErrorKind::InvalidInput, 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::Msg { msg } => Self::new(io::ErrorKind::InvalidData, msg), } } } diff --git a/lib/src/sparql/error.rs b/lib/src/sparql/error.rs index 71428e66..b5bb3910 100644 --- a/lib/src/sparql/error.rs +++ b/lib/src/sparql/error.rs @@ -1,5 +1,4 @@ use crate::io::read::ParserError; -use crate::sparql::ParseError; use crate::storage::StorageError; use std::convert::Infallible; use std::error; @@ -11,11 +10,13 @@ use std::io; #[non_exhaustive] pub enum EvaluationError { /// An error in SPARQL parsing - Parsing(ParseError), + Parsing(spargebra::ParseError), /// An error from the storage Storage(StorageError), /// An error while parsing an external RDF file 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 Io(io::Error), /// An error returned during the query evaluation itself @@ -39,6 +40,7 @@ impl fmt::Display for EvaluationError { Self::Parsing(error) => error.fmt(f), Self::Storage(error) => error.fmt(f), Self::ExternalParser(error) => error.fmt(f), + Self::ResultsParsing(error) => error.fmt(f), Self::Io(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::Storage(e) => Some(e), Self::ExternalParser(e) => Some(e), + Self::ResultsParsing(e) => Some(e), Self::Io(e) => Some(e), Self::Query(e) => Some(e), } @@ -97,8 +100,8 @@ impl From for EvaluationError { } } -impl From for EvaluationError { - fn from(error: ParseError) -> Self { +impl From for EvaluationError { + fn from(error: spargebra::ParseError) -> Self { Self::Parsing(error) } } @@ -121,11 +124,18 @@ impl From for EvaluationError { } } +impl From for EvaluationError { + fn from(error: sparesults::ParseError) -> Self { + Self::ResultsParsing(error) + } +} + impl From for io::Error { fn from(error: EvaluationError) -> Self { match error { EvaluationError::Parsing(error) => Self::new(io::ErrorKind::InvalidData, error), EvaluationError::ExternalParser(error) => error.into(), + EvaluationError::ResultsParsing(error) => error.into(), EvaluationError::Io(error) => error, EvaluationError::Storage(error) => error.into(), EvaluationError::Query(error) => Self::new(io::ErrorKind::Other, error), diff --git a/lib/src/sparql/eval.rs b/lib/src/sparql/eval.rs index a21717fd..9c85fca3 100644 --- a/lib/src/sparql/eval.rs +++ b/lib/src/sparql/eval.rs @@ -2330,7 +2330,7 @@ fn encode_bindings( put_variable_value( variable, &variables, - dataset.encode_term(term.as_ref()), + dataset.encode_term(term), &mut encoded_terms, ) } diff --git a/lib/src/sparql/io/mod.rs b/lib/src/sparql/io/mod.rs deleted file mode 100644 index 4c84e26e..00000000 --- a/lib/src/sparql/io/mod.rs +++ /dev/null @@ -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 { - 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 { - 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( - &self, - reader: R, - ) -> Result, 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 { - Solutions(SolutionsReader), - Boolean(bool), -} - -pub struct SolutionsReader { - variables: Rc>, - solutions: SolutionsReaderKind, -} - -enum SolutionsReaderKind { - Xml(XmlSolutionsReader), - Json(JsonSolutionsReader), - Tsv(TsvSolutionsReader), -} - -impl SolutionsReader { - #[inline] - pub fn variables(&self) -> &[Variable] { - &self.variables - } -} - -impl Iterator for SolutionsReaderKind { - type Item = Result>, ParserError>; - - fn next(&mut self) -> Option>, ParserError>> { - match self { - Self::Xml(reader) => reader.read_next(), - Self::Json(reader) => reader.read_next(), - Self::Tsv(reader) => reader.read_next(), - } - .transpose() - } -} - -impl Iterator for SolutionsReader { - type Item = Result; - - fn next(&mut self) -> Option> { - Some(self.solutions.next()?.map(|values| QuerySolution { - values, - variables: self.variables.clone(), - })) - } -} - -impl From> for QuerySolutionIter { - fn from(reader: SolutionsReader) -> Self { - Self::new( - reader.variables.clone(), - Box::new(reader.solutions.map(|r| r.map_err(EvaluationError::from))), - ) - } -} - -impl From> for QueryResults { - fn from(reader: QueryResultsReader) -> 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(&self, writer: W, value: bool) -> io::Result { - 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( - &self, - writer: W, - variables: &[Variable], - ) -> io::Result> { - 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 { - formatter: SolutionsWriterKind, -} - -enum SolutionsWriterKind { - Xml(XmlSolutionsWriter), - Json(JsonSolutionsWriter), - Csv(CsvSolutionsWriter), - Tsv(TsvSolutionsWriter), -} - -impl SolutionsWriter { - /// Writes a solution - pub fn write<'a>( - &mut self, - solution: impl IntoIterator>>, - ) -> 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(()) - } -} diff --git a/lib/src/sparql/mod.rs b/lib/src/sparql/mod.rs index 65bfb3ad..31c46275 100644 --- a/lib/src/sparql/mod.rs +++ b/lib/src/sparql/mod.rs @@ -7,7 +7,6 @@ mod dataset; mod error; mod eval; mod http; -pub mod io; mod model; mod plan; mod plan_builder; @@ -19,7 +18,6 @@ pub use crate::sparql::algebra::{Query, Update}; use crate::sparql::dataset::DatasetView; pub use crate::sparql::error::EvaluationError; use crate::sparql::eval::SimpleEvaluator; -pub use crate::sparql::io::QueryResultsFormat; pub use crate::sparql::model::{QueryResults, QuerySolution, QuerySolutionIter, QueryTripleIter}; use crate::sparql::plan_builder::PlanBuilder; pub use crate::sparql::service::ServiceHandler; @@ -27,6 +25,7 @@ use crate::sparql::service::{EmptyServiceHandler, ErrorConversionServiceHandler} pub(crate) use crate::sparql::update::evaluate_update; use crate::storage::Storage; pub use oxrdf::{Variable, VariableNameParseError}; +pub use sparesults::QueryResultsFormat; pub use spargebra::ParseError; use std::collections::HashMap; use std::rc::Rc; diff --git a/lib/src/sparql/model.rs b/lib/src/sparql/model.rs index af47adb9..ab24aad8 100644 --- a/lib/src/sparql/model.rs +++ b/lib/src/sparql/model.rs @@ -2,8 +2,12 @@ use crate::io::GraphFormat; use crate::io::GraphSerializer; use crate::model::*; use crate::sparql::error::EvaluationError; -use crate::sparql::io::{QueryResultsFormat, QueryResultsParser, QueryResultsSerializer}; use oxrdf::Variable; +pub use sparesults::QuerySolution; +use sparesults::{ + QueryResultsFormat, QueryResultsParser, QueryResultsReader, QueryResultsSerializer, + SolutionsReader, +}; use std::io::{self, BufRead, Write}; use std::rc::Rc; @@ -54,32 +58,25 @@ impl QueryResults { serializer.write_boolean_result(writer, value)?; } 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 { - writer.write( - solution? - .values - .iter() - .map(|t| t.as_ref().map(|t| t.as_ref())), - )?; + writer.write(&solution?)?; } writer.finish()?; } QueryResults::Graph(triples) => { - let mut writer = serializer.solutions_writer( - writer, - &[ - Variable::new_unchecked("subject"), - Variable::new_unchecked("predicate"), - Variable::new_unchecked("object"), - ], - )?; + let s = Variable::new_unchecked("subject"); + let p = Variable::new_unchecked("predicate"); + let o = Variable::new_unchecked("object"); + let mut writer = + serializer.solutions_writer(writer, vec![s.clone(), p.clone(), o.clone()])?; for triple in triples { let triple = triple?; writer.write([ - Some(triple.subject.as_ref().into()), - Some(triple.predicate.as_ref().into()), - Some(triple.object.as_ref()), + (&s, &triple.subject.into()), + (&p, &triple.predicate.into()), + (&o, &triple.object), ])?; } writer.finish()?; @@ -135,6 +132,15 @@ impl From for QueryResults { } } +impl From> for QueryResults { + fn from(reader: QueryResultsReader) -> Self { + match reader { + QueryResultsReader::Solutions(s) => Self::Solutions(s.into()), + QueryResultsReader::Boolean(v) => Self::Boolean(v), + } + } +} + /// An iterator over [`QuerySolution`]s. /// /// ``` @@ -151,15 +157,18 @@ impl From for QueryResults { /// ``` pub struct QuerySolutionIter { variables: Rc>, - iter: Box>, EvaluationError>>>, + iter: Box>>, } impl QuerySolutionIter { pub fn new( variables: Rc>, - iter: Box>, EvaluationError>>>, + iter: impl Iterator>, EvaluationError>> + 'static, ) -> 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. @@ -180,15 +189,21 @@ impl QuerySolutionIter { } } +impl From> for QuerySolutionIter { + fn from(reader: SolutionsReader) -> Self { + Self { + variables: Rc::new(reader.variables().to_vec()), + iter: Box::new(reader.map(|t| t.map_err(EvaluationError::from))), + } + } +} + impl Iterator for QuerySolutionIter { type Item = Result; #[inline] fn next(&mut self) -> Option> { - Some(self.iter.next()?.map(|values| QuerySolution { - values, - variables: self.variables.clone(), - })) + self.iter.next() } #[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>, - pub(super) variables: Rc>, -} - -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 { - 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> { - 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; -} - -impl VariableSolutionIndex for usize { - #[inline] - fn index(self, _: &QuerySolution) -> Option { - Some(self) - } -} - -impl VariableSolutionIndex for &str { - #[inline] - fn index(self, solution: &QuerySolution) -> Option { - solution.variables.iter().position(|v| v.as_str() == self) - } -} - -impl VariableSolutionIndex for &Variable { - #[inline] - fn index(self, solution: &QuerySolution) -> Option { - solution.variables.iter().position(|v| v == self) - } -} - -impl VariableSolutionIndex for Variable { - #[inline] - fn index(self, solution: &QuerySolution) -> Option { - (&self).index(solution) - } -} - /// An iterator over the triples that compose a graph solution. /// /// ``` diff --git a/python/src/sparql.rs b/python/src/sparql.rs index d6bb2647..12aa6e00 100644 --- a/python/src/sparql.rs +++ b/python/src/sparql.rs @@ -129,12 +129,7 @@ impl PyQuerySolution { fn __iter__(&self) -> SolutionValueIter { SolutionValueIter { - inner: self - .inner - .values() - .map(|v| v.cloned()) - .collect::>() - .into_iter(), + inner: self.inner.values().to_vec().into_iter(), } } }