use crate::rdf_source::RdfSource;
use crate::vocab;
use bytes::Bytes;
use futures::Stream;
use oxigraph::io::{RdfFormat, RdfParser};
use oxigraph::model::{Dataset, GraphNameRef, NamedNodeRef};
use reqwest_middleware::reqwest::{Client, Response, StatusCode, Url, header};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder};
use tracing::error;
#[derive(Clone, Debug)]
pub struct ResourceRequestBuilder {
client: ClientWithMiddleware,
url: Url,
follow_described_by: bool,
formats: Vec<RdfFormat>,
}
impl ResourceRequestBuilder {
pub fn new(url: Url) -> Self {
Self::with_client_and_url(ClientBuilder::new(Client::new()).build(), url)
}
pub fn with_client_and_url(client: ClientWithMiddleware, url: Url) -> Self {
Self {
client,
url,
follow_described_by: true,
formats: Vec::new(),
}
}
pub fn follow_described_by(mut self, value: bool) -> Self {
self.follow_described_by = value;
self
}
pub fn accept_rdf_format(mut self, format: RdfFormat) -> Self {
self.formats.push(format);
self
}
pub fn accept_all_rdf_formats(mut self) -> Self {
self.formats = vec![
RdfFormat::N3,
RdfFormat::NQuads,
RdfFormat::NTriples,
RdfFormat::RdfXml,
RdfFormat::TriG,
RdfFormat::Turtle,
];
self
}
pub fn build(self) -> ResourceRequest {
ResourceRequest { builder: self }
}
}
#[derive(Clone, Debug)]
pub struct ResourceRequest {
builder: ResourceRequestBuilder,
}
impl ResourceRequest {
fn extract_described_by(response: &Response) -> Option<Url> {
if response.status() == StatusCode::OK {
let headers = response.headers();
for link in headers.get_all(header::LINK) {
let link = link.to_str().unwrap_or("");
match parse_link_header::parse(link) {
Ok(link_map) => {
if let Some(metadata_url) = link_map.get(&Some("describedby".to_string())) {
let raw_url = metadata_url.raw_uri.as_str();
return Url::parse(raw_url).ok();
}
}
Err(err) => error!(err = ?err, "Failed to parse Link header"),
}
}
}
None
}
fn ensure_ldp_support(response: &Response) -> crate::Result<()> {
if response.status() == StatusCode::OK {
let headers = response.headers();
for link in headers.get_all(header::LINK) {
let link = link.to_str().unwrap_or("");
match parse_link_header::parse(link) {
Ok(link_map) => {
if let Some(metadata_url) = link_map.get(&Some("type".to_string())) {
let raw_url = metadata_url.raw_uri.as_str();
if raw_url == vocab::ldp::RESOURCE {
return Ok(());
}
}
}
Err(err) => error!(err = ?err, "Failed to parse Link header"),
}
}
}
Err(crate::Error::LDPUnsupported)
}
fn add_media_types(&self, mut request_builder: RequestBuilder) -> RequestBuilder {
let media_types = self.builder.formats.iter().map(|f| f.media_type());
for media_type in media_types {
request_builder = request_builder.header(header::ACCEPT, media_type);
}
request_builder
}
pub async fn send(&self) -> crate::Result<Resource> {
let request_builder = self.builder.client.head(self.builder.url.clone());
let mut response = request_builder.send().await?.error_for_status()?;
Self::ensure_ldp_support(&response)?;
let url_to_get;
let described_by = Self::extract_described_by(&response);
if let Some(new_url) = &described_by
&& self.builder.follow_described_by
{
url_to_get = new_url.clone();
} else {
url_to_get = self.builder.url.clone();
}
let mut request_builder = self.builder.client.get(url_to_get);
request_builder = self.add_media_types(request_builder);
response = request_builder.send().await?.error_for_status()?;
Self::ensure_ldp_support(&response)?;
let state_token = response
.headers()
.get(crate::header::X_STATE_TOKEN)
.and_then(|hv| hv.to_str().ok().map(|et| et.to_string()));
let format = response
.headers()
.get(header::CONTENT_TYPE)
.and_then(|hv| hv.to_str().ok())
.map(|value| {
if let Some(format) = RdfFormat::from_media_type(value) {
ResponseFormat::RdfFormat(format)
} else {
ResponseFormat::Other(value.to_string())
}
})
.unwrap_or(ResponseFormat::Unspecified);
Ok(Resource {
origin: self.builder.url.clone(),
described_by,
state_token,
format,
response,
})
}
}
pub enum ResponseFormat {
RdfFormat(RdfFormat),
Other(String),
Unspecified,
}
pub struct Resource {
origin: Url,
described_by: Option<Url>,
state_token: Option<String>,
format: ResponseFormat,
response: Response,
}
impl Resource {
pub fn origin(&self) -> &Url {
&self.origin
}
pub fn described_by(&self) -> Option<&Url> {
self.described_by.as_ref()
}
pub fn format(&self) -> &ResponseFormat {
&self.format
}
pub fn state_token(&self) -> Option<&String> {
self.state_token.as_ref()
}
pub fn into_stream(self) -> impl Stream<Item = reqwest_middleware::reqwest::Result<Bytes>> {
self.response.bytes_stream()
}
pub async fn into_rdf_source(self) -> crate::Result<RdfSource> {
if let ResponseFormat::RdfFormat(format) = self.format {
let graph_url = self.described_by.as_ref().unwrap_or(&self.origin);
let graph = GraphNameRef::NamedNode(NamedNodeRef::new_unchecked(graph_url.as_str()));
let parser = RdfParser::from_format(format).with_default_graph(graph);
let body = self.response.bytes().await?;
let quads = parser.for_slice(&body);
let dataset = quads.filter_map(Result::ok).collect::<Dataset>();
Ok(RdfSource {
origin: self.origin,
described_by: self.described_by,
state_token: self.state_token,
dataset,
})
} else {
Err(crate::Error::UnsupportedFormat)
}
}
}