airdatable 0.1.3

wip airtable api w/ data sync utilities
Documentation
use std::{num::NonZero, sync::Arc};

use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
use reqwest::{Client, Method, Url};
use serde::{Serialize, de::DeserializeOwned};

use crate::airtable::{AirtableError, Result};

const MAX_REQUESTS_PER_SECOND: u32 = 5;
const QUOTA: Quota = Quota::per_second(NonZero::new(MAX_REQUESTS_PER_SECOND).unwrap());
const API_BASE_URL: &str = "https://api.airtable.com/v0";

#[derive(Clone)]
pub struct HttpClient {
    client: Client,
    rate_limiter: Arc<DefaultDirectRateLimiter>,
    base_url: String,
    airtable_pat: String,
}

impl HttpClient {
    pub fn new(base_id: String, airtable_pat: String) -> Self {
        Self {
            client: Client::new(),
            rate_limiter: Arc::new(RateLimiter::direct(QUOTA)),
            base_url: format!("{API_BASE_URL}/{base_id}"),
            airtable_pat,
        }
    }

    pub async fn get<Res: DeserializeOwned>(
        &self,
        endpoint: &str,
        params: Option<Vec<(&str, &str)>>,
    ) -> Result<Res> {
        self.request(endpoint, params, None::<&()>, Method::GET)
            .await
    }

    pub async fn request<Req: Serialize, Res: DeserializeOwned>(
        &self,
        endpoint: &str,
        params: Option<Vec<(&str, &str)>>,
        body: Option<&Req>,
        method: Method,
    ) -> Result<Res> {
        self.rate_limiter.until_ready().await;

        let url = {
            let url_without_params = format!("{}{}", self.base_url, endpoint);

            if let Some(params) = params {
                Url::parse_with_params(&url_without_params, params)
            } else {
                Url::parse(&url_without_params)
            }
        }?;

        let request = {
            let request_without_body = self
                .client
                .request(method, url.clone())
                .bearer_auth(self.airtable_pat.clone());

            if let Some(body) = body {
                request_without_body.json(body)
            } else {
                request_without_body
            }
        };

        let response = request.send().await?;
        let status = response.status();

        if status.is_success() {
            let body = response.text().await?;
            serde_json::from_str(&body)
                .map_err(|_| AirtableError::DeserializeSuccessResponse { response: body })
        } else {
            let body = response.text().await?;

            Err(AirtableError::Airtable {
                status,
                response: body,
                url,
            })
        }
    }
}