1use crate::{WeChatError, Result};
2use reqwest::{Client as HttpClient, header};
3use serde::{Deserialize, Serialize};
4use std::time::Duration;
5use tracing::{debug, error};
6
7#[derive(Debug, Clone)]
9pub struct ClientConfig {
10 pub app_id: String,
11 pub app_secret: String,
12 pub timeout: Duration,
13 pub user_agent: String,
14}
15
16impl Default for ClientConfig {
17 fn default() -> Self {
18 Self {
19 app_id: String::new(),
20 app_secret: String::new(),
21 timeout: Duration::from_secs(30),
22 user_agent: "wechat-sdk-rust/0.1.0".to_string(),
23 }
24 }
25}
26
27#[derive(Debug, Clone)]
29pub struct Client {
30 config: ClientConfig,
31 http_client: HttpClient,
32}
33
34impl Client {
35 pub fn new(config: ClientConfig) -> Result<Self> {
37 let mut headers = header::HeaderMap::new();
38 headers.insert(
39 header::USER_AGENT,
40 header::HeaderValue::from_str(&config.user_agent)
41 .map_err(|_| WeChatError::Config("Invalid user agent".to_string()))?,
42 );
43
44 let http_client = HttpClient::builder()
45 .timeout(config.timeout)
46 .default_headers(headers)
47 .build()
48 .map_err(|e| WeChatError::Http(e.to_string()))?;
49
50 Ok(Self {
51 config,
52 http_client,
53 })
54 }
55
56 pub fn app_id(&self) -> &str {
58 &self.config.app_id
59 }
60
61 pub fn app_secret(&self) -> &str {
63 &self.config.app_secret
64 }
65
66 pub fn http_client(&self) -> &HttpClient {
68 &self.http_client
69 }
70
71 pub async fn get<T>(&self, url: &str) -> Result<T>
73 where
74 T: for<'de> Deserialize<'de>,
75 {
76 debug!("GET request to: {}", url);
77
78 let response = self.http_client
79 .get(url)
80 .send()
81 .await
82 .map_err(|e| WeChatError::Http(e.to_string()))?;
83
84 self.handle_response(response).await
85 }
86
87 pub async fn post_json<B, T>(&self, url: &str, body: &B) -> Result<T>
89 where
90 B: Serialize,
91 T: for<'de> Deserialize<'de>,
92 {
93 debug!("POST JSON request to: {}", url);
94
95 let response = self.http_client
96 .post(url)
97 .json(body)
98 .send()
99 .await
100 .map_err(|e| WeChatError::Http(e.to_string()))?;
101
102 self.handle_response(response).await
103 }
104
105 async fn handle_response<T>(&self, response: reqwest::Response) -> Result<T>
107 where
108 T: for<'de> Deserialize<'de>,
109 {
110 let status = response.status();
111 let text = response.text().await.map_err(WeChatError::Reqwest)?;
112
113 debug!("Response status: {}, body: {}", status, text);
114
115 if !status.is_success() {
116 error!("HTTP error: {} - {}", status, text);
117 return Err(WeChatError::Http(format!("HTTP {}: {}", status, text)));
118 }
119
120 if let Ok(api_response) = serde_json::from_str::<ApiResponse>(&text) {
122 if let Some(errcode) = api_response.errcode {
123 if errcode != 0 {
124 return Err(WeChatError::Api {
125 code: errcode,
126 message: api_response.errmsg.unwrap_or_else(|| "Unknown error".to_string()),
127 });
128 }
129 }
130 }
131
132 serde_json::from_str(&text).map_err(WeChatError::Json)
134 }
135}
136
137pub struct ClientBuilder {
139 config: ClientConfig,
140}
141
142impl ClientBuilder {
143 pub fn new() -> Self {
144 Self {
145 config: ClientConfig::default(),
146 }
147 }
148
149 pub fn app_id<S: Into<String>>(mut self, app_id: S) -> Self {
150 self.config.app_id = app_id.into();
151 self
152 }
153
154 pub fn app_secret<S: Into<String>>(mut self, app_secret: S) -> Self {
155 self.config.app_secret = app_secret.into();
156 self
157 }
158
159 pub fn timeout(mut self, timeout: Duration) -> Self {
160 self.config.timeout = timeout;
161 self
162 }
163
164 pub fn user_agent<S: Into<String>>(mut self, user_agent: S) -> Self {
165 self.config.user_agent = user_agent.into();
166 self
167 }
168
169 pub fn build(self) -> Result<Client> {
170 if self.config.app_id.is_empty() {
171 return Err(WeChatError::Config("app_id is required".to_string()));
172 }
173 if self.config.app_secret.is_empty() {
174 return Err(WeChatError::Config("app_secret is required".to_string()));
175 }
176
177 Client::new(self.config)
178 }
179}
180
181impl Default for ClientBuilder {
182 fn default() -> Self {
183 Self::new()
184 }
185}
186
187#[derive(Debug, Deserialize)]
189struct ApiResponse {
190 errcode: Option<i32>,
191 errmsg: Option<String>,
192}