parent
d121aed47b
commit
23cc09f481
@ -0,0 +1,19 @@ |
|||||||
|
//! Simple HTTP client
|
||||||
|
|
||||||
|
use crate::error::invalid_input_error; |
||||||
|
use http::{Request, Response}; |
||||||
|
use std::io; |
||||||
|
|
||||||
|
pub struct Client {} |
||||||
|
|
||||||
|
impl Client { |
||||||
|
pub fn new() -> Self { |
||||||
|
Self {} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn request(&self, _request: &Request<Option<Vec<u8>>>) -> io::Result<Response<Vec<u8>>> { |
||||||
|
Err(invalid_input_error( |
||||||
|
"HTTP client is not available. Enable the feature 'simple_http'", |
||||||
|
)) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
#[cfg(not(feature = "http_client"))] |
||||||
|
mod dummy; |
||||||
|
#[cfg(feature = "http_client")] |
||||||
|
mod simple; |
||||||
|
|
||||||
|
#[cfg(not(feature = "http_client"))] |
||||||
|
pub use dummy::Client; |
||||||
|
#[cfg(feature = "http_client")] |
||||||
|
pub use simple::Client; |
@ -0,0 +1,425 @@ |
|||||||
|
//! Simple HTTP client
|
||||||
|
|
||||||
|
use crate::error::{invalid_data_error, invalid_input_error}; |
||||||
|
use http::header::{CONNECTION, CONTENT_LENGTH, HOST, TRANSFER_ENCODING}; |
||||||
|
use http::{Request, Response, Version}; |
||||||
|
use httparse::Status; |
||||||
|
use native_tls::TlsConnector; |
||||||
|
use std::cmp::min; |
||||||
|
use std::convert::TryInto; |
||||||
|
use std::io; |
||||||
|
use std::io::{BufRead, BufReader, Read, Write}; |
||||||
|
use std::net::TcpStream; |
||||||
|
|
||||||
|
pub struct Client {} |
||||||
|
|
||||||
|
impl Client { |
||||||
|
pub fn new() -> Self { |
||||||
|
Self {} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn request( |
||||||
|
&self, |
||||||
|
request: &Request<Option<Vec<u8>>>, |
||||||
|
) -> io::Result<Response<Box<dyn BufRead>>> { |
||||||
|
let scheme = request |
||||||
|
.uri() |
||||||
|
.scheme_str() |
||||||
|
.ok_or_else(|| invalid_input_error("No host provided"))?; |
||||||
|
let port = if let Some(port) = request.uri().port_u16() { |
||||||
|
port |
||||||
|
} else { |
||||||
|
match scheme { |
||||||
|
"http" => 80, |
||||||
|
"https" => 443, |
||||||
|
_ => { |
||||||
|
return Err(invalid_input_error(format!( |
||||||
|
"No port provided for scheme '{}'", |
||||||
|
scheme |
||||||
|
))) |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
let host = request |
||||||
|
.uri() |
||||||
|
.host() |
||||||
|
.ok_or_else(|| invalid_input_error("No host provided"))?; |
||||||
|
|
||||||
|
match scheme { |
||||||
|
"http" => { |
||||||
|
let mut stream = TcpStream::connect((host, port))?; |
||||||
|
self.encode(request, &mut stream)?; |
||||||
|
self.decode(stream) |
||||||
|
} |
||||||
|
"https" => { |
||||||
|
let connector = |
||||||
|
TlsConnector::new().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; |
||||||
|
let stream = TcpStream::connect((host, port))?; |
||||||
|
let mut stream = connector |
||||||
|
.connect(host, stream) |
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; |
||||||
|
self.encode(request, &mut stream)?; |
||||||
|
self.decode(stream) |
||||||
|
} |
||||||
|
_ => Err(invalid_input_error(format!( |
||||||
|
"Not supported URL scheme: {}", |
||||||
|
scheme |
||||||
|
))), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn encode( |
||||||
|
&self, |
||||||
|
request: &Request<Option<Vec<u8>>>, |
||||||
|
mut writer: &mut impl Write, |
||||||
|
) -> io::Result<()> { |
||||||
|
if request.headers().contains_key(CONTENT_LENGTH) { |
||||||
|
return Err(invalid_input_error( |
||||||
|
"content-length header is set by the client library", |
||||||
|
)); |
||||||
|
} |
||||||
|
if request.headers().contains_key(HOST) { |
||||||
|
return Err(invalid_input_error( |
||||||
|
"host header is set by the client library", |
||||||
|
)); |
||||||
|
} |
||||||
|
if request.headers().contains_key(CONNECTION) { |
||||||
|
return Err(invalid_input_error( |
||||||
|
"connection header is set by the client library", |
||||||
|
)); |
||||||
|
} |
||||||
|
if let Some(query) = request.uri().query() { |
||||||
|
write!( |
||||||
|
&mut writer, |
||||||
|
"{} {}?{} HTTP/1.1\r\n", |
||||||
|
request.method(), |
||||||
|
request.uri().path(), |
||||||
|
query |
||||||
|
)?; |
||||||
|
} else { |
||||||
|
write!( |
||||||
|
&mut writer, |
||||||
|
"{} {} HTTP/1.1\r\n", |
||||||
|
request.method(), |
||||||
|
request.uri().path() |
||||||
|
)?; |
||||||
|
} |
||||||
|
|
||||||
|
// host
|
||||||
|
let host = request |
||||||
|
.uri() |
||||||
|
.host() |
||||||
|
.ok_or_else(|| invalid_input_error("No host provided"))?; |
||||||
|
if let Some(port) = request.uri().port() { |
||||||
|
write!(writer, "host: {}:{}\r\n", request.uri(), port) |
||||||
|
} else { |
||||||
|
write!(writer, "host: {}\r\n", host) |
||||||
|
}?; |
||||||
|
|
||||||
|
// connection
|
||||||
|
write!(writer, "connection: close\r\n")?; |
||||||
|
|
||||||
|
// headers
|
||||||
|
for (name, value) in request.headers() { |
||||||
|
write!(writer, "{}: ", name.as_str())?; |
||||||
|
writer.write_all(value.as_bytes())?; |
||||||
|
write!(writer, "\r\n")?; |
||||||
|
} |
||||||
|
|
||||||
|
// body with content-length
|
||||||
|
if let Some(payload) = request.body() { |
||||||
|
write!(writer, "content-length: {}\r\n\r\n", payload.len())?; |
||||||
|
writer.write_all(payload)?; |
||||||
|
} else { |
||||||
|
write!(writer, "\r\n")?; |
||||||
|
} |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn decode<'a>(&self, reader: impl Read + 'a) -> io::Result<Response<Box<dyn BufRead + 'a>>> { |
||||||
|
let mut reader = BufReader::new(reader); |
||||||
|
|
||||||
|
// Let's read the headers
|
||||||
|
let mut buffer = Vec::new(); |
||||||
|
let mut headers = [httparse::EMPTY_HEADER; 1024]; |
||||||
|
let mut parsed_response = httparse::Response::new(&mut headers); |
||||||
|
loop { |
||||||
|
if reader.read_until(b'\n', &mut buffer)? == 0 { |
||||||
|
return Err(invalid_data_error("Empty HTTP response")); |
||||||
|
} |
||||||
|
if buffer.len() > 8 * 1024 { |
||||||
|
return Err(invalid_data_error("The headers size should fit in 8kb")); |
||||||
|
} |
||||||
|
|
||||||
|
if buffer.ends_with(b"\r\n\r\n") || buffer.ends_with(b"\n\n") { |
||||||
|
break; //end of buffer
|
||||||
|
} |
||||||
|
} |
||||||
|
if parsed_response |
||||||
|
.parse(&buffer) |
||||||
|
.map_err(invalid_data_error)? |
||||||
|
.is_partial() |
||||||
|
{ |
||||||
|
return Err(invalid_input_error( |
||||||
|
"Partial HTTP headers containing two line jumps", |
||||||
|
)); |
||||||
|
} |
||||||
|
|
||||||
|
// Let's build the response
|
||||||
|
let mut response = Response::builder() |
||||||
|
.status( |
||||||
|
parsed_response |
||||||
|
.code |
||||||
|
.ok_or_else(|| invalid_data_error("No status code in the HTTP response"))?, |
||||||
|
) |
||||||
|
.version(match parsed_response.version { |
||||||
|
Some(0) => Version::HTTP_10, |
||||||
|
Some(1) => Version::HTTP_11, |
||||||
|
Some(id) => { |
||||||
|
return Err(invalid_data_error(format!( |
||||||
|
"Unsupported HTTP response version: 1.{}", |
||||||
|
id |
||||||
|
))) |
||||||
|
} |
||||||
|
None => return Err(invalid_data_error("No HTTP version in the HTTP response")), |
||||||
|
}); |
||||||
|
for header in parsed_response.headers { |
||||||
|
response = response.header(header.name, header.value); |
||||||
|
} |
||||||
|
|
||||||
|
let content_length = response.headers_ref().and_then(|h| h.get(CONTENT_LENGTH)); |
||||||
|
let transfer_encoding = response |
||||||
|
.headers_ref() |
||||||
|
.and_then(|h| h.get(TRANSFER_ENCODING)); |
||||||
|
if transfer_encoding.is_some() && content_length.is_some() { |
||||||
|
return Err(invalid_data_error( |
||||||
|
"Transfer-Encoding and Content-Length should not be set at the same time", |
||||||
|
)); |
||||||
|
} |
||||||
|
|
||||||
|
let body: Box<dyn BufRead> = if let Some(content_length) = content_length { |
||||||
|
let len = content_length |
||||||
|
.to_str() |
||||||
|
.map_err(invalid_data_error)? |
||||||
|
.parse::<u64>() |
||||||
|
.map_err(invalid_data_error)?; |
||||||
|
Box::new(reader.take(len)) |
||||||
|
} else if let Some(transfer_encoding) = transfer_encoding { |
||||||
|
let transfer_encoding = transfer_encoding.to_str().map_err(invalid_data_error)?; |
||||||
|
if transfer_encoding.eq_ignore_ascii_case("chunked") { |
||||||
|
buffer.clear(); |
||||||
|
Box::new(BufReader::new(ChunkedResponse { |
||||||
|
reader, |
||||||
|
buffer, |
||||||
|
is_start: true, |
||||||
|
chunk_position: 1, |
||||||
|
chunk_size: 1, |
||||||
|
})) |
||||||
|
} else { |
||||||
|
return Err(invalid_data_error(format!( |
||||||
|
"Transfer-Encoding: {} is not supported", |
||||||
|
transfer_encoding |
||||||
|
))); |
||||||
|
} |
||||||
|
} else { |
||||||
|
Box::new(io::empty()) |
||||||
|
}; |
||||||
|
|
||||||
|
response.body(body).map_err(invalid_data_error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
struct ChunkedResponse<R: BufRead> { |
||||||
|
reader: R, |
||||||
|
buffer: Vec<u8>, |
||||||
|
is_start: bool, |
||||||
|
chunk_position: usize, |
||||||
|
chunk_size: usize, |
||||||
|
} |
||||||
|
|
||||||
|
impl<R: BufRead> Read for ChunkedResponse<R> { |
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { |
||||||
|
loop { |
||||||
|
// In case we still have data
|
||||||
|
if self.chunk_position < self.chunk_size { |
||||||
|
let inner_buf = self.reader.fill_buf()?; |
||||||
|
let size = min( |
||||||
|
min(buf.len(), inner_buf.len()), |
||||||
|
self.chunk_size - self.chunk_position, |
||||||
|
); |
||||||
|
buf[..size].copy_from_slice(&inner_buf[..size]); |
||||||
|
self.reader.consume(size); |
||||||
|
self.chunk_position += size; |
||||||
|
return Ok(size); // Won't be 0 if there is still some inner buffer
|
||||||
|
} |
||||||
|
|
||||||
|
if self.chunk_size == 0 { |
||||||
|
return Ok(0); // We know it's the end
|
||||||
|
} |
||||||
|
|
||||||
|
if self.is_start { |
||||||
|
self.is_start = false; |
||||||
|
} else { |
||||||
|
// chunk end
|
||||||
|
self.buffer.clear(); |
||||||
|
self.reader.read_until(b'\n', &mut self.buffer)?; |
||||||
|
if self.buffer != b"\r\n" && self.buffer != b"\n" { |
||||||
|
return Err(invalid_data_error("Invalid chunked element end")); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// We load a new chunk
|
||||||
|
self.buffer.clear(); |
||||||
|
self.reader.read_until(b'\n', &mut self.buffer)?; |
||||||
|
self.chunk_position = 0; |
||||||
|
self.chunk_size = if let Ok(Status::Complete((read, chunk_size))) = |
||||||
|
httparse::parse_chunk_size(&self.buffer) |
||||||
|
{ |
||||||
|
if read != self.buffer.len() { |
||||||
|
return Err(invalid_data_error("Chuncked header containing a line jump")); |
||||||
|
} |
||||||
|
chunk_size.try_into().map_err(invalid_data_error)? |
||||||
|
} else { |
||||||
|
return Err(invalid_data_error("Invalid chuncked header")); |
||||||
|
}; |
||||||
|
|
||||||
|
if self.chunk_size == 0 { |
||||||
|
// we read the trailers
|
||||||
|
loop { |
||||||
|
if self.reader.read_until(b'\n', &mut self.buffer)? == 0 { |
||||||
|
return Err(invalid_data_error("Missing chunked encoding end")); |
||||||
|
} |
||||||
|
if self.buffer.len() > 8 * 1024 { |
||||||
|
return Err(invalid_data_error("The trailers size should fit in 8kb")); |
||||||
|
} |
||||||
|
if self.buffer.ends_with(b"\r\n\r\n") || self.buffer.ends_with(b"\n\n") { |
||||||
|
break; //end of buffer
|
||||||
|
} |
||||||
|
} |
||||||
|
return Ok(0); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
use http::header::{ACCEPT, CONTENT_TYPE}; |
||||||
|
use http::{Method, StatusCode}; |
||||||
|
use std::io::Cursor; |
||||||
|
use std::str; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn encode_get_request() -> io::Result<()> { |
||||||
|
let mut buffer = Vec::new(); |
||||||
|
Client::new().encode( |
||||||
|
&Request::builder() |
||||||
|
.method(Method::GET) |
||||||
|
.uri("http://example.com/foo/bar?query#fragment") |
||||||
|
.header(ACCEPT, "application/json") |
||||||
|
.body(None) |
||||||
|
.unwrap(), |
||||||
|
&mut buffer, |
||||||
|
)?; |
||||||
|
assert_eq!( |
||||||
|
str::from_utf8(&buffer).unwrap(), |
||||||
|
"GET /foo/bar?query HTTP/1.1\r\nhost: example.com\r\nconnection: close\r\naccept: application/json\r\n\r\n" |
||||||
|
); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn encode_post_request() -> io::Result<()> { |
||||||
|
let mut buffer = Vec::new(); |
||||||
|
Client::new().encode( |
||||||
|
&Request::builder() |
||||||
|
.method(Method::POST) |
||||||
|
.uri("http://example.com/foo/bar?query#fragment") |
||||||
|
.header(ACCEPT, "application/json") |
||||||
|
.body(Some(b"testbody".to_vec())) |
||||||
|
.unwrap(), |
||||||
|
&mut buffer, |
||||||
|
)?; |
||||||
|
assert_eq!( |
||||||
|
str::from_utf8(&buffer).unwrap(), |
||||||
|
"POST /foo/bar?query HTTP/1.1\r\nhost: example.com\r\nconnection: close\r\naccept: application/json\r\ncontent-length: 8\r\n\r\ntestbody" |
||||||
|
); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn decode_response_without_payload() -> io::Result<()> { |
||||||
|
let response = Client::new() |
||||||
|
.decode(Cursor::new("HTTP/1.1 404 Not Found\r\n\r\n")) |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(response.status(), StatusCode::NOT_FOUND); |
||||||
|
let mut buf = String::new(); |
||||||
|
response.into_body().read_to_string(&mut buf)?; |
||||||
|
assert!(buf.is_empty()); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn decode_response_with_fixed_payload() -> io::Result<()> { |
||||||
|
let response = Client::new().decode(Cursor::new( |
||||||
|
"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ncontent-length:8\r\n\r\ntestbody", |
||||||
|
))?; |
||||||
|
assert_eq!(response.status(), StatusCode::OK); |
||||||
|
assert_eq!( |
||||||
|
response |
||||||
|
.headers() |
||||||
|
.get(CONTENT_TYPE) |
||||||
|
.unwrap() |
||||||
|
.to_str() |
||||||
|
.unwrap(), |
||||||
|
"text/plain" |
||||||
|
); |
||||||
|
let mut buf = String::new(); |
||||||
|
response.into_body().read_to_string(&mut buf)?; |
||||||
|
assert_eq!(buf, "testbody"); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn decode_response_with_chunked_payload() -> io::Result<()> { |
||||||
|
let response = Client::new().decode(Cursor::new( |
||||||
|
"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ntransfer-encoding:chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n", |
||||||
|
))?; |
||||||
|
assert_eq!(response.status(), StatusCode::OK); |
||||||
|
assert_eq!( |
||||||
|
response |
||||||
|
.headers() |
||||||
|
.get(CONTENT_TYPE) |
||||||
|
.unwrap() |
||||||
|
.to_str() |
||||||
|
.unwrap(), |
||||||
|
"text/plain" |
||||||
|
); |
||||||
|
let mut buf = String::new(); |
||||||
|
response.into_body().read_to_string(&mut buf)?; |
||||||
|
assert_eq!(buf, "Wikipedia in\r\n\r\nchunks."); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn decode_response_with_trailer() -> io::Result<()> { |
||||||
|
let response = Client::new().decode(Cursor::new( |
||||||
|
"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ntransfer-encoding:chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\ntest: foo\r\n\r\n", |
||||||
|
))?; |
||||||
|
assert_eq!(response.status(), StatusCode::OK); |
||||||
|
assert_eq!( |
||||||
|
response |
||||||
|
.headers() |
||||||
|
.get(CONTENT_TYPE) |
||||||
|
.unwrap() |
||||||
|
.to_str() |
||||||
|
.unwrap(), |
||||||
|
"text/plain" |
||||||
|
); |
||||||
|
let mut buf = String::new(); |
||||||
|
response.into_body().read_to_string(&mut buf)?; |
||||||
|
assert_eq!(buf, "Wikipedia in\r\n\r\nchunks."); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,150 @@ |
|||||||
|
use crate::error::{invalid_data_error, invalid_input_error}; |
||||||
|
use crate::model::NamedNode; |
||||||
|
use crate::sparql::error::EvaluationError; |
||||||
|
use crate::sparql::http::Client; |
||||||
|
use crate::sparql::model::QueryResults; |
||||||
|
use crate::sparql::parser::Query; |
||||||
|
use crate::sparql::QueryResultsFormat; |
||||||
|
use http::header::{ACCEPT, CONTENT_TYPE, USER_AGENT}; |
||||||
|
use http::{Method, Request, StatusCode}; |
||||||
|
use std::error::Error; |
||||||
|
|
||||||
|
/// Handler for [SPARQL 1.1 Federated Query](https://www.w3.org/TR/sparql11-federated-query/) SERVICE.
|
||||||
|
///
|
||||||
|
/// Should be given to [`QueryOptions`](struct.QueryOptions.html#method.with_service_handler)
|
||||||
|
/// before evaluating a SPARQL query that uses SERVICE calls.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use oxigraph::MemoryStore;
|
||||||
|
/// use oxigraph::model::*;
|
||||||
|
/// use oxigraph::sparql::{QueryOptions, QueryResults, ServiceHandler, Query, EvaluationError};
|
||||||
|
///
|
||||||
|
/// #[derive(Default)]
|
||||||
|
/// struct TestServiceHandler {
|
||||||
|
/// store: MemoryStore
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl ServiceHandler for TestServiceHandler {
|
||||||
|
/// type Error = EvaluationError;
|
||||||
|
///
|
||||||
|
/// fn handle(&self,service_name: NamedNode, query: Query) -> Result<QueryResults,EvaluationError> {
|
||||||
|
/// if service_name == "http://example.com/service" {
|
||||||
|
/// self.store.query(query, QueryOptions::default())
|
||||||
|
/// } else {
|
||||||
|
/// panic!()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let store = MemoryStore::new();
|
||||||
|
/// let service = TestServiceHandler::default();
|
||||||
|
/// let ex = NamedNode::new("http://example.com")?;
|
||||||
|
/// service.store.insert(Quad::new(ex.clone(), ex.clone(), ex.clone(), None));
|
||||||
|
///
|
||||||
|
/// if let QueryResults::Solutions(mut solutions) = store.query(
|
||||||
|
/// "SELECT ?s WHERE { SERVICE <http://example.com/service> { ?s ?p ?o } }",
|
||||||
|
/// QueryOptions::default().with_service_handler(service)
|
||||||
|
/// )? {
|
||||||
|
/// assert_eq!(solutions.next().unwrap()?.get("s"), Some(&ex.into()));
|
||||||
|
/// }
|
||||||
|
/// # Result::<_,Box<dyn std::error::Error>>::Ok(())
|
||||||
|
/// ```
|
||||||
|
pub trait ServiceHandler { |
||||||
|
type Error: Error + Send + Sync + 'static; |
||||||
|
|
||||||
|
/// Evaluates a [`Query`](struct.Query.html) against a given service identified by a [`NamedNode`](../model/struct.NamedNode.html).
|
||||||
|
fn handle(&self, service_name: NamedNode, query: Query) -> Result<QueryResults, Self::Error>; |
||||||
|
} |
||||||
|
|
||||||
|
pub struct EmptyServiceHandler; |
||||||
|
|
||||||
|
impl ServiceHandler for EmptyServiceHandler { |
||||||
|
type Error = EvaluationError; |
||||||
|
|
||||||
|
fn handle(&self, _: NamedNode, _: Query) -> Result<QueryResults, EvaluationError> { |
||||||
|
Err(EvaluationError::msg( |
||||||
|
"The SERVICE feature is not implemented", |
||||||
|
)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub struct ErrorConversionServiceHandler<S: ServiceHandler> { |
||||||
|
handler: S, |
||||||
|
} |
||||||
|
|
||||||
|
impl<S: ServiceHandler> ErrorConversionServiceHandler<S> { |
||||||
|
pub fn wrap(handler: S) -> Self { |
||||||
|
Self { handler } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<S: ServiceHandler> ServiceHandler for ErrorConversionServiceHandler<S> { |
||||||
|
type Error = EvaluationError; |
||||||
|
|
||||||
|
fn handle( |
||||||
|
&self, |
||||||
|
service_name: NamedNode, |
||||||
|
query: Query, |
||||||
|
) -> Result<QueryResults, EvaluationError> { |
||||||
|
self.handler |
||||||
|
.handle(service_name, query) |
||||||
|
.map_err(EvaluationError::wrap) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub struct SimpleServiceHandler { |
||||||
|
client: Client, |
||||||
|
} |
||||||
|
|
||||||
|
impl SimpleServiceHandler { |
||||||
|
pub fn new() -> Self { |
||||||
|
Self { |
||||||
|
client: Client::new(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl ServiceHandler for SimpleServiceHandler { |
||||||
|
type Error = EvaluationError; |
||||||
|
|
||||||
|
fn handle( |
||||||
|
&self, |
||||||
|
service_name: NamedNode, |
||||||
|
query: Query, |
||||||
|
) -> Result<QueryResults, EvaluationError> { |
||||||
|
let request = Request::builder() |
||||||
|
.method(Method::POST) |
||||||
|
.uri(service_name.as_str()) |
||||||
|
.header(CONTENT_TYPE, "application/sparql-query") |
||||||
|
.header(ACCEPT, QueryResultsFormat::Xml.media_type()) |
||||||
|
.header(USER_AGENT, concat!("Oxigraph/", env!("CARGO_PKG_VERSION"))) |
||||||
|
.body(Some(query.to_string().into_bytes())) |
||||||
|
.map_err(invalid_input_error)?; |
||||||
|
let response = self.client.request(&request)?; |
||||||
|
if response.status() != StatusCode::OK { |
||||||
|
return Err(EvaluationError::msg(format!( |
||||||
|
"HTTP error code {} returned when querying service {}", |
||||||
|
response.status(), |
||||||
|
service_name |
||||||
|
))); |
||||||
|
} |
||||||
|
let content_type = response |
||||||
|
.headers() |
||||||
|
.get(CONTENT_TYPE) |
||||||
|
.ok_or_else(|| { |
||||||
|
EvaluationError::msg(format!( |
||||||
|
"No Content-Type header returned by {}", |
||||||
|
service_name |
||||||
|
)) |
||||||
|
})? |
||||||
|
.to_str() |
||||||
|
.map_err(invalid_data_error)?; |
||||||
|
let format = QueryResultsFormat::from_media_type(content_type).ok_or_else(|| { |
||||||
|
EvaluationError::msg(format!( |
||||||
|
"Unsupported Content-Type returned by {}: {}", |
||||||
|
service_name, content_type |
||||||
|
)) |
||||||
|
})?; |
||||||
|
Ok(QueryResults::read(response.into_body(), format)?) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue