open_lark/core/request_builder/
mod.rs

1mod 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
16/// 统一的请求构建器,负责协调各个子构建器
17pub 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            // 1. 构建基础请求
28            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            // 2. 构建请求头
34            req_builder = HeaderBuilder::build_headers(req_builder, config, option);
35
36            // 3. 处理认证
37            req_builder =
38                AuthHandler::apply_auth(req_builder, access_token_type, config, option).await?;
39
40            // 4. 处理请求体
41            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        // Should build request successfully
109        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        // Add a file to the request
134        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        // This might fail due to multipart builder dependencies, but should not panic
144        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        // May have trailing ? due to parse_with_params implementation
246        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        // URL encoding should be handled properly
284        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        // Should return error for invalid URL
314        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        // Test that UnifiedRequestBuilder implements required traits
366        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        // Add files - this should take precedence over body
380        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        // Should handle files taking precedence over body
390        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        // May have trailing ? due to parse_with_params implementation
404        assert!(url
405            .as_str()
406            .starts_with("https://open.feishu.cn/open-apis/v1/users/123/messages"));
407    }
408}