use crate::{LoadError, Profile};
use super::{LD_JSON_MEDIA_TYPE, Loader, RemoteDocument};
use crate::HashSet;
use iri_rs::{Iri, IriBuf};
use jstrict::Parse;
use reqwest::{
StatusCode,
header::{ACCEPT, CONTENT_TYPE, LINK},
};
use reqwest_middleware::ClientWithMiddleware;
mod content_type;
mod link;
use content_type::ContentType;
use link::Link;
pub struct Options {
pub request_profile: Vec<Profile>,
pub max_redirections: usize,
pub max_document_size: Option<usize>,
pub timeout: Option<std::time::Duration>,
pub client: ClientWithMiddleware,
}
impl Default for Options {
fn default() -> Self {
Self {
request_profile: Vec::new(),
max_redirections: 8,
max_document_size: Some(32 * 1024 * 1024),
timeout: Some(std::time::Duration::from_mins(1)),
client: reqwest_middleware::ClientBuilder::new(reqwest::Client::default()).build(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("HTTP request failed: {0}")]
Reqwest(reqwest_middleware::Error),
#[error("query failed: status code {0}")]
QueryFailed(StatusCode),
#[error("invalid content type")]
InvalidContentType,
#[error("multiple context link headers")]
MultipleContextLinkHeaders,
#[error("too many redirections")]
TooManyRedirections,
#[error("document exceeds the size limit ({0} bytes)")]
TooLarge(usize),
#[error("JSON parse error: {0}")]
Parse(jstrict::parse::Error<utf8_decode::Utf8Error>),
}
pub struct ReqwestLoader {
options: Options,
accept_header: String,
}
impl Default for ReqwestLoader {
fn default() -> Self {
Self::new_using(Options::default())
}
}
impl ReqwestLoader {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn new_using(options: Options) -> Self {
let mut json_ld_params = String::new();
if !options.request_profile.is_empty() {
json_ld_params.push_str("; profile=");
if options.request_profile.len() > 1 {
json_ld_params.push('"');
}
for (i, p) in options.request_profile.iter().enumerate() {
if i > 0 {
json_ld_params.push(' ');
}
json_ld_params.push_str(p.iri().as_str());
}
if options.request_profile.len() > 1 {
json_ld_params.push('"');
}
}
Self {
options,
accept_header: format!("application/ld+json{json_ld_params}, application/json"),
}
}
}
impl Loader for ReqwestLoader {
type Error = Error;
async fn load(&self, url: Iri<&str>) -> Result<RemoteDocument<IriBuf>, LoadError<Self::Error>> {
let mut redirection_number = 0;
let mut url: IriBuf = url.into();
'next_url: loop {
if redirection_number > self.options.max_redirections {
return Err(LoadError::new(url.clone(), Error::TooManyRedirections));
}
log::debug!("downloading: {url}");
let mut request = self.options.client.get(url.as_str()).header(ACCEPT, &self.accept_header);
if let Some(timeout) = self.options.timeout {
request = request.timeout(timeout);
}
let mut response = request.send().await.map_err(|e| LoadError::new(url.clone(), Error::Reqwest(e)))?;
match response.status() {
StatusCode::OK => {
let mut content_types = response.headers().get_all(CONTENT_TYPE).into_iter().filter_map(ContentType::new);
if let Some(content_type) = content_types.find(ContentType::is_json_ld) {
let mut context_url = None;
if *content_type.media_type() != LD_JSON_MEDIA_TYPE {
for link in response.headers().get_all(LINK).into_iter().flat_map(Link::parse_header) {
if link.rel() == Some(b"http://www.w3.org/ns/json-ld#context") {
if context_url.is_some() {
return Err(LoadError::new(url, Error::MultipleContextLinkHeaders));
}
if let Ok(resolved) = link.href().resolved(&url)
&& let Ok(iri) = IriBuf::try_from(resolved)
{
context_url = Some(iri);
}
}
}
}
let mut profile = HashSet::default();
for p in content_type.profile().into_iter().flat_map(|p| p.split(|b| *b == b' ')) {
if let Ok(p) = std::str::from_utf8(p)
&& let Ok(iri) = Iri::parse(p)
{
profile.insert(Profile::new(iri));
}
}
if let (Some(limit), Some(len)) = (self.options.max_document_size, response.content_length())
&& len > limit as u64
{
return Err(LoadError::new(url, Error::TooLarge(limit)));
}
let mut bytes = Vec::new();
while let Some(chunk) = response.chunk().await.map_err(|e| LoadError::new(url.clone(), Error::Reqwest(e.into())))? {
if let Some(limit) = self.options.max_document_size
&& bytes.len() + chunk.len() > limit
{
return Err(LoadError::new(url, Error::TooLarge(limit)));
}
bytes.extend_from_slice(&chunk);
}
let decoder = utf8_decode::Decoder::new(bytes.iter().copied());
let (document, _) = jstrict::Value::parse_utf8(decoder).map_err(|e| LoadError::new(url.clone(), Error::Parse(e)))?;
break Ok(RemoteDocument::new_full(
Some(url),
Some(content_type.into_media_type()),
context_url,
profile,
document,
));
}
log::debug!("no valid media type found");
for link in response.headers().get_all(LINK).into_iter().flat_map(Link::parse_header) {
if link.rel() == Some(b"alternate") && link.type_() == Some(b"application/ld+json") {
log::debug!("link found");
if let Ok(resolved) = link.href().resolved(&url)
&& let Ok(next) = IriBuf::try_from(resolved)
{
url = next;
redirection_number += 1;
continue 'next_url;
}
}
}
break Err(LoadError::new(url, Error::InvalidContentType));
}
code => break Err(LoadError::new(url, Error::QueryFailed(code))),
}
}
}
}