use crate::{WeChatError, Result};
use reqwest::{Client as HttpClient, header};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tracing::{debug, error};
#[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(),
}
}
}
#[derive(Debug, Clone)]
pub struct Client {
config: ClientConfig,
http_client: HttpClient,
}
impl Client {
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,
})
}
pub fn app_id(&self) -> &str {
&self.config.app_id
}
pub fn app_secret(&self) -> &str {
&self.config.app_secret
}
pub fn http_client(&self) -> &HttpClient {
&self.http_client
}
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
}
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
}
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)));
}
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()),
});
}
}
}
serde_json::from_str(&text).map_err(WeChatError::Json)
}
}
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()
}
}
#[derive(Debug, Deserialize)]
struct ApiResponse {
errcode: Option<i32>,
errmsg: Option<String>,
}