wechat-api-rs 0.1.0

A Rust SDK for WeChat Official Account and Mini Program APIs
Documentation
use crate::{WeChatError, Result};
use reqwest::{Client as HttpClient, header};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tracing::{debug, error};

/// WeChat HTTP client configuration
#[derive(Debug, Clone)]
pub struct ClientConfig {
    pub app_id: String,
    pub app_secret: String,
    pub timeout: Duration,
    pub user_agent: String,
}

impl Default for ClientConfig {
    fn default() -> Self {
        Self {
            app_id: String::new(),
            app_secret: String::new(),
            timeout: Duration::from_secs(30),
            user_agent: "wechat-sdk-rust/0.1.0".to_string(),
        }
    }
}

/// WeChat HTTP client
#[derive(Debug, Clone)]
pub struct Client {
    config: ClientConfig,
    http_client: HttpClient,
}

impl Client {
    /// Create a new client with configuration
    pub fn new(config: ClientConfig) -> Result<Self> {
        let mut headers = header::HeaderMap::new();
        headers.insert(
            header::USER_AGENT,
            header::HeaderValue::from_str(&config.user_agent)
                .map_err(|_| WeChatError::Config("Invalid user agent".to_string()))?,
        );

        let http_client = HttpClient::builder()
            .timeout(config.timeout)
            .default_headers(headers)
            .build()
            .map_err(|e| WeChatError::Http(e.to_string()))?;

        Ok(Self {
            config,
            http_client,
        })
    }

    /// Get the app ID
    pub fn app_id(&self) -> &str {
        &self.config.app_id
    }

    /// Get the app secret
    pub fn app_secret(&self) -> &str {
        &self.config.app_secret
    }

    /// Get the HTTP client
    pub fn http_client(&self) -> &HttpClient {
        &self.http_client
    }

    /// Send a GET request
    pub async fn get<T>(&self, url: &str) -> Result<T>
    where
        T: for<'de> Deserialize<'de>,
    {
        debug!("GET request to: {}", url);
        
        let response = self.http_client
            .get(url)
            .send()
            .await
            .map_err(|e| WeChatError::Http(e.to_string()))?;

        self.handle_response(response).await
    }

    /// Send a POST request with JSON body
    pub async fn post_json<B, T>(&self, url: &str, body: &B) -> Result<T>
    where
        B: Serialize,
        T: for<'de> Deserialize<'de>,
    {
        debug!("POST JSON request to: {}", url);
        
        let response = self.http_client
            .post(url)
            .json(body)
            .send()
            .await
            .map_err(|e| WeChatError::Http(e.to_string()))?;

        self.handle_response(response).await
    }

    /// Handle HTTP response and parse JSON
    async fn handle_response<T>(&self, response: reqwest::Response) -> Result<T>
    where
        T: for<'de> Deserialize<'de>,
    {
        let status = response.status();
        let text = response.text().await.map_err(WeChatError::Reqwest)?;

        debug!("Response status: {}, body: {}", status, text);

        if !status.is_success() {
            error!("HTTP error: {} - {}", status, text);
            return Err(WeChatError::Http(format!("HTTP {}: {}", status, text)));
        }

        // Try to parse as WeChat API response first
        if let Ok(api_response) = serde_json::from_str::<ApiResponse>(&text) {
            if let Some(errcode) = api_response.errcode {
                if errcode != 0 {
                    return Err(WeChatError::Api {
                        code: errcode,
                        message: api_response.errmsg.unwrap_or_else(|| "Unknown error".to_string()),
                    });
                }
            }
        }

        // Parse as the expected type
        serde_json::from_str(&text).map_err(WeChatError::Json)
    }
}

/// Builder for Client
pub struct ClientBuilder {
    config: ClientConfig,
}

impl ClientBuilder {
    pub fn new() -> Self {
        Self {
            config: ClientConfig::default(),
        }
    }

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

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

    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.config.timeout = timeout;
        self
    }

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

    pub fn build(self) -> Result<Client> {
        if self.config.app_id.is_empty() {
            return Err(WeChatError::Config("app_id is required".to_string()));
        }
        if self.config.app_secret.is_empty() {
            return Err(WeChatError::Config("app_secret is required".to_string()));
        }
        
        Client::new(self.config)
    }
}

impl Default for ClientBuilder {
    fn default() -> Self {
        Self::new()
    }
}

/// Generic WeChat API response
#[derive(Debug, Deserialize)]
struct ApiResponse {
    errcode: Option<i32>,
    errmsg: Option<String>,
}