#![deny( future_incompatible, nonstandard_style, rust_2018_idioms, missing_copy_implementations, trivial_casts, trivial_numeric_casts, unsafe_code, unused_qualifications )] use argh::FromArgs; use async_std::future::Future; use async_std::io::Read; use async_std::net::{TcpListener, TcpStream}; use async_std::prelude::*; use async_std::task::{block_on, spawn}; use http_types::{ bail_status, headers, Error, Method, Mime, Request, Response, Result, StatusCode, }; use oxigraph::io::{DatasetFormat, GraphFormat}; 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, Url}; const MAX_SPARQL_BODY_SIZE: u64 = 1_048_576; const HTML_ROOT_PAGE: &str = include_str!("../templates/query.html"); const LOGO: &str = include_str!("../logo.svg"); const SERVER: &str = concat!("Oxigraph/", env!("CARGO_PKG_VERSION")); #[derive(FromArgs)] /// Oxigraph SPARQL server struct Args { /// specify a server socket to bind using the format $(HOST):$(PORT) #[argh(option, short = 'b', default = "\"localhost:7878\".to_string()")] bind: String, /// directory in which persist the data #[argh(option, short = 'f')] file: String, } #[async_std::main] pub async fn main() -> Result<()> { let args: Args = argh::from_env(); let store = Store::open(args.file)?; println!("Listening for requests at http://{}", &args.bind); http_server(&args.bind, move |request| { handle_request(request, store.clone()) }) .await } async fn handle_request(request: Request, store: Store) -> Result { let mut response = match (request.url().path(), request.method()) { ("/", Method::Get) => { let mut response = Response::new(StatusCode::Ok); response.append_header(headers::CONTENT_TYPE, "text/html"); response.set_body(HTML_ROOT_PAGE); response } ("/logo.svg", Method::Get) => { let mut response = Response::new(StatusCode::Ok); response.append_header(headers::CONTENT_TYPE, "image/svg+xml"); response.set_body(LOGO); response } ("/query", Method::Get) => { configure_and_evaluate_sparql_query(store, url_query(&request), None, request)? } ("/query", Method::Post) => { if let Some(content_type) = request.content_type() { if content_type.essence() == "application/sparql-query" { let mut buffer = String::new(); let mut request = request; request .take_body() .take(MAX_SPARQL_BODY_SIZE) .read_to_string(&mut buffer) .await?; configure_and_evaluate_sparql_query( store, url_query(&request), Some(buffer), request, )? } else if content_type.essence() == "application/x-www-form-urlencoded" { let mut buffer = Vec::new(); let mut request = request; request .take_body() .take(MAX_SPARQL_BODY_SIZE) .read_to_end(&mut buffer) .await?; configure_and_evaluate_sparql_query(store, buffer, None, request)? } else { bail_status!(415, "Not supported Content-Type given: {}", content_type) } } else { bail_status!(400, "No Content-Type given"); } } ("/update", Method::Post) => { if let Some(content_type) = request.content_type() { if content_type.essence() == "application/sparql-update" { let mut buffer = String::new(); let mut request = request; request .take_body() .take(MAX_SPARQL_BODY_SIZE) .read_to_string(&mut buffer) .await?; configure_and_evaluate_sparql_update( store, url_query(&request), Some(buffer), request, )? } else if content_type.essence() == "application/x-www-form-urlencoded" { let mut buffer = Vec::new(); let mut request = request; request .take_body() .take(MAX_SPARQL_BODY_SIZE) .read_to_end(&mut buffer) .await?; configure_and_evaluate_sparql_update(store, buffer, None, request)? } else { bail_status!(415, "Not supported Content-Type given: {}", content_type) } } else { bail_status!(400, "No Content-Type given") } } (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::()))? .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) -> Result { 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 { request.url().query().unwrap_or("").as_bytes().to_vec() } fn configure_and_evaluate_sparql_query( store: Store, encoded: Vec, mut query: Option, request: Request, ) -> Result { let mut default_graph_uris = Vec::new(); let mut named_graph_uris = Vec::new(); for (k, v) in form_urlencoded::parse(&encoded) { match k.as_ref() { "query" => { if query.is_some() { bail_status!(400, "Multiple query parameters provided") } query = Some(v.into_owned()) } "default-graph-uri" => default_graph_uris.push(v.into_owned()), "named-graph-uri" => named_graph_uris.push(v.into_owned()), _ => bail_status!(400, "Unexpected parameter: {}", k), } } if let Some(query) = query { evaluate_sparql_query(store, query, default_graph_uris, named_graph_uris, request) } else { bail_status!(400, "You should set the 'query' parameter") } } fn evaluate_sparql_query( store: Store, query: String, default_graph_uris: Vec, named_graph_uris: Vec, request: Request, ) -> Result { 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())) .collect::>>() .map_err(bad_request)?; let named_graph_uris = named_graph_uris .into_iter() .map(|e| Ok(NamedNode::new(e)?.into())) .collect::>>() .map_err(bad_request)?; if !default_graph_uris.is_empty() || !named_graph_uris.is_empty() { query.dataset_mut().set_default_graph(default_graph_uris); query .dataset_mut() .set_available_named_graphs(named_graph_uris); } let results = store.query(query)?; //TODO: stream if let QueryResults::Graph(_) = results { let format = graph_content_negotiation(request)?; let mut body = Vec::default(); results.write_graph(&mut body, format)?; let mut response = Response::from(body); response.insert_header(headers::CONTENT_TYPE, format.media_type()); Ok(response) } else { let format = content_negotiation( request, &[ QueryResultsFormat::Xml.media_type(), QueryResultsFormat::Json.media_type(), QueryResultsFormat::Csv.media_type(), QueryResultsFormat::Tsv.media_type(), ], QueryResultsFormat::from_media_type, )?; let mut body = Vec::default(); results.write(&mut body, format)?; let mut response = Response::from(body); response.insert_header(headers::CONTENT_TYPE, format.media_type()); Ok(response) } } fn configure_and_evaluate_sparql_update( store: Store, encoded: Vec, mut update: Option, request: Request, ) -> Result { let mut default_graph_uris = Vec::new(); let mut named_graph_uris = Vec::new(); for (k, v) in form_urlencoded::parse(&encoded) { match k.as_ref() { "update" => { if update.is_some() { bail_status!(400, "Multiple update parameters provided") } update = Some(v.into_owned()) } "using-graph-uri" => default_graph_uris.push(v.into_owned()), "using-named-graph-uri" => named_graph_uris.push(v.into_owned()), _ => bail_status!(400, "Unexpected parameter: {}", k), } } if let Some(update) = update { evaluate_sparql_update(store, update, default_graph_uris, named_graph_uris, request) } else { bail_status!(400, "You should set the 'update' parameter") } } fn evaluate_sparql_update( store: Store, update: String, default_graph_uris: Vec, named_graph_uris: Vec, request: Request, ) -> Result { 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())) .collect::>>() .map_err(bad_request)?; let named_graph_uris = named_graph_uris .into_iter() .map(|e| Ok(NamedNode::new(e)?.into())) .collect::>>() .map_err(bad_request)?; if !default_graph_uris.is_empty() || !named_graph_uris.is_empty() { for operation in &mut update.operations { if let GraphUpdateOperation::DeleteInsert { using, .. } = operation { if !using.is_default_dataset() { bail_status!(400, "using-graph-uri and using-named-graph-uri must not be used with a SPARQL UPDATE containing USING", ); } using.set_default_graph(default_graph_uris.clone()); using.set_available_named_graphs(named_graph_uris.clone()); } } } store.update(update)?; Ok(Response::new(StatusCode::NoContent)) } fn store_target(request: &Request) -> Result> { 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>, >( host: &str, handle: F, ) -> Result<()> { async fn accept Fut, Fut: Future>>( stream: TcpStream, handle: F, ) -> Result<()> { async_h1::accept(stream, |request| async { Ok(match handle(request).await { Ok(result) => result, Err(error) => { if error.status().is_server_error() { eprintln!("{}", error); } let mut response = Response::new(error.status()); response.set_body(error.to_string()); response } }) }) .await } let listener = TcpListener::bind(host).await?; let mut incoming = listener.incoming(); while let Some(stream) = incoming.next().await { let stream = stream?; let handle = handle.clone(); spawn(async { if let Err(error) = accept(stream, handle).await { eprintln!("{}", error); }; }); } Ok(()) } fn graph_content_negotiation(request: Request) -> Result { content_negotiation( request, &[ GraphFormat::NTriples.media_type(), GraphFormat::Turtle.media_type(), GraphFormat::RdfXml.media_type(), ], GraphFormat::from_media_type, ) } fn dataset_content_negotiation(request: Request) -> Result { content_negotiation( request, &[ DatasetFormat::NQuads.media_type(), DatasetFormat::TriG.media_type(), ], DatasetFormat::from_media_type, ) } fn content_negotiation( request: Request, supported: &[&str], parse: impl Fn(&str) -> Option, ) -> Result { let header = request .header(headers::ACCEPT) .map(|h| h.last().as_str().trim()) .unwrap_or(""); let supported: Vec = supported .iter() .map(|h| Mime::from_str(h).unwrap()) .collect(); let mut result = supported.first().unwrap(); let mut result_score = 0f32; if !header.is_empty() { for possible in header.split(',') { let possible = Mime::from_str(possible.trim())?; let score = if let Some(q) = possible.param("q") { f32::from_str(&q.to_string())? } else { 1. }; if score <= result_score { continue; } for candidate in &supported { if (possible.basetype() == candidate.basetype() || possible.basetype() == "*") && (possible.subtype() == candidate.subtype() || possible.subtype() == "*") { result = candidate; result_score = score; break; } } } } parse(result.essence()) .ok_or_else(|| Error::from_str(StatusCode::InternalServerError, "Unknown mime type")) } fn bad_request(e: impl Into) -> Error { let mut e = e.into(); e.set_status(StatusCode::BadRequest); e } struct SyncAsyncReader { inner: R, } impl From for SyncAsyncReader { fn from(inner: R) -> Self { Self { inner } } } impl std::io::Read for SyncAsyncReader { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { block_on(self.inner.read(buf)) } //TODO: implement other methods } #[cfg(test)] mod tests { use super::*; use crate::handle_request; use async_std::task::block_on; use tempfile::{tempdir, TempDir}; #[test] fn get_ui() { ServerTest::new().test_status( Request::new(Method::Get, Url::parse("http://localhost/").unwrap()), StatusCode::Ok, ) } #[test] 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(" ."); ServerTest::new().test_status(request, StatusCode::NoContent) } #[test] fn post_wrong_file() { let mut request = Request::new(Method::Post, Url::parse("http://localhost/store").unwrap()); request.insert_header("Content-Type", "application/trig"); request.set_body(""); ServerTest::new().test_status(request, StatusCode::BadRequest) } #[test] fn post_unsupported_file() { let mut request = Request::new(Method::Post, Url::parse("http://localhost/store").unwrap()); request.insert_header("Content-Type", "text/foo"); ServerTest::new().test_status(request, StatusCode::UnsupportedMediaType) } #[test] fn get_query() { ServerTest::new().test_status( Request::new( Method::Get, Url::parse( "http://localhost/query?query=SELECT%20*%20WHERE%20{%20?s%20?p%20?o%20}", ) .unwrap(), ), StatusCode::Ok, ); } #[test] fn get_bad_query() { ServerTest::new().test_status( Request::new( Method::Get, Url::parse("http://localhost/query?query=SELECT").unwrap(), ), StatusCode::BadRequest, ); } #[test] fn get_without_query() { ServerTest::new().test_status( Request::new(Method::Get, Url::parse("http://localhost/query").unwrap()), StatusCode::BadRequest, ); } #[test] fn post_query() { 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 }"); ServerTest::new().test_status(request, StatusCode::Ok) } #[test] fn post_bad_query() { 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"); ServerTest::new().test_status(request, StatusCode::BadRequest) } #[test] fn post_unknown_query() { 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"); ServerTest::new().test_status(request, StatusCode::UnsupportedMediaType) } #[test] fn post_federated_query() { 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 { ?p ?o } }"); ServerTest::new().test_status(request, StatusCode::Ok) } #[test] fn post_update() { let mut request = Request::new(Method::Post, Url::parse("http://localhost/update").unwrap()); request.insert_header("Content-Type", "application/sparql-update"); request.set_body( "INSERT DATA { }", ); ServerTest::new().test_status(request, StatusCode::NoContent) } #[test] fn post_bad_update() { let mut request = Request::new(Method::Post, Url::parse("http://localhost/update").unwrap()); request.insert_header("Content-Type", "application/sparql-update"); request.set_body("INSERT"); ServerTest::new().test_status(request, StatusCode::BadRequest) } #[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: . @prefix v: . 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: . @prefix v: . 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: . @prefix v: . [] 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: . @prefix v: . [] 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: . @prefix v: . [] 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() ); } } }