open_lark/core/request_builder/
mod.rs1mod auth_handler;
2mod header_builder;
3mod multipart_builder;
4
5pub use auth_handler::AuthHandler;
6pub use header_builder::HeaderBuilder;
7pub use multipart_builder::MultipartBuilder;
8
9use crate::core::{
10 api_req::ApiRequest, config::Config, constants::AccessTokenType, error::LarkAPIError,
11 req_option::RequestOption,
12};
13use reqwest::RequestBuilder;
14use std::{future::Future, pin::Pin};
15
16pub struct UnifiedRequestBuilder;
18
19impl UnifiedRequestBuilder {
20 pub fn build<'a>(
21 req: &'a mut ApiRequest,
22 access_token_type: AccessTokenType,
23 config: &'a Config,
24 option: &'a RequestOption,
25 ) -> Pin<Box<dyn Future<Output = Result<RequestBuilder, LarkAPIError>> + Send + 'a>> {
26 Box::pin(async move {
27 let url = Self::build_url(config, req)?;
29 let mut req_builder = config
30 .http_client
31 .request(req.http_method.clone(), url.as_ref());
32
33 req_builder = HeaderBuilder::build_headers(req_builder, config, option);
35
36 req_builder =
38 AuthHandler::apply_auth(req_builder, access_token_type, config, option).await?;
39
40 if !req.file.is_empty() {
42 req_builder = MultipartBuilder::build_multipart(req_builder, &req.body, &req.file)?;
43 } else if !req.body.is_empty() {
44 req_builder = req_builder.body(req.body.clone());
45 req_builder = req_builder.header(
46 crate::core::constants::CONTENT_TYPE_HEADER,
47 crate::core::constants::DEFAULT_CONTENT_TYPE,
48 );
49 }
50
51 Ok(req_builder)
52 })
53 }
54
55 fn build_url(config: &Config, req: &ApiRequest) -> Result<url::Url, LarkAPIError> {
56 let path = format!("{}{}", config.base_url, req.api_path);
57 let query_params = req
58 .query_params
59 .iter()
60 .map(|(k, v)| (*k, v.as_str()))
61 .collect::<Vec<_>>();
62 Ok(url::Url::parse_with_params(&path, query_params)?)
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use crate::core::{api_req::ApiRequest, constants::AppType};
70 use reqwest::Method;
71 use std::collections::HashMap;
72
73 fn create_test_config() -> Config {
74 Config::builder()
75 .app_id("test_app_id")
76 .app_secret("test_app_secret")
77 .app_type(AppType::SelfBuild)
78 .base_url("https://open.feishu.cn")
79 .build()
80 }
81
82 fn create_test_api_request() -> ApiRequest {
83 ApiRequest {
84 http_method: Method::GET,
85 api_path: "/open-apis/test".to_string(),
86 body: vec![],
87 file: vec![],
88 query_params: HashMap::new(),
89 ..Default::default()
90 }
91 }
92
93 #[test]
94 fn test_unified_request_builder_struct_creation() {
95 let _builder = UnifiedRequestBuilder;
96 }
97
98 #[tokio::test]
99 async fn test_build_basic_request() {
100 let mut api_req = create_test_api_request();
101 let config = create_test_config();
102 let option = RequestOption::default();
103
104 let result =
105 UnifiedRequestBuilder::build(&mut api_req, AccessTokenType::None, &config, &option)
106 .await;
107
108 assert!(result.is_ok());
110 }
111
112 #[tokio::test]
113 async fn test_build_request_with_body() {
114 let mut api_req = create_test_api_request();
115 api_req.http_method = Method::POST;
116 api_req.body = b"{\"test\": \"data\"}".to_vec();
117
118 let config = create_test_config();
119 let option = RequestOption::default();
120
121 let result =
122 UnifiedRequestBuilder::build(&mut api_req, AccessTokenType::None, &config, &option)
123 .await;
124
125 assert!(result.is_ok());
126 }
127
128 #[tokio::test]
129 async fn test_build_request_with_files() {
130 let mut api_req = create_test_api_request();
131 api_req.http_method = Method::POST;
132
133 api_req.file = b"file content".to_vec();
135
136 let config = create_test_config();
137 let option = RequestOption::default();
138
139 let result =
140 UnifiedRequestBuilder::build(&mut api_req, AccessTokenType::None, &config, &option)
141 .await;
142
143 assert!(result.is_ok() || result.is_err());
145 }
146
147 #[tokio::test]
148 async fn test_build_request_with_query_params() {
149 let mut api_req = create_test_api_request();
150 api_req.query_params.insert("page", "1".to_string());
151 api_req.query_params.insert("limit", "10".to_string());
152
153 let config = create_test_config();
154 let option = RequestOption::default();
155
156 let result =
157 UnifiedRequestBuilder::build(&mut api_req, AccessTokenType::None, &config, &option)
158 .await;
159
160 assert!(result.is_ok());
161 }
162
163 #[tokio::test]
164 async fn test_build_request_with_app_token() {
165 let mut api_req = create_test_api_request();
166 let config = create_test_config();
167 let option = RequestOption {
168 app_access_token: "app_token_123".to_string(),
169 ..Default::default()
170 };
171
172 let result =
173 UnifiedRequestBuilder::build(&mut api_req, AccessTokenType::App, &config, &option)
174 .await;
175
176 assert!(result.is_ok());
177 }
178
179 #[tokio::test]
180 async fn test_build_request_with_tenant_token() {
181 let mut api_req = create_test_api_request();
182 let config = create_test_config();
183 let option = RequestOption {
184 tenant_access_token: "tenant_token_123".to_string(),
185 ..Default::default()
186 };
187
188 let result =
189 UnifiedRequestBuilder::build(&mut api_req, AccessTokenType::Tenant, &config, &option)
190 .await;
191
192 assert!(result.is_ok());
193 }
194
195 #[tokio::test]
196 async fn test_build_request_with_user_token() {
197 let mut api_req = create_test_api_request();
198 let config = create_test_config();
199 let option = RequestOption {
200 user_access_token: "user_token_123".to_string(),
201 ..Default::default()
202 };
203
204 let result =
205 UnifiedRequestBuilder::build(&mut api_req, AccessTokenType::User, &config, &option)
206 .await;
207
208 assert!(result.is_ok());
209 }
210
211 #[tokio::test]
212 async fn test_build_request_different_methods() {
213 let config = create_test_config();
214 let option = RequestOption::default();
215
216 let methods = [
217 Method::GET,
218 Method::POST,
219 Method::PUT,
220 Method::DELETE,
221 Method::PATCH,
222 ];
223
224 for method in methods.iter() {
225 let mut api_req = create_test_api_request();
226 api_req.http_method = method.clone();
227
228 let result =
229 UnifiedRequestBuilder::build(&mut api_req, AccessTokenType::None, &config, &option)
230 .await;
231
232 assert!(result.is_ok(), "Failed for method: {:?}", method);
233 }
234 }
235
236 #[test]
237 fn test_build_url_basic() {
238 let config = create_test_config();
239 let api_req = create_test_api_request();
240
241 let result = UnifiedRequestBuilder::build_url(&config, &api_req);
242
243 assert!(result.is_ok());
244 let url = result.unwrap();
245 assert!(url
247 .as_str()
248 .starts_with("https://open.feishu.cn/open-apis/test"));
249 }
250
251 #[test]
252 fn test_build_url_with_query_params() {
253 let config = create_test_config();
254 let mut api_req = create_test_api_request();
255 api_req.query_params.insert("page", "1".to_string());
256 api_req.query_params.insert("size", "20".to_string());
257
258 let result = UnifiedRequestBuilder::build_url(&config, &api_req);
259
260 assert!(result.is_ok());
261 let url = result.unwrap();
262 let url_str = url.as_str();
263 assert!(url_str.starts_with("https://open.feishu.cn/open-apis/test"));
264 assert!(url_str.contains("page=1"));
265 assert!(url_str.contains("size=20"));
266 }
267
268 #[test]
269 fn test_build_url_with_special_characters() {
270 let config = create_test_config();
271 let mut api_req = create_test_api_request();
272 api_req
273 .query_params
274 .insert("query", "test with spaces".to_string());
275 api_req
276 .query_params
277 .insert("filter", "key=value&other=data".to_string());
278
279 let result = UnifiedRequestBuilder::build_url(&config, &api_req);
280
281 assert!(result.is_ok());
282 let url = result.unwrap();
283 assert!(url.as_str().contains("query="));
285 assert!(url.as_str().contains("filter="));
286 }
287
288 #[test]
289 fn test_build_url_with_empty_query_params() {
290 let config = create_test_config();
291 let mut api_req = create_test_api_request();
292 api_req.query_params.insert("empty", "".to_string());
293
294 let result = UnifiedRequestBuilder::build_url(&config, &api_req);
295
296 assert!(result.is_ok());
297 let url = result.unwrap();
298 assert!(url.as_str().contains("empty="));
299 }
300
301 #[test]
302 fn test_build_url_invalid_base_url() {
303 let config = Config::builder()
304 .app_id("test_app_id")
305 .app_secret("test_app_secret")
306 .app_type(AppType::SelfBuild)
307 .base_url("invalid-url")
308 .build();
309 let api_req = create_test_api_request();
310
311 let result = UnifiedRequestBuilder::build_url(&config, &api_req);
312
313 assert!(result.is_err());
315 }
316
317 #[tokio::test]
318 async fn test_build_request_with_custom_headers() {
319 let mut api_req = create_test_api_request();
320 let config = create_test_config();
321 let mut option = RequestOption {
322 request_id: "custom-request-123".to_string(),
323 ..Default::default()
324 };
325 option
326 .header
327 .insert("X-Custom-Header".to_string(), "custom-value".to_string());
328
329 let result =
330 UnifiedRequestBuilder::build(&mut api_req, AccessTokenType::None, &config, &option)
331 .await;
332
333 assert!(result.is_ok());
334 }
335
336 #[tokio::test]
337 async fn test_build_request_complex_scenario() {
338 let mut api_req = ApiRequest {
339 http_method: Method::POST,
340 api_path: "/open-apis/complex/test".to_string(),
341 body: b"{\"complex\": \"data\", \"nested\": {\"value\": 123}}".to_vec(),
342 file: vec![],
343 query_params: HashMap::new(),
344 ..Default::default()
345 };
346 api_req.query_params.insert("version", "v1".to_string());
347 api_req.query_params.insert("format", "json".to_string());
348
349 let config = create_test_config();
350 let option = RequestOption {
351 request_id: "complex-request-456".to_string(),
352 app_access_token: "app_token_456".to_string(),
353 ..Default::default()
354 };
355
356 let result =
357 UnifiedRequestBuilder::build(&mut api_req, AccessTokenType::App, &config, &option)
358 .await;
359
360 assert!(result.is_ok());
361 }
362
363 #[test]
364 fn test_unified_request_builder_is_send_sync() {
365 fn assert_send<T: Send>() {}
367 fn assert_sync<T: Sync>() {}
368
369 assert_send::<UnifiedRequestBuilder>();
370 assert_sync::<UnifiedRequestBuilder>();
371 }
372
373 #[tokio::test]
374 async fn test_build_request_with_body_and_files_edge_case() {
375 let mut api_req = create_test_api_request();
376 api_req.http_method = Method::POST;
377 api_req.body = b"regular body".to_vec();
378
379 api_req.file = b"file content combined".to_vec();
381
382 let config = create_test_config();
383 let option = RequestOption::default();
384
385 let result =
386 UnifiedRequestBuilder::build(&mut api_req, AccessTokenType::None, &config, &option)
387 .await;
388
389 assert!(result.is_ok() || result.is_err());
391 }
392
393 #[test]
394 fn test_build_url_with_path_segments() {
395 let config = create_test_config();
396 let mut api_req = create_test_api_request();
397 api_req.api_path = "/open-apis/v1/users/123/messages".to_string();
398
399 let result = UnifiedRequestBuilder::build_url(&config, &api_req);
400
401 assert!(result.is_ok());
402 let url = result.unwrap();
403 assert!(url
405 .as_str()
406 .starts_with("https://open.feishu.cn/open-apis/v1/users/123/messages"));
407 }
408}