paper-gap 0.3.2

Local CLI for finding research papers with missing, weak, or stale public code
Documentation
use crate::Error;
use serde::{Deserialize, Serialize};
use std::time::Duration;

const RETRY_DELAY: Duration = Duration::from_millis(250);
const MAX_RETRY_AFTER: Duration = Duration::from_secs(5);

pub fn retry_delay(status: reqwest::StatusCode, retry_after: Option<&str>) -> Option<Duration> {
    match status {
        reqwest::StatusCode::TOO_MANY_REQUESTS => Some(
            retry_after
                .and_then(|value| value.parse::<u64>().ok())
                .map(Duration::from_secs)
                .unwrap_or(RETRY_DELAY)
                .min(MAX_RETRY_AFTER),
        ),
        reqwest::StatusCode::BAD_GATEWAY
        | reqwest::StatusCode::SERVICE_UNAVAILABLE
        | reqwest::StatusCode::GATEWAY_TIMEOUT => Some(RETRY_DELAY),
        _ => None,
    }
}

pub async fn send_with_retry(
    request: reqwest::RequestBuilder,
) -> reqwest::Result<reqwest::Response> {
    let retry = request.try_clone();
    match request.send().await {
        Ok(response) => {
            let delay = retry_delay(
                response.status(),
                response
                    .headers()
                    .get(reqwest::header::RETRY_AFTER)
                    .and_then(|value| value.to_str().ok()),
            );
            if let (Some(request), Some(delay)) = (retry, delay) {
                tokio::time::sleep(delay).await;
                request.send().await
            } else {
                Ok(response)
            }
        }
        Err(error) if error.is_connect() || error.is_timeout() => {
            if let Some(request) = retry {
                tokio::time::sleep(RETRY_DELAY).await;
                request.send().await
            } else {
                Err(error)
            }
        }
        Err(error) => Err(error),
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProviderOutcome {
    pub provider: String,
    pub status: ProviderStatus,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ProviderStatus {
    Success,
    Unavailable,
    RateLimited,
    Failed,
}

impl ProviderOutcome {
    pub fn success(provider: impl Into<String>) -> Self {
        Self {
            provider: provider.into(),
            status: ProviderStatus::Success,
            message: None,
        }
    }

    pub fn from_error(provider: impl Into<String>, error: &Error) -> Self {
        let status = match error {
            Error::Reqwest(error)
                if error.status() == Some(reqwest::StatusCode::TOO_MANY_REQUESTS) =>
            {
                ProviderStatus::RateLimited
            }
            Error::Reqwest(error) if error.is_timeout() || error.is_connect() => {
                ProviderStatus::Unavailable
            }
            _ => ProviderStatus::Failed,
        };
        Self {
            provider: provider.into(),
            status,
            message: Some(error.to_string()),
        }
    }

    pub fn succeeded(&self) -> bool {
        self.status == ProviderStatus::Success
    }

    pub fn aggregate(provider: impl Into<String>, outcomes: &[Self]) -> Self {
        let status = outcomes
            .iter()
            .map(|outcome| outcome.status)
            .max_by_key(|status| match status {
                ProviderStatus::Success => 0,
                ProviderStatus::Unavailable => 1,
                ProviderStatus::RateLimited => 2,
                ProviderStatus::Failed => 3,
            })
            .unwrap_or(ProviderStatus::Success);
        let messages: Vec<_> = outcomes
            .iter()
            .filter_map(|outcome| outcome.message.as_deref())
            .collect();
        Self {
            provider: provider.into(),
            status,
            message: (!messages.is_empty()).then(|| messages.join("; ")),
        }
    }
}

pub fn default_success() -> ProviderOutcome {
    ProviderOutcome::success("legacy")
}

impl std::fmt::Display for ProviderStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            Self::Success => "success",
            Self::Unavailable => "unavailable",
            Self::RateLimited => "rate_limited",
            Self::Failed => "failed",
        })
    }
}