open_lark/core/
api_req.rs

1use std::collections::HashMap;
2
3use reqwest::Method;
4
5use crate::core::constants::AccessTokenType;
6
7/// API请求的核心数据结构 - 命令模式的体现
8///
9/// `ApiRequest` 是整个SDK架构的核心,采用命令模式(Command Pattern)设计。
10/// 它封装了发起一次飞书API调用所需的所有信息,充当服务层(Service)与传输层(Transport)之间的桥梁。
11///
12/// # 设计理念
13///
14/// - **解耦性**:服务层只负责构建请求,不关心HTTP细节
15/// - **统一性**:所有API调用都通过这个统一的结构体表示
16/// - **灵活性**:支持普通请求和multipart/form-data请求
17///
18/// # 使用流程
19///
20/// 1. 服务层方法创建并配置 `ApiRequest` 实例
21/// 2. 设置HTTP方法、路径、认证需求等基本信息
22/// 3. 根据请求类型填充 `body` 和/或 `file` 字段
23/// 4. 将配置好的请求传递给 `Transport::request` 进行处理
24///
25/// # 示例
26///
27/// ```rust,ignore
28/// // 普通JSON请求
29/// let mut api_req = ApiRequest {
30///     http_method: Method::POST,
31///     api_path: "/open-apis/drive/v1/files".to_string(),
32///     body: serde_json::to_vec(&request_data).unwrap(),
33///     ..Default::default()
34/// };
35///
36/// // 文件上传请求(multipart)
37/// let mut api_req = ApiRequest {
38///     http_method: Method::POST,
39///     api_path: "/open-apis/drive/v1/files/upload".to_string(),
40///     body: serde_json::to_vec(&metadata).unwrap(),  // JSON元数据
41///     file: file_bytes,  // 文件内容
42///     ..Default::default()
43/// };
44/// ```
45#[derive(Debug, Clone, Default)]
46pub struct ApiRequest {
47    /// HTTP请求方法(GET、POST、PUT、DELETE等)
48    ///
49    /// 由服务层根据具体API的要求设置。
50    /// 使用 `pub(crate)` 限制只能在crate内部修改,保证一致性。
51    pub(crate) http_method: Method,
52
53    /// API的相对路径
54    ///
55    /// 例如:`/open-apis/drive/v1/files/{file_id}`
56    ///
57    /// 路径中的动态参数(如 `{file_id}`)通常通过 `format!` 宏直接嵌入,
58    /// 而不是使用 `path_params` 字段。
59    pub api_path: String,
60
61    /// 请求体数据(序列化后的字节数组)
62    ///
63    /// # 在不同请求类型中的用途:
64    ///
65    /// - **普通请求**:包含完整的请求体,通常是JSON序列化后的数据
66    /// - **文件上传(multipart)**:仅包含JSON元数据部分,文件内容存储在 `file` 字段
67    /// - **无请求体的请求**:保持为空 `Vec`
68    ///
69    /// # 注意事项
70    ///
71    /// 服务层通常使用 `serde_json::to_vec()` 将请求结构体序列化到这个字段。
72    pub body: Vec<u8>,
73
74    /// URL查询参数
75    ///
76    /// 存储将被附加到URL末尾的查询参数。
77    /// 例如:`?page_size=10&page_token=xxx`
78    ///
79    /// # 性能优化
80    ///
81    /// 键使用 `&'static str` 避免堆分配,配合 `QueryParams` 常量使用:
82    ///
83    /// ```rust,ignore
84    /// api_req.query_params.insert(QueryParams::PAGE_SIZE, "10".to_string());
85    /// api_req.query_params.insert(QueryParams::PAGE_TOKEN, token);
86    /// ```
87    ///
88    /// 这种设计减少了每次API调用约8-16字节的内存分配。
89    pub query_params: HashMap<&'static str, String>,
90
91    /// URL路径参数(保留字段)
92    ///
93    /// 该字段为未来的路径模板功能保留。目前在现有架构中:
94    ///
95    /// - **当前做法**: 路径参数通过 `format!` 宏直接嵌入 `api_path`
96    /// - **替代方案**: 可使用 `RequestExecutor::execute_with_path_params()` 进行路径参数替换
97    /// - **设计考虑**: 保留该字段可为未来的模板系统升级提供支持
98    ///
99    /// 关于路径参数处理,参考 `crate::service::endpoints::EndpointHelper::replace_path_params`
100    /// 和 `crate::core::request_executor::RequestExecutor::execute_with_path_params`。
101    pub path_params: HashMap<String, Vec<String>>,
102
103    /// 支持的访问令牌类型
104    ///
105    /// 指定此API端点接受哪些类型的访问令牌:
106    /// - `User`:用户访问令牌
107    /// - `Tenant`:租户访问令牌  
108    /// - `App`:应用访问令牌
109    ///
110    /// Transport层会根据这个列表和当前配置选择合适的令牌类型。
111    /// 使用 `pub(crate)` 确保只能由服务层设置。
112    pub(crate) supported_access_token_types: Vec<AccessTokenType>,
113
114    /// 文件内容(用于multipart/form-data请求)
115    ///
116    /// # 在不同请求类型中的用途:
117    ///
118    /// - **普通请求**:保持为空 `Vec`
119    /// - **文件上传(multipart)**:包含要上传的文件的二进制内容
120    ///
121    /// # 工作原理
122    ///
123    /// 当 `file` 字段非空时,Transport层会自动识别这是一个multipart请求:
124    /// 1. `body` 字段的内容作为multipart的JSON元数据部分
125    /// 2. `file` 字段的内容作为文件部分
126    /// 3. Content-Type自动设置为 `multipart/form-data`
127    ///
128    /// # 示例
129    ///
130    /// ```rust,ignore
131    /// // 文件上传请求
132    /// api_req.body = serde_json::to_vec(&FileMetadata {
133    ///     name: "document.pdf",
134    ///     parent_id: "folder123",
135    /// }).unwrap();
136    /// api_req.file = std::fs::read("path/to/document.pdf").unwrap();
137    /// ```
138    pub file: Vec<u8>,
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::core::constants::AccessTokenType;
145    use reqwest::Method;
146
147    #[test]
148    fn test_api_request_creation() {
149        let api_req = ApiRequest {
150            http_method: Method::POST,
151            api_path: "/open-apis/test/v1/endpoint".to_string(),
152            body: b"test body".to_vec(),
153            query_params: HashMap::new(),
154            path_params: HashMap::new(),
155            supported_access_token_types: vec![AccessTokenType::Tenant],
156            file: Vec::new(),
157        };
158
159        assert_eq!(api_req.http_method, Method::POST);
160        assert_eq!(api_req.api_path, "/open-apis/test/v1/endpoint");
161        assert_eq!(api_req.body, b"test body".to_vec());
162        assert!(api_req.query_params.is_empty());
163        assert!(api_req.path_params.is_empty());
164        assert_eq!(
165            api_req.supported_access_token_types,
166            vec![AccessTokenType::Tenant]
167        );
168        assert!(api_req.file.is_empty());
169    }
170
171    #[test]
172    fn test_api_request_default() {
173        let api_req = ApiRequest::default();
174
175        assert_eq!(api_req.http_method, Method::GET);
176        assert!(api_req.api_path.is_empty());
177        assert!(api_req.body.is_empty());
178        assert!(api_req.query_params.is_empty());
179        assert!(api_req.path_params.is_empty());
180        assert!(api_req.supported_access_token_types.is_empty());
181        assert!(api_req.file.is_empty());
182    }
183
184    #[test]
185    fn test_api_request_clone() {
186        let original = ApiRequest {
187            http_method: Method::PUT,
188            api_path: "/open-apis/clone/test".to_string(),
189            body: b"original body".to_vec(),
190            query_params: {
191                let mut params = HashMap::new();
192                params.insert("page_size", "10".to_string());
193                params
194            },
195            path_params: {
196                let mut params = HashMap::new();
197                params.insert("file_id".to_string(), vec!["123".to_string()]);
198                params
199            },
200            supported_access_token_types: vec![AccessTokenType::User, AccessTokenType::Tenant],
201            file: b"file content".to_vec(),
202        };
203
204        let cloned = original.clone();
205
206        assert_eq!(original.http_method, cloned.http_method);
207        assert_eq!(original.api_path, cloned.api_path);
208        assert_eq!(original.body, cloned.body);
209        assert_eq!(original.query_params, cloned.query_params);
210        assert_eq!(original.path_params, cloned.path_params);
211        assert_eq!(
212            original.supported_access_token_types,
213            cloned.supported_access_token_types
214        );
215        assert_eq!(original.file, cloned.file);
216    }
217
218    #[test]
219    fn test_api_request_debug() {
220        let api_req = ApiRequest {
221            http_method: Method::DELETE,
222            api_path: "/debug/test".to_string(),
223            body: b"debug body".to_vec(),
224            ..Default::default()
225        };
226
227        let debug_str = format!("{:?}", api_req);
228
229        assert!(debug_str.contains("ApiRequest"));
230        assert!(debug_str.contains("DELETE"));
231        assert!(debug_str.contains("/debug/test"));
232    }
233
234    #[test]
235    fn test_api_request_with_different_http_methods() {
236        let methods = vec![
237            Method::GET,
238            Method::POST,
239            Method::PUT,
240            Method::DELETE,
241            Method::PATCH,
242            Method::HEAD,
243            Method::OPTIONS,
244        ];
245
246        for method in methods {
247            let api_req = ApiRequest {
248                http_method: method.clone(),
249                ..Default::default()
250            };
251            assert_eq!(api_req.http_method, method);
252        }
253    }
254
255    #[test]
256    fn test_api_request_with_query_params() {
257        let mut api_req = ApiRequest::default();
258
259        // Add query parameters
260        api_req.query_params.insert("page_size", "20".to_string());
261        api_req
262            .query_params
263            .insert("page_token", "token123".to_string());
264        api_req
265            .query_params
266            .insert("filter", "status=active".to_string());
267
268        assert_eq!(api_req.query_params.len(), 3);
269        assert_eq!(
270            api_req.query_params.get("page_size"),
271            Some(&"20".to_string())
272        );
273        assert_eq!(
274            api_req.query_params.get("page_token"),
275            Some(&"token123".to_string())
276        );
277        assert_eq!(
278            api_req.query_params.get("filter"),
279            Some(&"status=active".to_string())
280        );
281    }
282
283    #[test]
284    fn test_api_request_with_path_params() {
285        let mut api_req = ApiRequest::default();
286
287        // Add path parameters
288        api_req
289            .path_params
290            .insert("user_id".to_string(), vec!["user123".to_string()]);
291        api_req
292            .path_params
293            .insert("file_id".to_string(), vec!["file456".to_string()]);
294        api_req.path_params.insert(
295            "multiple".to_string(),
296            vec!["val1".to_string(), "val2".to_string()],
297        );
298
299        assert_eq!(api_req.path_params.len(), 3);
300        assert_eq!(
301            api_req.path_params.get("user_id"),
302            Some(&vec!["user123".to_string()])
303        );
304        assert_eq!(
305            api_req.path_params.get("file_id"),
306            Some(&vec!["file456".to_string()])
307        );
308        assert_eq!(
309            api_req.path_params.get("multiple"),
310            Some(&vec!["val1".to_string(), "val2".to_string()])
311        );
312    }
313
314    #[test]
315    fn test_api_request_with_different_access_token_types() {
316        let token_types = vec![
317            vec![AccessTokenType::User],
318            vec![AccessTokenType::Tenant],
319            vec![AccessTokenType::App],
320            vec![AccessTokenType::User, AccessTokenType::Tenant],
321            vec![
322                AccessTokenType::User,
323                AccessTokenType::Tenant,
324                AccessTokenType::App,
325            ],
326        ];
327
328        for token_type_vec in token_types {
329            let api_req = ApiRequest {
330                supported_access_token_types: token_type_vec.clone(),
331                ..Default::default()
332            };
333            assert_eq!(api_req.supported_access_token_types, token_type_vec);
334        }
335    }
336
337    #[test]
338    fn test_api_request_with_body_serialization() {
339        // Test with JSON serialization
340        let json_data = serde_json::json!({
341            "name": "test file",
342            "parent_id": "folder123"
343        });
344        let json_bytes = serde_json::to_vec(&json_data).unwrap();
345
346        let api_req = ApiRequest {
347            body: json_bytes.clone(),
348            ..Default::default()
349        };
350
351        assert_eq!(api_req.body, json_bytes);
352
353        // Verify it can be deserialized back
354        let deserialized: serde_json::Value = serde_json::from_slice(&api_req.body).unwrap();
355        assert_eq!(deserialized, json_data);
356    }
357
358    #[test]
359    fn test_api_request_with_empty_body() {
360        let api_req = ApiRequest {
361            body: Vec::new(),
362            ..Default::default()
363        };
364
365        assert!(api_req.body.is_empty());
366    }
367
368    #[test]
369    fn test_api_request_with_large_body() {
370        let large_body = vec![0u8; 1024 * 1024]; // 1MB
371        let api_req = ApiRequest {
372            body: large_body.clone(),
373            ..Default::default()
374        };
375
376        assert_eq!(api_req.body.len(), 1024 * 1024);
377        assert_eq!(api_req.body, large_body);
378    }
379
380    #[test]
381    fn test_api_request_with_file_upload() {
382        let file_content = b"binary file content";
383        let metadata = serde_json::json!({
384            "filename": "test.txt",
385            "size": file_content.len()
386        });
387
388        let api_req = ApiRequest {
389            http_method: Method::POST,
390            api_path: "/upload".to_string(),
391            body: serde_json::to_vec(&metadata).unwrap(),
392            file: file_content.to_vec(),
393            ..Default::default()
394        };
395
396        assert_eq!(api_req.http_method, Method::POST);
397        assert_eq!(api_req.api_path, "/upload");
398        assert!(!api_req.body.is_empty());
399        assert_eq!(api_req.file, file_content.to_vec());
400    }
401
402    #[test]
403    fn test_api_request_with_empty_file() {
404        let api_req = ApiRequest {
405            file: Vec::new(),
406            ..Default::default()
407        };
408
409        assert!(api_req.file.is_empty());
410    }
411
412    #[test]
413    fn test_api_request_with_large_file() {
414        let large_file = vec![1u8; 10 * 1024 * 1024]; // 10MB
415        let api_req = ApiRequest {
416            file: large_file.clone(),
417            ..Default::default()
418        };
419
420        assert_eq!(api_req.file.len(), 10 * 1024 * 1024);
421        assert_eq!(api_req.file, large_file);
422    }
423
424    #[test]
425    fn test_api_request_multipart_structure() {
426        let metadata = serde_json::json!({
427            "name": "document.pdf",
428            "parent_id": "folder123"
429        });
430        let file_content = b"PDF file binary content";
431
432        let api_req = ApiRequest {
433            http_method: Method::POST,
434            api_path: "/upload/multipart".to_string(),
435            body: serde_json::to_vec(&metadata).unwrap(),
436            file: file_content.to_vec(),
437            supported_access_token_types: vec![AccessTokenType::Tenant],
438            ..Default::default()
439        };
440
441        // Verify multipart request structure
442        assert_eq!(api_req.http_method, Method::POST);
443        assert!(!api_req.body.is_empty()); // Has metadata
444        assert!(!api_req.file.is_empty()); // Has file content
445        assert_eq!(
446            api_req.supported_access_token_types,
447            vec![AccessTokenType::Tenant]
448        );
449    }
450
451    #[test]
452    fn test_api_request_path_variations() {
453        let paths = vec![
454            "/open-apis/drive/v1/files",
455            "/open-apis/drive/v1/files/{file_id}",
456            "/open-apis/contact/v3/users/{user_id}/update",
457            "",
458            "/",
459            "/simple",
460            "/very/deep/nested/path/structure/endpoint",
461        ];
462
463        for path in paths {
464            let api_req = ApiRequest {
465                api_path: path.to_string(),
466                ..Default::default()
467            };
468            assert_eq!(api_req.api_path, path);
469        }
470    }
471
472    #[test]
473    fn test_api_request_special_characters_in_paths() {
474        let special_paths = vec![
475            "/path/with spaces",
476            "/path/with-dashes",
477            "/path/with_underscores",
478            "/path/with.dots",
479            "/path/with@symbols",
480            "/path/with中文字符",
481            "/path/with🚀emoji",
482        ];
483
484        for path in special_paths {
485            let api_req = ApiRequest {
486                api_path: path.to_string(),
487                ..Default::default()
488            };
489            assert_eq!(api_req.api_path, path);
490        }
491    }
492
493    #[test]
494    fn test_api_request_query_params_special_values() {
495        let mut api_req = ApiRequest::default();
496
497        // Test with special characters and edge cases
498        api_req.query_params.insert("empty", "".to_string());
499        api_req
500            .query_params
501            .insert("space", "value with space".to_string());
502        api_req
503            .query_params
504            .insert("special", "value@#$%^&*()".to_string());
505        api_req
506            .query_params
507            .insert("unicode", "中文值🚀".to_string());
508        api_req
509            .query_params
510            .insert("url_encoded", "value%20with%20encoding".to_string());
511
512        assert_eq!(api_req.query_params.len(), 5);
513        assert_eq!(api_req.query_params.get("empty"), Some(&"".to_string()));
514        assert_eq!(
515            api_req.query_params.get("space"),
516            Some(&"value with space".to_string())
517        );
518        assert_eq!(
519            api_req.query_params.get("special"),
520            Some(&"value@#$%^&*()".to_string())
521        );
522        assert_eq!(
523            api_req.query_params.get("unicode"),
524            Some(&"中文值🚀".to_string())
525        );
526        assert_eq!(
527            api_req.query_params.get("url_encoded"),
528            Some(&"value%20with%20encoding".to_string())
529        );
530    }
531
532    #[test]
533    fn test_api_request_path_params_complex() {
534        let mut api_req = ApiRequest::default();
535
536        // Test with complex path parameter structures
537        api_req
538            .path_params
539            .insert("single".to_string(), vec!["one".to_string()]);
540        api_req.path_params.insert(
541            "multiple".to_string(),
542            vec![
543                "first".to_string(),
544                "second".to_string(),
545                "third".to_string(),
546            ],
547        );
548        api_req.path_params.insert("empty".to_string(), vec![]);
549        api_req.path_params.insert(
550            "special".to_string(),
551            vec![
552                "value@#$".to_string(),
553                "中文".to_string(),
554                "🚀emoji".to_string(),
555            ],
556        );
557
558        assert_eq!(api_req.path_params.len(), 4);
559        assert_eq!(
560            api_req.path_params.get("single"),
561            Some(&vec!["one".to_string()])
562        );
563        assert_eq!(
564            api_req.path_params.get("multiple"),
565            Some(&vec![
566                "first".to_string(),
567                "second".to_string(),
568                "third".to_string()
569            ])
570        );
571        assert_eq!(api_req.path_params.get("empty"), Some(&vec![]));
572        assert_eq!(
573            api_req.path_params.get("special"),
574            Some(&vec![
575                "value@#$".to_string(),
576                "中文".to_string(),
577                "🚀emoji".to_string()
578            ])
579        );
580    }
581
582    #[test]
583    fn test_api_request_binary_data_handling() {
584        let binary_data = vec![0, 1, 2, 3, 4, 255, 254, 253];
585        let api_req = ApiRequest {
586            body: binary_data.clone(),
587            file: binary_data.clone(),
588            ..Default::default()
589        };
590
591        assert_eq!(api_req.body, binary_data);
592        assert_eq!(api_req.file, binary_data);
593    }
594
595    #[test]
596    fn test_api_request_memory_efficiency() {
597        // Test creating many ApiRequest instances
598        let requests: Vec<ApiRequest> = (0..100)
599            .map(|i| ApiRequest {
600                api_path: format!("/api/path/{}", i),
601                body: format!("body_{}", i).into_bytes(),
602                ..Default::default()
603            })
604            .collect();
605
606        assert_eq!(requests.len(), 100);
607
608        for (i, req) in requests.iter().enumerate() {
609            assert_eq!(req.api_path, format!("/api/path/{}", i));
610            assert_eq!(req.body, format!("body_{}", i).into_bytes());
611        }
612    }
613
614    #[test]
615    fn test_api_request_field_independence() {
616        let mut api_req = ApiRequest {
617            http_method: Method::POST,
618            api_path: "/test".to_string(),
619            body: b"test body".to_vec(),
620            ..Default::default()
621        };
622        api_req.query_params.insert("test", "value".to_string());
623        api_req
624            .path_params
625            .insert("id".to_string(), vec!["123".to_string()]);
626        api_req
627            .supported_access_token_types
628            .push(AccessTokenType::User);
629        api_req.file = b"file content".to_vec();
630
631        // Verify all fields are set correctly
632        assert_eq!(api_req.http_method, Method::POST);
633        assert_eq!(api_req.api_path, "/test");
634        assert_eq!(api_req.body, b"test body");
635        assert_eq!(api_req.query_params.len(), 1);
636        assert_eq!(api_req.path_params.len(), 1);
637        assert_eq!(api_req.supported_access_token_types.len(), 1);
638        assert_eq!(api_req.file, b"file content");
639    }
640}