mod data;
pub use data::*;
use reqwest::{Client, ClientBuilder, IntoUrl, Method, Response};
use std::fmt::Debug;
use std::future::Future;
use std::marker::PhantomData;
use std::time::Duration;
use url::Url;
#[derive(Clone, Debug)]
pub struct Fetcher {
client: Client,
retries: usize,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Request error: {0}")]
Request(#[from] reqwest::Error),
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct FetcherOptions {
pub timeout: Duration,
pub retries: usize,
}
impl FetcherOptions {
pub fn new() -> Self {
Self::default()
}
pub fn timeout(mut self, timeout: impl Into<Duration>) -> Self {
self.timeout = timeout.into();
self
}
pub fn retries(mut self, retries: usize) -> Self {
self.retries = retries;
self
}
}
impl Default for FetcherOptions {
fn default() -> Self {
Self {
timeout: Duration::from_secs(30),
retries: 5,
}
}
}
impl From<Client> for Fetcher {
fn from(client: Client) -> Self {
Self::with_client(client, FetcherOptions::default())
}
}
impl Fetcher {
pub async fn new(options: FetcherOptions) -> anyhow::Result<Self> {
let client = ClientBuilder::new().timeout(options.timeout);
Ok(Self::with_client(client.build()?, options))
}
fn with_client(client: Client, options: FetcherOptions) -> Self {
Self {
client,
retries: options.retries,
}
}
async fn new_request(
&self,
method: Method,
url: Url,
) -> Result<reqwest::RequestBuilder, reqwest::Error> {
Ok(self.client.request(method, url))
}
pub async fn fetch<D: Data>(&self, url: impl IntoUrl) -> Result<D, Error> {
log::debug!("Fetching: {}", url.as_str());
self.fetch_processed(url, TypedProcessor::<D>::new()).await
}
pub async fn fetch_processed<D: DataProcessor>(
&self,
url: impl IntoUrl,
processor: D,
) -> Result<D::Type, Error> {
let url = url.into_url()?;
let mut retries = self.retries;
loop {
match self.fetch_once(url.clone(), &processor).await {
Ok(result) => break Ok(result),
Err(err) => {
log::info!("Failed to retrieve (retries: {retries}): {err}");
if retries > 0 {
retries -= 1;
} else {
break Err(err);
}
}
}
}
}
async fn fetch_once<D: DataProcessor>(
&self,
url: Url,
processor: &D,
) -> Result<D::Type, Error> {
let response = self.new_request(Method::GET, url).await?.send().await?;
Ok(processor.process(response).await?)
}
}
pub trait DataProcessor {
type Type: Sized;
fn process(
&self,
response: reqwest::Response,
) -> impl Future<Output = Result<Self::Type, reqwest::Error>>;
}
struct TypedProcessor<D: Data> {
_marker: PhantomData<D>,
}
impl<D: Data> TypedProcessor<D> {
pub const fn new() -> Self {
Self {
_marker: PhantomData::<D>,
}
}
}
impl<D: Data> DataProcessor for TypedProcessor<D> {
type Type = D;
async fn process(&self, response: Response) -> Result<Self::Type, reqwest::Error> {
D::from_response(response).await
}
}