keplars 2.1.0

Official Rust SDK for Keplars Email API
Documentation
use reqwest::{Client, Method};
use serde::Serialize;
use serde_json::Value;
use tokio::time::sleep;

use crate::errors::{KeplarsError, KeplarsResult};
use crate::models::KeplarsConfig;
use crate::utils::exponential_backoff;

const API_KEY_REGEX: &str = r"^kms_[a-f0-9]+\.(live|adm)_[a-f0-9]+$";

pub struct Keplars {
    pub(crate) config: KeplarsConfig,
    pub(crate) http: Client,
}

impl Keplars {
    pub fn new(api_key: impl Into<String>) -> KeplarsResult<Self> {
        Self::with_config(KeplarsConfig {
            api_key: api_key.into(),
            ..Default::default()
        })
    }

    pub fn with_config(config: KeplarsConfig) -> KeplarsResult<Self> {
        if config.api_key.is_empty() {
            return Err(KeplarsError::Validation {
                message: "API key is required".to_string(),
                status_code: 0,
            });
        }

        let re = regex::Regex::new(API_KEY_REGEX).unwrap();
        if !re.is_match(&config.api_key) {
            return Err(KeplarsError::Validation {
                message: "Invalid API key format. Expected: kms_<id>.live_<secret> or kms_<id>.adm_<secret>".to_string(),
                status_code: 0,
            });
        }

        let http = Client::builder()
            .timeout(config.timeout)
            .user_agent("keplars-rust/1.1.0")
            .build()
            .map_err(KeplarsError::Network)?;

        Ok(Self { config, http })
    }

    pub(crate) async fn request<T, B>(
        &self,
        method: Method,
        path: &str,
        body: Option<&B>,
        retry_count: u32,
    ) -> KeplarsResult<T>
    where
        T: serde::de::DeserializeOwned,
        B: Serialize,
    {
        let url = format!("{}{}", self.config.base_url, path);
        let mut req = self
            .http
            .request(method.clone(), &url)
            .header("Authorization", format!("Bearer {}", self.config.api_key))
            .header("Content-Type", "application/json");

        if let Some(b) = body {
            req = req.json(b);
        }

        let response = match req.send().await {
            Ok(r) => r,
            Err(e) => {
                if retry_count < self.config.max_retries {
                    let delay = exponential_backoff(retry_count, self.config.retry_delay);
                    sleep(delay).await;
                    return Box::pin(self.request(method, path, body, retry_count + 1)).await;
                }
                return Err(KeplarsError::Network(e));
            }
        };

        let status = response.status();

        if status.is_success() {
            let data = response.json::<T>().await.map_err(KeplarsError::Network)?;
            return Ok(data);
        }

        let status_u16 = status.as_u16();
        let error_body: Value = response.json().await.unwrap_or(Value::Null);
        let message = error_body
            .get("message")
            .and_then(|v| v.as_str())
            .unwrap_or("Unknown error")
            .to_string();

        if (status_u16 >= 500) && retry_count < self.config.max_retries {
            let delay = exponential_backoff(retry_count, self.config.retry_delay);
            sleep(delay).await;
            return Box::pin(self.request(method, path, body, retry_count + 1)).await;
        }

        Err(KeplarsError::from_status(status_u16, message))
    }

    pub(crate) async fn get<T>(&self, path: &str) -> KeplarsResult<T>
    where
        T: serde::de::DeserializeOwned,
    {
        self.request::<T, ()>(Method::GET, path, None, 0).await
    }

    pub(crate) async fn post<T, B>(&self, path: &str, body: &B) -> KeplarsResult<T>
    where
        T: serde::de::DeserializeOwned,
        B: Serialize,
    {
        self.request(Method::POST, path, Some(body), 0).await
    }

    pub(crate) async fn patch<T, B>(&self, path: &str, body: &B) -> KeplarsResult<T>
    where
        T: serde::de::DeserializeOwned,
        B: Serialize,
    {
        self.request(Method::PATCH, path, Some(body), 0).await
    }

    pub(crate) async fn delete<T>(&self, path: &str) -> KeplarsResult<T>
    where
        T: serde::de::DeserializeOwned,
    {
        self.request::<T, ()>(Method::DELETE, path, None, 0).await
    }

    pub(crate) async fn delete_with_body<T, B>(&self, path: &str, body: &B) -> KeplarsResult<T>
    where
        T: serde::de::DeserializeOwned,
        B: Serialize,
    {
        self.request(Method::DELETE, path, Some(body), 0).await
    }
}