beeline 0.1.0

Async Rust helpers for Foursquare/Swarm OAuth and latest checkin polling.
Documentation
use crate::models::{ApiMeta, RateLimit};
use reqwest::StatusCode;
use thiserror::Error as ThisError;
use url::Url;

pub type Result<T> = std::result::Result<T, Error>;

#[derive(ThisError, Debug)]
pub enum Error {
    #[error("failed to build URL: {0}")]
    Url(#[from] url::ParseError),

    #[error("HTTP request failed: {source}")]
    Request {
        #[source]
        source: reqwest::Error,
    },

    #[error("failed to decode JSON response: {0}")]
    Decode(#[from] serde_json::Error),

    #[error("Foursquare callback returned error {error}: {description:?}")]
    CallbackDenied {
        error: String,
        description: Option<String>,
    },

    #[error("authorization callback did not include a code")]
    MissingCallbackCode,

    #[error("invalid OAuth state: {0}")]
    InvalidState(String),

    #[error("OAuth state has expired")]
    ExpiredState {
        issued_at: i64,
        max_age_seconds: u64,
    },

    #[error("bad request to Foursquare API")]
    BadRequest {
        meta: Option<Box<ApiMeta>>,
        body: Option<String>,
        rate_limit: RateLimit,
    },

    #[error("OAuth token is missing, invalid, or expired")]
    InvalidAuth {
        meta: Option<Box<ApiMeta>>,
        body: Option<String>,
        rate_limit: RateLimit,
    },

    #[error("Foursquare denied access for this token")]
    Forbidden {
        meta: Option<Box<ApiMeta>>,
        body: Option<String>,
        rate_limit: RateLimit,
    },

    #[error("Foursquare rate limit exceeded")]
    RateLimited {
        reset_at: Option<i64>,
        limit: Option<u64>,
        remaining: Option<u64>,
        meta: Option<Box<ApiMeta>>,
    },

    #[error("Foursquare daily quota exceeded")]
    QuotaExceeded {
        meta: Option<Box<ApiMeta>>,
        body: Option<String>,
        rate_limit: RateLimit,
    },

    #[error("Foursquare server error: {status}")]
    Server {
        status: StatusCode,
        meta: Option<Box<ApiMeta>>,
        body: Option<String>,
        rate_limit: RateLimit,
    },

    #[error("Foursquare API error: {status}")]
    Api {
        status: StatusCode,
        meta: Option<Box<ApiMeta>>,
        body: Option<String>,
        rate_limit: RateLimit,
    },

    #[error("malformed Foursquare response: {0}")]
    MalformedResponse(String),

    #[error("Foursquare response body exceeded {limit} bytes")]
    ResponseTooLarge { limit: usize },

    #[error("insecure {name} endpoint rejected: {url}")]
    InsecureEndpoint { name: &'static str, url: Url },
}

impl Error {
    pub(crate) fn request(source: reqwest::Error) -> Self {
        Self::Request {
            source: source.without_url(),
        }
    }

    pub fn is_invalid_auth(&self) -> bool {
        matches!(self, Error::InvalidAuth { .. })
    }

    pub fn is_rate_limited(&self) -> bool {
        matches!(
            self,
            Error::RateLimited { .. } | Error::QuotaExceeded { .. }
        )
    }

    pub fn is_retryable(&self) -> bool {
        matches!(self, Error::RateLimited { .. } | Error::Server { .. })
    }
}

impl From<reqwest::Error> for Error {
    fn from(source: reqwest::Error) -> Self {
        Self::request(source)
    }
}