wecomx_runtime/
builder.rs1use std::sync::Arc;
8use std::time::Duration;
9
10use wecomx_auth::TokenProvider;
11use wecomx_transport::{HttpTransportBackend, Transport};
12
13use crate::backend::WecomBackend;
14use crate::client::WecomClient;
15
16pub const DEFAULT_BASE_URL: &str = "https://qyapi.weixin.qq.com/cli";
21
22pub const DEFAULT_BIN_NAME: &str = "wecom-cli";
24
25#[derive(Clone)]
37pub struct WecomClientBuilder {
38 base_url: String,
39 provider: Option<Arc<dyn TokenProvider>>,
40 initial_token: Option<String>,
41 timeout: Option<Duration>,
42 headers: Vec<(String, String)>,
43 bin_name: String,
44}
45
46impl Default for WecomClientBuilder {
47 fn default() -> Self {
48 Self::new()
49 }
50}
51
52impl WecomClientBuilder {
53 pub fn new() -> Self {
55 Self {
56 base_url: DEFAULT_BASE_URL.to_string(),
57 provider: None,
58 initial_token: None,
59 timeout: None,
60 headers: Vec::new(),
61 bin_name: DEFAULT_BIN_NAME.to_string(),
62 }
63 }
64
65 #[must_use]
67 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
68 self.base_url = base_url.into();
69 self
70 }
71
72 #[must_use]
74 pub fn token_provider(mut self, provider: Arc<dyn TokenProvider>) -> Self {
75 self.provider = Some(provider);
76 self
77 }
78
79 #[must_use]
81 pub fn initial_token(mut self, token: impl Into<String>) -> Self {
82 self.initial_token = Some(token.into());
83 self
84 }
85
86 #[must_use]
88 pub fn timeout(mut self, timeout: Duration) -> Self {
89 self.timeout = Some(timeout);
90 self
91 }
92
93 #[must_use]
95 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
96 self.headers.push((name.into(), value.into()));
97 self
98 }
99
100 #[must_use]
102 pub fn bin_name(mut self, bin_name: impl Into<String>) -> Self {
103 self.bin_name = bin_name.into();
104 self
105 }
106
107 pub async fn build(self) -> Result<WecomClient, wecomx::Error> {
118 let transport = self.build_transport().await.map_err(wecomx::Error::from)?;
119 WecomClient::from_transport(transport)
120 }
121
122 pub async fn build_transport(self) -> Result<Transport, wecomx_transport::Error> {
136 let mut builder = HttpTransportBackend::builder().base_url(self.base_url);
137 for (name, value) in &self.headers {
138 builder = builder.header(name.as_str(), value.as_str());
139 }
140 if let Some(timeout) = self.timeout {
141 builder = builder.timeout(timeout);
142 }
143 let transport = builder.build()?;
144
145 let initial_token = match self.initial_token {
146 Some(token) => Some(token),
147 None => match &self.provider {
148 Some(provider) => provider
149 .access_token()
150 .await
151 .map_err(wecomx_transport::Error::from)?
152 .map(|t| t.into_inner()),
153 None => None,
154 },
155 };
156
157 Ok(transport.wrap_backend(move |backend| {
158 Arc::new(
159 WecomBackend::new(backend, self.provider, initial_token)
160 .with_bin_name(self.bin_name),
161 )
162 }))
163 }
164}
165
166#[cfg(test)]
167pub(crate) mod tests {
168 use std::sync::Arc;
176
177 use serde_json::json;
178 use wecomx_auth::{BotCredential, BotGatewayTokenProvider, Credentials, MemoryCredentialStore};
179 use wiremock::matchers::{method, path};
180 use wiremock::{Mock, MockServer, ResponseTemplate};
181
182 use super::*;
183
184 pub(crate) fn auth_ep(url: &str) -> wecomx_transport::Endpoint {
186 wecomx_auth::auth_endpoint(url)
187 }
188
189 #[tokio::test]
193 async fn build_reads_initial_token_from_provider() {
194 let server = MockServer::start().await;
195 Mock::given(method("POST"))
196 .and(path("/api"))
197 .and(wiremock::matchers::header(
198 "authorization",
199 "Bearer tok-stored",
200 ))
201 .respond_with(
202 ResponseTemplate::new(200).set_body_json(json!({"result": "{\"ok\":true}"})),
203 )
204 .expect(1)
205 .mount(&server)
206 .await;
207
208 let store = MemoryCredentialStore::new(Credentials {
209 bot: Some(BotCredential::new("bot1".into(), "secret1".into())),
210 token: Some("tok-stored".into()),
211 })
212 .shared();
213 let provider = BotGatewayTokenProvider::from_store(store)
214 .with_auth_endpoint(auth_ep(&format!("{}/bootstrap", server.uri())));
215
216 let client = WecomClientBuilder::new()
217 .base_url(server.uri())
218 .token_provider(Arc::new(provider))
219 .timeout(std::time::Duration::from_secs(5))
220 .build()
221 .await
222 .unwrap();
223
224 let endpoint = wecomx_transport::Endpoint::new()
225 .with(wecomx_transport::HttpEndpoint::new("/api").with_service(server.uri()));
226 let v = client
227 .transport()
228 .invoke(&endpoint, json!({}))
229 .await
230 .unwrap()
231 .into_result()
232 .unwrap();
233 assert_eq!(v, json!({"ok": true}));
234 server.verify().await;
235 }
236
237 #[tokio::test]
241 async fn initial_token_overrides_provider() {
242 let server = MockServer::start().await;
243 Mock::given(method("POST"))
244 .and(path("/api"))
245 .and(wiremock::matchers::header(
246 "authorization",
247 "Bearer tok-override",
248 ))
249 .respond_with(
250 ResponseTemplate::new(200).set_body_json(json!({"result": "{\"ok\":true}"})),
251 )
252 .expect(1)
253 .mount(&server)
254 .await;
255
256 let store = MemoryCredentialStore::new(Credentials {
257 bot: Some(BotCredential::new("bot1".into(), "secret1".into())),
258 token: Some("tok-stored".into()),
259 })
260 .shared();
261 let provider = BotGatewayTokenProvider::from_store(store);
262
263 let transport = WecomClientBuilder::new()
264 .base_url(server.uri())
265 .token_provider(Arc::new(provider))
266 .initial_token("tok-override")
267 .build_transport()
268 .await
269 .unwrap();
270
271 let endpoint = wecomx_transport::Endpoint::new()
272 .with(wecomx_transport::HttpEndpoint::new("/api").with_service(server.uri()));
273 let v = transport
274 .invoke(&endpoint, json!({}))
275 .await
276 .unwrap()
277 .into_result()
278 .unwrap();
279 assert_eq!(v, json!({"ok": true}));
280 server.verify().await;
281 }
282
283 #[tokio::test]
285 async fn build_without_provider() {
286 let client = WecomClientBuilder::new()
287 .base_url("http://localhost")
288 .build()
289 .await
290 .unwrap();
291 assert_eq!(client.transport().name(), "http");
292 }
293
294 #[tokio::test]
298 async fn build_attaches_gateway_catalog() {
299 let client = WecomClientBuilder::new()
300 .base_url("http://localhost")
301 .build()
302 .await
303 .unwrap();
304 let _endpoint = client.inner().endpoint("media/upload");
306 }
307}