SPARQL graph store API

pull/73/head
Tpt 4 years ago
parent 6aa27d4885
commit a4b09dcb3a
  1. 6
      CHANGELOG.md
  2. 12
      README.md
  3. 4
      server/Cargo.toml
  4. 622
      server/src/main.rs

@ -3,6 +3,7 @@
### Added
- [SPARQL 1.1 Update](https://www.w3.org/TR/sparql11-update/) support for Rust, Python and JavaScript. All store-like classes now provide an `update` method.
- [SPARQL 1.1 Query Results CSV and TSV Formats](https://www.w3.org/TR/sparql11-results-csv-tsv/) serializers and TSV format parser.
- [SPARQL 1.1 Graph Store HTTP Protocol](https://www.w3.org/TR/sparql11-http-rdf-update/) partial support in `oxigraph_server`. This protocol is accessible under the `/server` path.
- The SPARQL Query and Update algebra is now public.
- The stores are now "graph aware" i.e. it is possible to create and keep empty named graphs.
- A simple built-in HTTP client. In the Rust library, is disabled by default behind the `http_client` feature. It powers SPARQL federation and SPARQL UPDATE `LOAD` operations.
@ -15,12 +16,17 @@
- `(Memory|RocksDB|Sled)Store::prepare_query` methods. It is possible to cache SPARQL query parsing using the `Query::parse` function and give the parsed query to the `query` method.
### Changed
- Loading data into `oxigraph_server` is now possible using `/store` and not anymore using `/`.
For example, you should use now `curl -f -X POST -H 'Content-Type:application/n-quads' --data-binary "@MY_FILE.nq" http://localhost:7878/store` to add the N-Quads file MY_FILE.nt to the server dataset.
- Fixes evaluation of `MONTH()` and `DAY()` functions on the `xsd:date` values.
- `Variable::new` now validates the variable name.
- `(Memory|RocksDB|Sled)Store::query` does not have an option parameter anymore. There is now a new `query_opt` method that allows giving options.
- `xsd:boolean` SPARQL function now properly follows XPath specification.
- Fixes SPARQL `DESCRIBE` evaluation.
### Disk data format
The disk data format has been changed between Oxigraph 0.1 (version 0) and Oxigraph 0.2 (version 1). Data is automatically migrated from the version 0 format to the version 1 format when opened with Oxigraph 0.2.
## [0.1.1] - 2020-08-14

@ -23,7 +23,7 @@ It is split into multiple parts:
* The `python` directory contains Pyoxigraph. Pyoxigraph allows using Oxigraph in Python. See the [Pyoxigraph website](https://oxigraph.org/pyoxigraph/) for Pyoxigraph documentation. [![PyPI](https://img.shields.io/pypi/v/pyoxigraph)](https://pypi.org/project/pyoxigraph/)
* The `js` directory contains bindings to use Oxigraph in JavaScript with the help of WebAssembly. See [its README](https://github.com/oxigraph/oxigraph/blob/master/js/README.md) for the JS bindings documentation.
[![npm](https://img.shields.io/npm/v/oxigraph)](https://www.npmjs.com/package/oxigraph)
* The `server` directory contains a stand-alone binary of a web server implementing the [SPARQL 1.1 Protocol](https://www.w3.org/TR/sparql11-protocol/). It uses the [RocksDB](https://rocksdb.org/) key-value store.
* The `server` directory contains a stand-alone binary of a web server implementing the [SPARQL 1.1 Protocol](https://www.w3.org/TR/sparql11-protocol/) and the [SPARQL 1.1 Graph Store Protocol](https://www.w3.org/TR/sparql11-http-rdf-update/). It uses the [RocksDB](https://rocksdb.org/) key-value store.
[![Latest Version](https://img.shields.io/crates/v/oxigraph_server.svg)](https://crates.io/crates/oxigraph_server)
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/oxigraph/oxigraph?sort=semver)](https://hub.docker.com/repository/docker/oxigraph/oxigraph)
* The `wikibase` directory contains a stand-alone binary of a web server able to synchronize with a [Wikibase instance](https://wikiba.se/).
@ -56,14 +56,16 @@ Run `oxigraph_server -f my_data_storage_directory` to start the server where `my
The server provides an HTML UI with a form to execute SPARQL requests.
It provides the following REST actions:
* `/` allows to `POST` data to the server.
For example `curl -f -X POST -H 'Content-Type:application/n-triples' --data-binary "@MY_FILE.nt" http://localhost:7878/`
will add the N-Triples file MY_FILE.nt to the server repository. [Turtle](https://www.w3.org/TR/turtle/), [TriG](https://www.w3.org/TR/trig/), [N-Triples](https://www.w3.org/TR/n-triples/), [N-Quads](https://www.w3.org/TR/n-quads/) and [RDF XML](https://www.w3.org/TR/rdf-syntax-grammar/) are supported.
* `/query` allows to evaluate SPARQL queries against the server repository following the [SPARQL 1.1 Protocol](https://www.w3.org/TR/sparql11-protocol/#query-operation).
For example `curl -X POST -H 'Content-Type:application/sparql-query' --data 'SELECT * WHERE { ?s ?p ?o } LIMIT 10' http://localhost:7878/query`.
This action supports content negotiation and could return [Turtle](https://www.w3.org/TR/turtle/), [N-Triples](https://www.w3.org/TR/n-triples/), [RDF XML](https://www.w3.org/TR/rdf-syntax-grammar/), [SPARQL Query Results XML Format](http://www.w3.org/TR/rdf-sparql-XMLres/) and [SPARQL Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/).
* `/update` allows to execute SPARQL updates against the server repository following the [SPARQL 1.1 Protocol](https://www.w3.org/TR/sparql11-protocol/#update-operation).
For example `curl -X POST -H 'Content-Type: application/sparql-update' --data 'DELETE WHERE { <http://example.com/s> ?p ?o }' http://localhost:7878/update`.
* `/server` allows to retrieve and change the server content using the [SPARQL 1.1 Graph Store HTTP Protocol](https://www.w3.org/TR/sparql11-http-rdf-update/).
For example `curl -f -X POST -H 'Content-Type:application/n-triples' --data-binary "@MY_FILE.nt" http://localhost:7878/store?graph=http://example.com/g` will add the N-Triples file MY_FILE.nt to the server dataset inside of the `http://example.com/g` named graph.
[Turtle](https://www.w3.org/TR/turtle/), [N-Triples](https://www.w3.org/TR/n-triples/) and [RDF XML](https://www.w3.org/TR/rdf-syntax-grammar/) are supported.
It is also possible to `POST`, `PUT` and `GET` the complete RDF dataset on the server using RDF dataset formats ([TriG](https://www.w3.org/TR/trig/) and [N-Quads](https://www.w3.org/TR/n-quads/)) against the `/server` endpoint.
For example `curl -f -X POST -H 'Content-Type:application/n-quads' --data-binary "@MY_FILE.nq" http://localhost:7878/store` will add the N-Quads file MY_FILE.nt to the server dataset.
Use `oxigraph_server --help` to see the possible options when starting the server.
@ -86,7 +88,7 @@ You can then access it from your machine on port `7878`:
firefox http://localhost:7878
# Post some data
curl http://localhost:7878 -H 'Content-Type: application/x-turtle' -d@./data.ttl
curl http://localhost:7878/store?default -H 'Content-Type: text/turtle' -d@./data.ttl
# Make a query
curl -X POST -H 'Accept: application/sparql-results+json' -H 'Content-Type: application/sparql-query' --data 'SELECT * WHERE { ?s ?p ?o } LIMIT 10' http://localhost:7878/query

@ -21,4 +21,8 @@ async-std = { version = "1", features = ["attributes"] }
async-h1 = "2"
http-types = "2"
oxigraph = { version = "0.1", path="../lib", features = ["http_client"] }
rand = "0.8"
url = "2"
[dev-dependencies]
tempfile = "3"

@ -19,16 +19,17 @@ use http_types::{
bail_status, headers, Error, Method, Mime, Request, Response, Result, StatusCode,
};
use oxigraph::io::{DatasetFormat, GraphFormat};
use oxigraph::model::{GraphName, NamedNode, NamedOrBlankNode};
use oxigraph::model::{GraphName, GraphNameRef, NamedNode, NamedOrBlankNode};
use oxigraph::sparql::algebra::GraphUpdateOperation;
use oxigraph::sparql::{Query, QueryResults, QueryResultsFormat, Update};
#[cfg(feature = "rocksdb")]
use oxigraph::RocksDbStore as Store;
#[cfg(all(feature = "sled", not(feature = "rocksdb")))]
use oxigraph::SledStore as Store;
use rand::random;
use std::io::BufReader;
use std::str::FromStr;
use url::form_urlencoded;
use url::{form_urlencoded, Url};
const MAX_SPARQL_BODY_SIZE: u64 = 1_048_576;
const HTML_ROOT_PAGE: &str = include_str!("../templates/query.html");
@ -73,31 +74,6 @@ async fn handle_request(request: Request, store: Store) -> Result<Response> {
response.set_body(LOGO);
response
}
("/", Method::Post) => {
if let Some(content_type) = request.content_type() {
if let Some(format) = GraphFormat::from_media_type(content_type.essence()) {
store.load_graph(
BufReader::new(SyncAsyncReader::from(request)),
format,
&GraphName::DefaultGraph,
None,
)
} else if let Some(format) = DatasetFormat::from_media_type(content_type.essence())
{
store.load_dataset(BufReader::new(SyncAsyncReader::from(request)), format, None)
} else {
bail_status!(
415,
"No supported content Content-Type given: {}",
content_type
)
}
.map_err(bad_request)?;
Response::new(StatusCode::NoContent)
} else {
bail_status!(400, "No Content-Type given")
}
}
("/query", Method::Get) => {
configure_and_evaluate_sparql_query(store, url_query(&request), None, request)?
}
@ -165,15 +141,215 @@ async fn handle_request(request: Request, store: Store) -> Result<Response> {
bail_status!(400, "No Content-Type given")
}
}
_ => Response::new(StatusCode::NotFound),
(path, Method::Get) if path.starts_with("/store") => {
//TODO: stream
let mut body = Vec::default();
let format = if let Some(target) = store_target(&request)? {
if !match &target {
GraphName::DefaultGraph => true,
GraphName::NamedNode(target) => store.contains_named_graph(target)?,
GraphName::BlankNode(target) => store.contains_named_graph(target)?,
} {
bail_status!(404, "The graph {} does not exists", target)
}
let format = graph_content_negotiation(request)?;
store.dump_graph(&mut body, format, &target)?;
format.media_type()
} else {
let format = dataset_content_negotiation(request)?;
store.dump_dataset(&mut body, format)?;
format.media_type()
};
let mut response = Response::from(body);
response.insert_header(headers::CONTENT_TYPE, format);
response
}
(path, Method::Put) if path.starts_with("/store") => {
if let Some(content_type) = request.content_type() {
if let Some(target) = store_target(&request)? {
if let Some(format) = GraphFormat::from_media_type(content_type.essence()) {
let new = !match &target {
GraphName::NamedNode(target) => {
if store.contains_named_graph(target)? {
store.clear_graph(target)?;
true
} else {
store.insert_named_graph(target)?;
false
}
}
GraphName::BlankNode(target) => {
if store.contains_named_graph(target)? {
store.clear_graph(target)?;
true
} else {
store.insert_named_graph(target)?;
false
}
}
GraphName::DefaultGraph => {
store.clear_graph(&target)?;
true
}
};
store
.load_graph(
BufReader::new(SyncAsyncReader::from(request)),
format,
&target,
None,
)
.map_err(bad_request)?;
Response::new(if new {
StatusCode::Created
} else {
StatusCode::NoContent
})
} else {
bail_status!(
415,
"No supported content Content-Type given: {}",
content_type
)
}
} else if let Some(format) = DatasetFormat::from_media_type(content_type.essence())
{
store.clear()?;
store
.load_dataset(BufReader::new(SyncAsyncReader::from(request)), format, None)
.map_err(bad_request)?;
Response::new(StatusCode::NoContent)
} else {
bail_status!(
415,
"No supported content Content-Type given: {}",
content_type
)
}
} else {
bail_status!(400, "No Content-Type given")
}
}
(path, Method::Delete) if path.starts_with("/store") => {
if let Some(target) = store_target(&request)? {
match target {
GraphName::DefaultGraph => store.clear_graph(GraphNameRef::DefaultGraph)?,
GraphName::NamedNode(target) => {
if store.contains_named_graph(&target)? {
store.remove_named_graph(&target)?;
} else {
bail_status!(404, "The graph {} does not exists", target)
}
}
GraphName::BlankNode(target) => {
if store.contains_named_graph(&target)? {
store.remove_named_graph(&target)?;
} else {
bail_status!(404, "The graph {} does not exists", target)
}
}
}
} else {
store.clear()?;
}
Response::new(StatusCode::NoContent)
}
(path, Method::Post) if path.starts_with("/store") => {
if let Some(content_type) = request.content_type() {
if let Some(target) = store_target(&request)? {
if let Some(format) = GraphFormat::from_media_type(content_type.essence()) {
let new = !match &target {
GraphName::NamedNode(target) => store.contains_named_graph(target)?,
GraphName::BlankNode(target) => store.contains_named_graph(target)?,
GraphName::DefaultGraph => true,
};
store
.load_graph(
BufReader::new(SyncAsyncReader::from(request)),
format,
&target,
None,
)
.map_err(bad_request)?;
Response::new(if new {
StatusCode::Created
} else {
StatusCode::NoContent
})
} else {
bail_status!(
415,
"No supported content Content-Type given: {}",
content_type
)
}
} else if let Some(format) = DatasetFormat::from_media_type(content_type.essence())
{
store
.load_dataset(BufReader::new(SyncAsyncReader::from(request)), format, None)
.map_err(bad_request)?;
Response::new(StatusCode::NoContent)
} else if let Some(format) = GraphFormat::from_media_type(content_type.essence()) {
let graph = NamedNode::new(
base_url(&request)?
.join(&format!("/store/{:x}", random::<u128>()))?
.into_string(),
)?;
store
.load_graph(
BufReader::new(SyncAsyncReader::from(request)),
format,
&graph,
None,
)
.map_err(bad_request)?;
let mut response = Response::new(StatusCode::Created);
response.insert_header(headers::LOCATION, graph.into_string());
response
} else {
bail_status!(
415,
"No supported content Content-Type given: {}",
content_type
)
}
} else {
bail_status!(400, "No Content-Type given")
}
}
(path, Method::Head) if path.starts_with("/store") => {
if let Some(target) = store_target(&request)? {
if !match &target {
GraphName::DefaultGraph => true,
GraphName::NamedNode(target) => store.contains_named_graph(target)?,
GraphName::BlankNode(target) => store.contains_named_graph(target)?,
} {
bail_status!(404, "The graph {} does not exists", target)
}
Response::new(StatusCode::Ok)
} else {
Response::new(StatusCode::Ok)
}
}
_ => bail_status!(
404,
"{} {} is not supported by this server",
request.method(),
request.url().path()
),
};
response.append_header(headers::SERVER, SERVER);
Ok(response)
}
fn base_url(request: &Request) -> &str {
let url = request.url().as_str();
url.split('?').next().unwrap_or(url)
fn base_url(request: &Request) -> Result<Url> {
let mut url = request.url().clone();
if let Some(host) = request.host() {
url.set_host(Some(host)).map_err(bad_request)?;
}
url.set_query(None);
url.set_fragment(None);
Ok(url)
}
fn url_query(request: &Request) -> Vec<u8> {
@ -215,7 +391,8 @@ fn evaluate_sparql_query(
named_graph_uris: Vec<String>,
request: Request,
) -> Result<Response> {
let mut query = Query::parse(&query, Some(base_url(&request))).map_err(bad_request)?;
let mut query =
Query::parse(&query, Some(base_url(&request)?.as_str())).map_err(bad_request)?;
let default_graph_uris = default_graph_uris
.into_iter()
.map(|e| Ok(NamedNode::new(e)?.into()))
@ -297,7 +474,8 @@ fn evaluate_sparql_update(
named_graph_uris: Vec<String>,
request: Request,
) -> Result<Response> {
let mut update = Update::parse(&update, Some(base_url(&request))).map_err(bad_request)?;
let mut update =
Update::parse(&update, Some(base_url(&request)?.as_str())).map_err(bad_request)?;
let default_graph_uris = default_graph_uris
.into_iter()
.map(|e| Ok(NamedNode::new(e)?.into()))
@ -325,6 +503,47 @@ fn evaluate_sparql_update(
Ok(Response::new(StatusCode::NoContent))
}
fn store_target(request: &Request) -> Result<Option<GraphName>> {
if request.url().path() == "/store" {
let mut graph = None;
let mut default = false;
for (k, v) in form_urlencoded::parse(request.url().query().unwrap_or("").as_bytes()) {
match k.as_ref() {
"graph" => graph = Some(v.into_owned()),
"default" => default = true,
_ => bail_status!(400, "Unexpected parameter: {}", k),
}
}
Ok(if let Some(graph) = graph {
if default {
bail_status!(
400,
"Both graph and default parameters should not be set at the same time",
)
} else {
Some(
NamedNode::new(
base_url(request)?
.join(&graph)
.map_err(bad_request)?
.into_string(),
)
.map_err(bad_request)?
.into(),
)
}
} else if default {
Some(GraphName::DefaultGraph)
} else {
None
})
} else {
Ok(Some(
NamedNode::new(base_url(request)?.into_string())?.into(),
))
}
}
async fn http_server<
F: Clone + Send + Sync + 'static + Fn(Request) -> Fut,
Fut: Send + Future<Output = Result<Response>>,
@ -378,6 +597,17 @@ fn graph_content_negotiation(request: Request) -> Result<GraphFormat> {
)
}
fn dataset_content_negotiation(request: Request) -> Result<DatasetFormat> {
content_negotiation(
request,
&[
DatasetFormat::NQuads.media_type(),
DatasetFormat::TriG.media_type(),
],
DatasetFormat::from_media_type,
)
}
fn content_negotiation<F>(
request: Request,
supported: &[&str],
@ -448,49 +678,45 @@ impl<R: Read + Unpin> std::io::Read for SyncAsyncReader<R> {
#[cfg(test)]
mod tests {
use super::Store;
use super::*;
use crate::handle_request;
use async_std::task::block_on;
use http_types::{Method, Request, StatusCode, Url};
use std::collections::hash_map::DefaultHasher;
use std::env::temp_dir;
use std::fs::remove_dir_all;
use std::hash::{Hash, Hasher};
use tempfile::{tempdir, TempDir};
#[test]
fn get_ui() {
exec(
ServerTest::new().test_status(
Request::new(Method::Get, Url::parse("http://localhost/").unwrap()),
StatusCode::Ok,
)
}
#[test]
fn post_file() {
let mut request = Request::new(Method::Post, Url::parse("http://localhost/").unwrap());
request.insert_header("Content-Type", "text/turtle");
fn post_dataset_file() {
let mut request = Request::new(Method::Post, Url::parse("http://localhost/store").unwrap());
request.insert_header("Content-Type", "application/trig");
request.set_body("<http://example.com> <http://example.com> <http://example.com> .");
exec(request, StatusCode::NoContent)
ServerTest::new().test_status(request, StatusCode::NoContent)
}
#[test]
fn post_wrong_file() {
let mut request = Request::new(Method::Post, Url::parse("http://localhost/").unwrap());
request.insert_header("Content-Type", "text/turtle");
let mut request = Request::new(Method::Post, Url::parse("http://localhost/store").unwrap());
request.insert_header("Content-Type", "application/trig");
request.set_body("<http://example.com>");
exec(request, StatusCode::BadRequest)
ServerTest::new().test_status(request, StatusCode::BadRequest)
}
#[test]
fn post_unsupported_file() {
let mut request = Request::new(Method::Post, Url::parse("http://localhost/").unwrap());
let mut request = Request::new(Method::Post, Url::parse("http://localhost/store").unwrap());
request.insert_header("Content-Type", "text/foo");
exec(request, StatusCode::UnsupportedMediaType)
ServerTest::new().test_status(request, StatusCode::UnsupportedMediaType)
}
#[test]
fn get_query() {
exec(
ServerTest::new().test_status(
Request::new(
Method::Get,
Url::parse(
@ -504,7 +730,7 @@ mod tests {
#[test]
fn get_bad_query() {
exec(
ServerTest::new().test_status(
Request::new(
Method::Get,
Url::parse("http://localhost/query?query=SELECT").unwrap(),
@ -515,7 +741,7 @@ mod tests {
#[test]
fn get_without_query() {
exec(
ServerTest::new().test_status(
Request::new(Method::Get, Url::parse("http://localhost/query").unwrap()),
StatusCode::BadRequest,
);
@ -526,7 +752,7 @@ mod tests {
let mut request = Request::new(Method::Post, Url::parse("http://localhost/query").unwrap());
request.insert_header("Content-Type", "application/sparql-query");
request.set_body("SELECT * WHERE { ?s ?p ?o }");
exec(request, StatusCode::Ok)
ServerTest::new().test_status(request, StatusCode::Ok)
}
#[test]
@ -534,7 +760,7 @@ mod tests {
let mut request = Request::new(Method::Post, Url::parse("http://localhost/query").unwrap());
request.insert_header("Content-Type", "application/sparql-query");
request.set_body("SELECT");
exec(request, StatusCode::BadRequest)
ServerTest::new().test_status(request, StatusCode::BadRequest)
}
#[test]
@ -542,7 +768,7 @@ mod tests {
let mut request = Request::new(Method::Post, Url::parse("http://localhost/query").unwrap());
request.insert_header("Content-Type", "application/sparql-todo");
request.set_body("SELECT");
exec(request, StatusCode::UnsupportedMediaType)
ServerTest::new().test_status(request, StatusCode::UnsupportedMediaType)
}
#[test]
@ -550,7 +776,7 @@ mod tests {
let mut request = Request::new(Method::Post, Url::parse("http://localhost/query").unwrap());
request.insert_header("Content-Type", "application/sparql-query");
request.set_body("SELECT * WHERE { SERVICE <https://query.wikidata.org/sparql> { <https://en.wikipedia.org/wiki/Paris> ?p ?o } }");
exec(request, StatusCode::Ok)
ServerTest::new().test_status(request, StatusCode::Ok)
}
#[test]
@ -561,7 +787,7 @@ mod tests {
request.set_body(
"INSERT DATA { <http://example.com> <http://example.com> <http://example.com> }",
);
exec(request, StatusCode::NoContent)
ServerTest::new().test_status(request, StatusCode::NoContent)
}
#[test]
@ -570,22 +796,274 @@ mod tests {
Request::new(Method::Post, Url::parse("http://localhost/update").unwrap());
request.insert_header("Content-Type", "application/sparql-update");
request.set_body("INSERT");
exec(request, StatusCode::BadRequest)
ServerTest::new().test_status(request, StatusCode::BadRequest)
}
fn exec(request: Request, expected_status: StatusCode) {
let mut path = temp_dir();
path.push("temp-oxigraph-server-test");
let mut s = DefaultHasher::new();
format!("{:?}", request).hash(&mut s);
path.push(&s.finish().to_string());
let store = Store::open(&path).unwrap();
let (code, message) = match block_on(handle_request(request, store)) {
Ok(r) => (r.status(), "".to_string()),
Err(e) => (e.status(), e.to_string()),
};
assert_eq!(code, expected_status, "Error message: {}", message);
remove_dir_all(&path).unwrap()
#[test]
fn graph_store_protocol() {
// Tests from https://www.w3.org/2009/sparql/docs/tests/data-sparql11/http-rdf-update/
let server = ServerTest::new();
// PUT - Initial state
let mut request = Request::new(
Method::Put,
Url::parse("http://localhost/store/person/1.ttl").unwrap(),
);
request.insert_header("Content-Type", "text/turtle; charset=utf-8");
request.set_body(
"
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix v: <http://www.w3.org/2006/vcard/ns#> .
<http://$HOST$/$GRAPHSTORE$/person/1> a foaf:Person;
foaf:businessCard [
a v:VCard;
v:fn \"John Doe\"
].
",
);
server.test_status(request, StatusCode::Created);
// GET of PUT - Initial state
let mut request = Request::new(
Method::Get,
Url::parse("http://localhost/store?graph=/store/person/1.ttl").unwrap(),
);
request.insert_header("Accept", "text/turtle");
server.test_status(request, StatusCode::Ok);
// HEAD on an existing graph
server.test_status(
Request::new(
Method::Head,
Url::parse("http://localhost/store/person/1.ttl").unwrap(),
),
StatusCode::Ok,
);
// HEAD on a non-existing graph
server.test_status(
Request::new(
Method::Head,
Url::parse("http://localhost/store/person/4.ttl").unwrap(),
),
StatusCode::NotFound,
);
// PUT - graph already in store
let mut request = Request::new(
Method::Put,
Url::parse("http://localhost/store/person/1.ttl").unwrap(),
);
request.insert_header("Content-Type", "text/turtle; charset=utf-8");
request.set_body(
"
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix v: <http://www.w3.org/2006/vcard/ns#> .
<http://$HOST$/$GRAPHSTORE$/person/1> a foaf:Person;
foaf:businessCard [
a v:VCard;
v:fn \"Jane Doe\"
].
",
);
server.test_status(request, StatusCode::NoContent);
// GET of PUT - graph already in store
let mut request = Request::new(
Method::Get,
Url::parse("http://localhost/store/person/1.ttl").unwrap(),
);
request.insert_header("Accept", "text/turtle");
server.test_status(request, StatusCode::Ok);
// PUT - default graph
let mut request = Request::new(
Method::Put,
Url::parse("http://localhost/store?default").unwrap(),
);
request.insert_header("Content-Type", "text/turtle; charset=utf-8");
request.set_body(
"
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix v: <http://www.w3.org/2006/vcard/ns#> .
[] a foaf:Person;
foaf:businessCard [
a v:VCard;
v:given-name \"Alice\"
] .
",
);
server.test_status(request, StatusCode::NoContent); // The default graph always exists in Oxigraph
// GET of PUT - default graph
let mut request = Request::new(
Method::Get,
Url::parse("http://localhost/store?default").unwrap(),
);
request.insert_header("Accept", "text/turtle");
server.test_status(request, StatusCode::Ok);
// PUT - mismatched payload
let mut request = Request::new(
Method::Put,
Url::parse("http://localhost/store/person/1.ttl").unwrap(),
);
request.insert_header("Content-Type", "text/turtle; charset=utf-8");
request.set_body("@prefix fo");
server.test_status(request, StatusCode::BadRequest);
// PUT - empty graph
let mut request = Request::new(
Method::Put,
Url::parse("http://localhost/store/person/2.ttl").unwrap(),
);
request.insert_header("Content-Type", "text/turtle; charset=utf-8");
server.test_status(request, StatusCode::Created);
// GET of PUT - empty graph
let mut request = Request::new(
Method::Get,
Url::parse("http://localhost/store/person/2.ttl").unwrap(),
);
request.insert_header("Accept", "text/turtle");
server.test_status(request, StatusCode::Ok);
// PUT - replace empty graph
let mut request = Request::new(
Method::Put,
Url::parse("http://localhost/store/person/2.ttl").unwrap(),
);
request.insert_header("Content-Type", "text/turtle; charset=utf-8");
request.set_body(
"
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix v: <http://www.w3.org/2006/vcard/ns#> .
[] a foaf:Person;
foaf:businessCard [
a v:VCard;
v:given-name \"Alice\"
] .
",
);
server.test_status(request, StatusCode::NoContent);
// GET of replacement for empty graph
let mut request = Request::new(
Method::Get,
Url::parse("http://localhost/store/person/2.ttl").unwrap(),
);
request.insert_header("Accept", "text/turtle");
server.test_status(request, StatusCode::Ok);
// DELETE - existing graph
server.test_status(
Request::new(
Method::Delete,
Url::parse("http://localhost/store/person/2.ttl").unwrap(),
),
StatusCode::NoContent,
);
// GET of DELETE - existing graph
server.test_status(
Request::new(
Method::Get,
Url::parse("http://localhost/store/person/2.ttl").unwrap(),
),
StatusCode::NotFound,
);
// DELETE - non-existent graph
server.test_status(
Request::new(
Method::Delete,
Url::parse("http://localhost/store/person/2.ttl").unwrap(),
),
StatusCode::NotFound,
);
// POST - existing graph
let mut request = Request::new(
Method::Put,
Url::parse("http://localhost/store/person/1.ttl").unwrap(),
);
request.insert_header("Content-Type", "text/turtle; charset=utf-8");
server.test_status(request, StatusCode::NoContent);
// TODO: POST - multipart/form-data
// TODO: GET of POST - multipart/form-data
// POST - create new graph
let mut request = Request::new(Method::Post, Url::parse("http://localhost/store").unwrap());
request.insert_header("Content-Type", "text/turtle; charset=utf-8");
request.set_body(
"
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix v: <http://www.w3.org/2006/vcard/ns#> .
[] a foaf:Person;
foaf:businessCard [
a v:VCard;
v:given-name \"Alice\"
] .
",
);
let response = server.exec(request);
assert_eq!(response.status(), StatusCode::Created);
let location = response.header("Location").unwrap().as_str();
// GET of POST - create new graph
let mut request = Request::new(Method::Get, Url::parse(location).unwrap());
request.insert_header("Accept", "text/turtle");
server.test_status(request, StatusCode::Ok);
// POST - empty graph to existing graph
let mut request = Request::new(Method::Put, Url::parse(location).unwrap());
request.insert_header("Content-Type", "text/turtle; charset=utf-8");
server.test_status(request, StatusCode::NoContent);
// GET of POST - after noop
let mut request = Request::new(Method::Get, Url::parse(location).unwrap());
request.insert_header("Accept", "text/turtle");
server.test_status(request, StatusCode::Ok);
}
struct ServerTest {
store: Store,
_path: TempDir,
}
impl ServerTest {
fn new() -> ServerTest {
let path = tempdir().unwrap();
let store = Store::open(path.path()).unwrap();
ServerTest { _path: path, store }
}
fn exec(&self, request: Request) -> Response {
match block_on(handle_request(request, self.store.clone())) {
Ok(response) => response,
Err(e) => {
let mut response = Response::new(e.status());
response.set_body(e.to_string());
response
}
}
}
fn test_status(&self, request: Request, expected_status: StatusCode) {
let mut response = self.exec(request);
assert_eq!(
response.status(),
expected_status,
"Error message: {}",
block_on(response.body_string()).unwrap()
);
}
}
}

Loading…
Cancel
Save