vatcheckapi 0.1.0

Rust client for vatcheckapi.com (status, validate)
Documentation
//! Rust client for the vatcheckapi.com API.
//! Supports authentication via `apikey` header (default) or query parameter.
//! Exposes two endpoints: `status` and `validate`.

use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE, USER_AGENT};
use serde::{Deserialize, Serialize};

const DEFAULT_BASE_URL: &str = "https://vatcheckapi.com/api";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthMethod {
    Header,
    QueryParam,
}

#[derive(thiserror::Error, Debug)]
pub enum VatCheckError {
    #[error("http error: {0}")]
    Http(#[from] reqwest::Error),
    #[error("api returned error: {0}")]
    Api(String),
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct StatusResponse {
    #[serde(default)]
    pub status: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ValidateResponse {
    #[serde(default)]
    pub valid: Option<bool>,
    #[serde(default)]
    pub vat_number: Option<String>,
    #[serde(default)]
    pub country_code: Option<String>,
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default)]
    pub address: Option<String>,
}

pub struct ClientBuilder {
    api_key: String,
    base_url: String,
    auth_method: AuthMethod,
    user_agent: Option<String>,
}

impl ClientBuilder {
    pub fn new<S: Into<String>>(api_key: S) -> Self {
        Self {
            api_key: api_key.into(),
            base_url: DEFAULT_BASE_URL.to_string(),
            auth_method: AuthMethod::Header,
            user_agent: None,
        }
    }

    pub fn with_base_url<S: Into<String>>(mut self, base_url: S) -> Self {
        self.base_url = base_url.into();
        self
    }

    pub fn with_auth_method(mut self, method: AuthMethod) -> Self {
        self.auth_method = method;
        self
    }

    pub fn with_user_agent<S: Into<String>>(mut self, ua: S) -> Self {
        self.user_agent = Some(ua.into());
        self
    }

    pub fn build(self) -> Client {
        let mut default_headers = HeaderMap::new();
        default_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
        if let Some(ua) = self.user_agent.as_ref() {
            if let Ok(h) = HeaderValue::from_str(ua) { default_headers.insert(USER_AGENT, h); }
        } else {
            let _ = default_headers.insert(USER_AGENT, HeaderValue::from_static("vatcheckapi-rs/0.1 (reqwest)"));
        }

        let http = reqwest::Client::builder()
            .default_headers(default_headers)
            .build()
            .expect("failed to build reqwest client");

        Client {
            http,
            api_key: self.api_key,
            base_url: self.base_url,
            auth_method: self.auth_method,
        }
    }
}

pub struct Client {
    http: reqwest::Client,
    api_key: String,
    base_url: String,
    auth_method: AuthMethod,
}

impl Client {
    pub fn builder<S: Into<String>>(api_key: S) -> ClientBuilder { ClientBuilder::new(api_key) }
    pub fn new<S: Into<String>>(api_key: S) -> Self { Self::builder(api_key).build() }

    pub async fn status(&self) -> Result<StatusResponse, VatCheckError> {
        let url = self.join("/status");
        let req = self.apply_auth(self.http.get(url));
        let resp = req.send().await?;
        Self::json_or_error::<StatusResponse>(resp).await
    }

    pub async fn validate<T: serde::Serialize + ?Sized>(&self, params: &T) -> Result<ValidateResponse, VatCheckError> {
        let url = self.join("/validate");
        let req = self.apply_auth(self.http.get(url).query(params));
        let resp = req.send().await?;
        Self::json_or_error::<ValidateResponse>(resp).await
    }

    fn join(&self, path: &str) -> String {
        let base = self.base_url.trim_end_matches('/');
        let p = path.trim_start_matches('/');
        format!("{}/{}", base, p)
    }

    fn apply_auth(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
        match self.auth_method {
            AuthMethod::Header => req.header("apikey", &self.api_key),
            AuthMethod::QueryParam => req.query(&[("apikey", self.api_key.as_str())]),
        }
    }

    async fn json_or_error<T: for<'de> serde::Deserialize<'de>>(resp: reqwest::Response) -> Result<T, VatCheckError> {
        let status = resp.status();
        let text = resp.text().await?;
        if !status.is_success() {
            // try extracting message, else return raw
            if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
                if let Some(m) = v.get("message").and_then(|m| m.as_str()) {
                    return Err(VatCheckError::Api(m.to_string()));
                }
            }
            return Err(VatCheckError::Api(format!("HTTP {}: {}", status, text)));
        }
        let parsed: T = serde_json::from_str(&text).map_err(|e| VatCheckError::Api(format!("invalid json: {}", e)))?;
        Ok(parsed)
    }
}