wechat_mp_sdk/client/
wechat_client.rs1use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
6use reqwest::Client;
7use serde::de::DeserializeOwned;
8use std::future::Future;
9use std::pin::Pin;
10use std::sync::Arc;
11use std::task::{Context, Poll};
12use std::time::Duration;
13use tower::Service;
14
15use crate::error::{HttpError, WechatError};
16use crate::types::{AppId, AppSecret};
17
18pub(crate) const DEFAULT_BASE_URL: &str = "https://api.weixin.qq.com";
19pub(crate) const DEFAULT_TIMEOUT_SECS: u64 = 30;
20pub(crate) const DEFAULT_CONNECT_TIMEOUT_SECS: u64 = 10;
21
22type MiddlewareFuture =
23 Pin<Box<dyn Future<Output = Result<reqwest::Response, reqwest::Error>> + Send>>;
24type MiddlewareExecutor = Arc<dyn Fn(reqwest::Request) -> MiddlewareFuture + Send + Sync>;
25
26#[derive(Clone)]
31pub struct WechatClient {
32 http: Client,
33 appid: AppId,
34 secret: AppSecret,
35 base_url: String,
36 middleware_executor: Option<MiddlewareExecutor>,
37}
38
39impl std::fmt::Debug for WechatClient {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 f.debug_struct("WechatClient")
42 .field("appid", &self.appid)
43 .field("base_url", &self.base_url)
44 .field(
45 "middleware_executor",
46 &self.middleware_executor.as_ref().map(|_| ".."),
47 )
48 .finish_non_exhaustive()
49 }
50}
51
52impl WechatClient {
53 pub fn builder() -> WechatClientBuilder {
55 WechatClientBuilder::default()
56 }
57
58 pub fn appid(&self) -> &str {
60 self.appid.as_str()
61 }
62
63 pub(crate) fn secret(&self) -> &str {
65 self.secret.as_str()
66 }
67
68 pub fn base_url(&self) -> &str {
70 &self.base_url
71 }
72
73 pub(crate) fn append_access_token(path: &str, access_token: &str) -> String {
74 let encoded = utf8_percent_encode(access_token, NON_ALPHANUMERIC);
75
76 if path.contains("access_token={}") {
77 return path.replacen("access_token={}", &format!("access_token={encoded}"), 1);
78 }
79
80 let separator = if path.contains('?') { '&' } else { '?' };
81 format!("{path}{separator}access_token={encoded}")
82 }
83
84 pub fn http(&self) -> &Client {
89 &self.http
90 }
91
92 pub(crate) fn with_middleware_executor(mut self, executor: MiddlewareExecutor) -> Self {
93 self.middleware_executor = Some(executor);
94 self
95 }
96
97 pub(crate) async fn send_request(
98 &self,
99 request: reqwest::Request,
100 ) -> Result<reqwest::Response, reqwest::Error> {
101 if let Some(executor) = &self.middleware_executor {
102 (executor)(request).await
103 } else {
104 self.http.execute(request).await
105 }
106 }
107
108 async fn execute<T: DeserializeOwned>(
109 &self,
110 request: reqwest::Request,
111 ) -> Result<T, WechatError> {
112 let response = self.send_request(request).await?;
113
114 if let Err(e) = response.error_for_status_ref() {
115 return Err(e.into());
116 }
117
118 let value: serde_json::Value = response.json().await?;
119
120 if let Some(errcode) = value.get("errcode").and_then(|v| v.as_i64()) {
121 if errcode != 0 {
122 let errmsg = value
123 .get("errmsg")
124 .and_then(|v| v.as_str())
125 .unwrap_or("unknown error");
126 return Err(WechatError::Api {
127 code: errcode.try_into().unwrap_or(i32::MAX),
128 message: errmsg.to_string(),
129 });
130 }
131 }
132
133 serde_json::from_value(value)
134 .map_err(|e| WechatError::Http(HttpError::Decode(e.to_string())))
135 }
136
137 pub async fn get<T: DeserializeOwned>(
150 &self,
151 path: &str,
152 query: &[(&str, &str)],
153 ) -> Result<T, WechatError> {
154 let url = format!("{}{}", self.base_url, path);
155 let request = self.http.get(url).query(query).build()?;
156 self.execute(request).await
157 }
158
159 pub async fn post<T: DeserializeOwned, B: serde::Serialize>(
172 &self,
173 path: &str,
174 body: &B,
175 ) -> Result<T, WechatError> {
176 let url = format!("{}{}", self.base_url, path);
177 let request = self.http.post(url).json(body).build()?;
178 self.execute(request).await
179 }
180}
181
182impl Service<reqwest::Request> for WechatClient {
183 type Response = reqwest::Response;
184 type Error = reqwest::Error;
185 type Future = MiddlewareFuture;
186
187 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
188 Poll::Ready(Ok(()))
189 }
190
191 fn call(&mut self, req: reqwest::Request) -> Self::Future {
192 let client = self.http.clone();
193 Box::pin(async move { client.execute(req).await })
194 }
195}
196
197#[derive(Debug, Default)]
219pub struct WechatClientBuilder {
220 appid: Option<AppId>,
221 secret: Option<AppSecret>,
222 base_url: Option<String>,
223 timeout: Option<Duration>,
224 connect_timeout: Option<Duration>,
225}
226
227impl WechatClientBuilder {
228 pub fn appid(mut self, appid: AppId) -> Self {
230 self.appid = Some(appid);
231 self
232 }
233
234 pub fn secret(mut self, secret: AppSecret) -> Self {
236 self.secret = Some(secret);
237 self
238 }
239
240 pub fn base_url(mut self, url: impl Into<String>) -> Self {
244 self.base_url = Some(url.into());
245 self
246 }
247
248 pub fn timeout(mut self, timeout: Duration) -> Self {
252 self.timeout = Some(timeout);
253 self
254 }
255
256 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
260 self.connect_timeout = Some(timeout);
261 self
262 }
263
264 pub fn build(self) -> Result<WechatClient, WechatError> {
269 let appid = self
270 .appid
271 .ok_or_else(|| WechatError::Config("appid is required".to_string()))?;
272 let secret = self
273 .secret
274 .ok_or_else(|| WechatError::Config("secret is required".to_string()))?;
275
276 let base_url = self
277 .base_url
278 .unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
279
280 let timeout = self
281 .timeout
282 .unwrap_or(Duration::from_secs(DEFAULT_TIMEOUT_SECS));
283 let connect_timeout = self
284 .connect_timeout
285 .unwrap_or(Duration::from_secs(DEFAULT_CONNECT_TIMEOUT_SECS));
286
287 let client = Client::builder()
288 .timeout(timeout)
289 .connect_timeout(connect_timeout)
290 .build()?;
291
292 Ok(WechatClient {
293 http: client,
294 appid,
295 secret,
296 base_url,
297 middleware_executor: None,
298 })
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_builder_default_values() {
308 let appid = AppId::new("wx1234567890abcdef").unwrap();
309 let secret = AppSecret::new("secret1234567890ab").unwrap();
310
311 let client = WechatClient::builder()
312 .appid(appid.clone())
313 .secret(secret.clone())
314 .build()
315 .unwrap();
316
317 assert_eq!(client.appid(), appid.as_str());
318 assert_eq!(client.base_url(), DEFAULT_BASE_URL);
319 }
320
321 #[test]
322 fn test_builder_custom_base_url() {
323 let appid = AppId::new("wx1234567890abcdef").unwrap();
324 let secret = AppSecret::new("secret1234567890ab").unwrap();
325
326 let client = WechatClient::builder()
327 .appid(appid)
328 .secret(secret)
329 .base_url("https://custom.api.example.com")
330 .build()
331 .unwrap();
332
333 assert_eq!(client.base_url(), "https://custom.api.example.com");
334 }
335
336 #[test]
337 fn test_builder_custom_timeouts() {
338 let appid = AppId::new("wx1234567890abcdef").unwrap();
339 let secret = AppSecret::new("secret1234567890ab").unwrap();
340
341 let client = WechatClient::builder()
342 .appid(appid)
343 .secret(secret)
344 .timeout(Duration::from_secs(60))
345 .connect_timeout(Duration::from_secs(5))
346 .build()
347 .unwrap();
348
349 assert_eq!(client.base_url(), DEFAULT_BASE_URL);
353 assert_eq!(client.appid(), "wx1234567890abcdef");
354 }
355
356 #[test]
357 fn test_builder_missing_appid() {
358 let secret = AppSecret::new("secret1234567890ab").unwrap();
359
360 let result = WechatClient::builder().secret(secret).build();
361
362 assert!(result.is_err());
363 }
364
365 #[test]
366 fn test_builder_missing_secret() {
367 let appid = AppId::new("wx1234567890abcdef").unwrap();
368
369 let result = WechatClient::builder().appid(appid).build();
370
371 assert!(result.is_err());
372 }
373}