open_lark/service/cloud_docs/drive/v1/
folder.rs

1use reqwest::Method;
2use serde::{Deserialize, Serialize};
3
4use crate::{
5    core::{
6        api_req::ApiRequest,
7        api_resp::{ApiResponseTrait, BaseResponse, ResponseFormat},
8        config::Config,
9        constants::AccessTokenType,
10        endpoints::cloud_docs::*,
11        http::Transport,
12        req_option::RequestOption,
13        SDKResult,
14    },
15    impl_executable_builder_owned,
16};
17
18/// 文件夹服务
19pub struct FolderService {
20    config: Config,
21}
22
23impl FolderService {
24    pub fn new(config: Config) -> Self {
25        Self { config }
26    }
27
28    /// 获取我的空间(root folder)元数据
29    ///
30    /// 该接口用于根据用户的访问凭证获取用户的根目录信息,包括根目录的token等。
31    ///
32    /// <https://open.feishu.cn/document/server-docs/docs/drive-v1/folder/get-root-folder-meta>
33    pub async fn get_root_folder_meta(
34        &self,
35        option: Option<RequestOption>,
36    ) -> SDKResult<BaseResponse<GetRootFolderMetaRespData>> {
37        let api_req = ApiRequest {
38            http_method: Method::GET,
39            api_path: DRIVE_V1_FOLDERS_ROOT_FOLDER_META.to_string(),
40            supported_access_token_types: vec![AccessTokenType::User],
41            ..Default::default()
42        };
43
44        let api_resp = Transport::request(api_req, &self.config, option).await?;
45        Ok(api_resp)
46    }
47
48    /// 获取文件夹中的文件清单
49    ///
50    /// 该接口用于根据文件夹的token获取文件夹中的文件清单。
51    ///
52    /// <https://open.feishu.cn/document/server-docs/docs/drive-v1/folder/list>
53    pub async fn list_files(
54        &self,
55        request: ListFilesRequest,
56        option: Option<RequestOption>,
57    ) -> SDKResult<BaseResponse<ListFilesRespData>> {
58        let mut api_req = ApiRequest {
59            http_method: Method::GET,
60            api_path: DRIVE_V1_FOLDER_CHILDREN.replace("{}", &request.folder_token),
61            ..Default::default()
62        };
63        api_req.supported_access_token_types = vec![AccessTokenType::User, AccessTokenType::Tenant];
64
65        // 添加查询参数
66        if let Some(page_token) = request.page_token {
67            api_req.query_params.insert("page_token", page_token);
68        }
69        if let Some(page_size) = request.page_size {
70            api_req
71                .query_params
72                .insert("page_size", page_size.to_string());
73        }
74        if let Some(order_by) = request.order_by {
75            api_req.query_params.insert("order_by", order_by);
76        }
77        if let Some(direction) = request.direction {
78            api_req.query_params.insert("direction", direction);
79        }
80
81        let api_resp = Transport::request(api_req, &self.config, option).await?;
82        Ok(api_resp)
83    }
84
85    /// 获取文件夹元数据
86    ///
87    /// 该接口用于根据文件夹的token获取文件夹的详细元数据信息。
88    ///
89    /// <https://open.feishu.cn/document/server-docs/docs/drive-v1/folder/get-folder-meta>
90    pub async fn get_folder_meta(
91        &self,
92        request: GetFolderMetaRequest,
93        option: Option<RequestOption>,
94    ) -> SDKResult<BaseResponse<GetFolderMetaRespData>> {
95        let api_req = ApiRequest {
96            http_method: Method::GET,
97            api_path: DRIVE_V1_FOLDER_GET.replace("{}", &request.folder_token),
98            supported_access_token_types: vec![AccessTokenType::User, AccessTokenType::Tenant],
99            ..Default::default()
100        };
101
102        let api_resp = Transport::request(api_req, &self.config, option).await?;
103        Ok(api_resp)
104    }
105
106    /// 新建文件夹
107    ///
108    /// 该接口用于根据父文件夹的token在其中创建一个新的空文件夹。
109    ///
110    /// <https://open.feishu.cn/document/server-docs/docs/drive-v1/folder/create_folder>
111    pub async fn create_folder(
112        &self,
113        request: CreateFolderRequest,
114        option: Option<RequestOption>,
115    ) -> SDKResult<BaseResponse<CreateFolderRespData>> {
116        let api_req = ApiRequest {
117            http_method: Method::POST,
118            api_path: DRIVE_V1_FOLDERS.to_string(),
119            supported_access_token_types: vec![AccessTokenType::User, AccessTokenType::Tenant],
120            body: serde_json::to_vec(&request)?,
121            ..Default::default()
122        };
123
124        let api_resp = Transport::request(api_req, &self.config, option).await?;
125        Ok(api_resp)
126    }
127
128    /// 移动或删除文件夹
129    ///
130    /// 该接口用于根据文件夹的token移动或删除文件夹。
131    ///
132    /// <https://open.feishu.cn/document/server-docs/docs/drive-v1/folder/move-delete-folder>
133    pub async fn move_or_delete_folder(
134        &self,
135        request: MoveOrDeleteFolderRequest,
136        option: Option<RequestOption>,
137    ) -> SDKResult<BaseResponse<MoveOrDeleteFolderRespData>> {
138        let mut api_req = ApiRequest {
139            http_method: Method::POST,
140            api_path: DRIVE_V1_FOLDER_MOVE.replace("{}", &request.folder_token),
141            supported_access_token_types: vec![AccessTokenType::User, AccessTokenType::Tenant],
142            ..Default::default()
143        };
144
145        // 构建请求体,只包含需要的字段
146        let body = serde_json::json!({
147            "type": request.operation_type,
148            "parent_token": request.parent_token
149        });
150        api_req.body = serde_json::to_vec(&body)?;
151
152        let api_resp = Transport::request(api_req, &self.config, option).await?;
153        Ok(api_resp)
154    }
155
156    /// 查询异步任务状态
157    ///
158    /// 该接口用于查询异步任务的执行状态,如移动或删除文件夹等操作。
159    ///
160    /// <https://open.feishu.cn/document/server-docs/docs/drive-v1/file/async-task/task_check>
161    pub async fn check_async_task(
162        &self,
163        request: CheckAsyncTaskRequest,
164        option: Option<RequestOption>,
165    ) -> SDKResult<BaseResponse<CheckAsyncTaskRespData>> {
166        let api_req = ApiRequest {
167            http_method: Method::GET,
168            api_path: DRIVE_V1_TASK_GET.replace("{}", &request.task_id),
169            supported_access_token_types: vec![AccessTokenType::User, AccessTokenType::Tenant],
170            ..Default::default()
171        };
172
173        let api_resp = Transport::request(api_req, &self.config, option).await?;
174        Ok(api_resp)
175    }
176}
177
178/// 获取我的空间(root folder)元数据响应数据
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct GetRootFolderMetaRespData {
181    /// 用户空间的根目录 token
182    pub token: String,
183    /// 用户 ID
184    pub user_id: String,
185}
186
187impl ApiResponseTrait for GetRootFolderMetaRespData {
188    fn data_format() -> ResponseFormat {
189        ResponseFormat::Data
190    }
191}
192
193/// 获取文件夹中的文件清单请求参数
194#[derive(Debug, Clone, Default, Serialize, Deserialize)]
195pub struct ListFilesRequest {
196    /// 文件夹的token
197    pub folder_token: String,
198    /// 分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该 page_token 获取查询结果
199    pub page_token: Option<String>,
200    /// 分页大小,最大200
201    pub page_size: Option<i32>,
202    /// 排序字段,支持:创建时间(created_time)、修改时间(edited_time)、文件类型(file_type)、大小(size)
203    pub order_by: Option<String>,
204    /// 排序方向,支持:升序(ASC)、降序(DESC)
205    pub direction: Option<String>,
206}
207
208impl ListFilesRequest {
209    pub fn new(folder_token: impl Into<String>) -> Self {
210        Self {
211            folder_token: folder_token.into(),
212            ..Default::default()
213        }
214    }
215
216    pub fn builder() -> ListFilesRequestBuilder {
217        ListFilesRequestBuilder::default()
218    }
219}
220
221/// 获取文件夹中的文件清单请求构建器
222#[derive(Debug, Clone, Default)]
223pub struct ListFilesRequestBuilder {
224    request: ListFilesRequest,
225}
226
227impl ListFilesRequestBuilder {
228    pub fn folder_token(mut self, folder_token: impl Into<String>) -> Self {
229        self.request.folder_token = folder_token.into();
230        self
231    }
232
233    pub fn page_token(mut self, page_token: impl Into<String>) -> Self {
234        self.request.page_token = Some(page_token.into());
235        self
236    }
237
238    pub fn page_size(mut self, page_size: i32) -> Self {
239        self.request.page_size = Some(page_size);
240        self
241    }
242
243    pub fn order_by(mut self, order_by: impl Into<String>) -> Self {
244        self.request.order_by = Some(order_by.into());
245        self
246    }
247
248    pub fn direction(mut self, direction: impl Into<String>) -> Self {
249        self.request.direction = Some(direction.into());
250        self
251    }
252
253    pub fn build(self) -> ListFilesRequest {
254        self.request
255    }
256}
257
258/// 获取文件夹中的文件清单响应数据
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct ListFilesRespData {
261    /// 是否还有更多项
262    pub has_more: bool,
263    /// 分页标记,当 has_more 为 true 时,会返回新的 page_token,否则不返回 page_token
264    pub page_token: Option<String>,
265    /// 文件清单
266    pub files: Vec<DriveFile>,
267}
268
269impl ApiResponseTrait for ListFilesRespData {
270    fn data_format() -> ResponseFormat {
271        ResponseFormat::Data
272    }
273}
274
275/// 驱动文件信息
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct DriveFile {
278    /// 文件的token
279    pub token: String,
280    /// 文件名
281    pub name: String,
282    /// 文件类型
283    #[serde(rename = "type")]
284    pub file_type: String,
285    /// 父文件夹token
286    pub parent_token: Option<String>,
287    /// 文件链接
288    pub url: Option<String>,
289    /// 文件短链接
290    pub short_url: Option<String>,
291    /// 文件大小(字节)
292    pub size: Option<i64>,
293    /// 文件mime类型
294    pub mime_type: Option<String>,
295    /// 创建时间
296    pub created_time: Option<String>,
297    /// 修改时间
298    pub modified_time: Option<String>,
299    /// 拥有者id
300    pub owner_id: Option<String>,
301}
302
303/// 获取文件夹元数据请求参数
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct GetFolderMetaRequest {
306    /// 文件夹的token
307    pub folder_token: String,
308}
309
310impl GetFolderMetaRequest {
311    pub fn new(folder_token: impl Into<String>) -> Self {
312        Self {
313            folder_token: folder_token.into(),
314        }
315    }
316}
317
318/// 获取文件夹元数据响应数据
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct GetFolderMetaRespData {
321    /// 文件夹token
322    pub token: String,
323    /// 文件夹ID
324    pub id: String,
325    /// 文件夹名称
326    pub name: String,
327    /// 父文件夹token
328    pub parent_token: Option<String>,
329    /// 拥有者ID
330    pub owner_id: String,
331    /// 创建者ID
332    pub creator_id: Option<String>,
333    /// 创建时间
334    pub create_time: String,
335    /// 修改时间
336    pub edit_time: String,
337    /// 文件夹描述
338    pub description: Option<String>,
339    /// 文件夹链接
340    pub url: String,
341}
342
343impl ApiResponseTrait for GetFolderMetaRespData {
344    fn data_format() -> ResponseFormat {
345        ResponseFormat::Data
346    }
347}
348
349/// 新建文件夹请求参数
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct CreateFolderRequest {
352    /// 文件夹名称
353    pub name: String,
354    /// 父文件夹token
355    pub parent_token: String,
356}
357
358impl CreateFolderRequest {
359    pub fn new(name: impl Into<String>, parent_token: impl Into<String>) -> Self {
360        Self {
361            name: name.into(),
362            parent_token: parent_token.into(),
363        }
364    }
365}
366
367/// 新建文件夹响应数据
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct CreateFolderRespData {
370    /// 新创建文件夹的token
371    pub token: String,
372    /// 新创建文件夹的链接
373    pub url: String,
374}
375
376impl ApiResponseTrait for CreateFolderRespData {
377    fn data_format() -> ResponseFormat {
378        ResponseFormat::Data
379    }
380}
381
382/// 移动或删除文件夹请求参数
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct MoveOrDeleteFolderRequest {
385    /// 文件夹token
386    pub folder_token: String,
387    /// 操作类型,move: 移动,delete: 删除
388    #[serde(rename = "type")]
389    pub operation_type: String,
390    /// 移动的目标父文件夹token(删除操作时可以为空)
391    pub parent_token: Option<String>,
392}
393
394impl MoveOrDeleteFolderRequest {
395    /// 创建移动文件夹的请求
396    pub fn move_folder(folder_token: impl Into<String>, parent_token: impl Into<String>) -> Self {
397        Self {
398            folder_token: folder_token.into(),
399            operation_type: "move".to_string(),
400            parent_token: Some(parent_token.into()),
401        }
402    }
403
404    /// 创建删除文件夹的请求
405    pub fn delete_folder(folder_token: impl Into<String>) -> Self {
406        Self {
407            folder_token: folder_token.into(),
408            operation_type: "delete".to_string(),
409            parent_token: None,
410        }
411    }
412}
413
414/// 移动或删除文件夹响应数据
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct MoveOrDeleteFolderRespData {
417    /// 异步任务ID,可以通过该ID查询任务执行状态
418    pub task_id: Option<String>,
419}
420
421impl ApiResponseTrait for MoveOrDeleteFolderRespData {
422    fn data_format() -> ResponseFormat {
423        ResponseFormat::Data
424    }
425}
426
427/// 查询异步任务状态请求参数
428#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct CheckAsyncTaskRequest {
430    /// 任务ID
431    pub task_id: String,
432}
433
434impl CheckAsyncTaskRequest {
435    pub fn new(task_id: impl Into<String>) -> Self {
436        Self {
437            task_id: task_id.into(),
438        }
439    }
440}
441
442/// 查询异步任务状态响应数据
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct CheckAsyncTaskRespData {
445    /// 任务状态,PENDING: 等待中,SUCCESS: 成功,FAILURE: 失败
446    pub status: String,
447    /// 任务错误信息(如果失败)
448    pub error_msg: Option<String>,
449}
450
451impl ApiResponseTrait for CheckAsyncTaskRespData {
452    fn data_format() -> ResponseFormat {
453        ResponseFormat::Data
454    }
455}
456
457// === 宏实现 ===
458
459impl_executable_builder_owned!(
460    ListFilesRequestBuilder,
461    FolderService,
462    ListFilesRequest,
463    BaseResponse<ListFilesRespData>,
464    list_files
465);
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470    use rstest::*;
471
472    fn mock_config() -> Config {
473        Config::builder()
474            .app_id("test_app_id")
475            .app_secret("test_app_secret")
476            .build()
477    }
478
479    // === FolderService Tests ===
480
481    #[test]
482    fn test_folder_service_new() {
483        let config = mock_config();
484        let service = FolderService::new(config.clone());
485        assert_eq!(service.config.app_id, config.app_id);
486    }
487
488    // === Request Data Structure Tests ===
489
490    #[test]
491    fn test_list_files_request_new() {
492        let request = ListFilesRequest::new("test_folder_token");
493        assert_eq!(request.folder_token, "test_folder_token");
494        assert!(request.page_token.is_none());
495        assert!(request.page_size.is_none());
496        assert!(request.order_by.is_none());
497        assert!(request.direction.is_none());
498    }
499
500    #[test]
501    fn test_list_files_request_builder() {
502        let request = ListFilesRequest::builder()
503            .folder_token("folder123")
504            .page_token("page123")
505            .page_size(100)
506            .order_by("created_time")
507            .direction("ASC")
508            .build();
509
510        assert_eq!(request.folder_token, "folder123");
511        assert_eq!(request.page_token, Some("page123".to_string()));
512        assert_eq!(request.page_size, Some(100));
513        assert_eq!(request.order_by, Some("created_time".to_string()));
514        assert_eq!(request.direction, Some("ASC".to_string()));
515    }
516
517    #[test]
518    fn test_list_files_request_builder_fluent() {
519        let request = ListFilesRequest::builder()
520            .folder_token("test")
521            .page_size(50)
522            .order_by("modified_time")
523            .build();
524
525        assert_eq!(request.folder_token, "test");
526        assert_eq!(request.page_size, Some(50));
527        assert_eq!(request.order_by, Some("modified_time".to_string()));
528        assert!(request.page_token.is_none());
529        assert!(request.direction.is_none());
530    }
531
532    #[test]
533    fn test_get_folder_meta_request_new() {
534        let request = GetFolderMetaRequest::new("folder_token_123");
535        assert_eq!(request.folder_token, "folder_token_123");
536    }
537
538    #[test]
539    fn test_create_folder_request_new() {
540        let request = CreateFolderRequest::new("My Folder", "parent_token_456");
541        assert_eq!(request.name, "My Folder");
542        assert_eq!(request.parent_token, "parent_token_456");
543    }
544
545    #[test]
546    fn test_move_or_delete_folder_request_move() {
547        let request = MoveOrDeleteFolderRequest::move_folder("folder123", "new_parent456");
548        assert_eq!(request.folder_token, "folder123");
549        assert_eq!(request.operation_type, "move");
550        assert_eq!(request.parent_token, Some("new_parent456".to_string()));
551    }
552
553    #[test]
554    fn test_move_or_delete_folder_request_delete() {
555        let request = MoveOrDeleteFolderRequest::delete_folder("folder789");
556        assert_eq!(request.folder_token, "folder789");
557        assert_eq!(request.operation_type, "delete");
558        assert!(request.parent_token.is_none());
559    }
560
561    #[test]
562    fn test_check_async_task_request_new() {
563        let request = CheckAsyncTaskRequest::new("task_id_abc");
564        assert_eq!(request.task_id, "task_id_abc");
565    }
566
567    // === Response Data Structure Tests ===
568
569    #[test]
570    fn test_get_root_folder_meta_resp_data() {
571        let data = GetRootFolderMetaRespData {
572            token: "root_token".to_string(),
573            user_id: "user123".to_string(),
574        };
575        assert_eq!(data.token, "root_token");
576        assert_eq!(data.user_id, "user123");
577    }
578
579    #[test]
580    fn test_list_files_resp_data() {
581        let file = DriveFile {
582            token: "file_token".to_string(),
583            name: "document.pdf".to_string(),
584            file_type: "pdf".to_string(),
585            parent_token: Some("parent123".to_string()),
586            url: Some("https://example.com/file".to_string()),
587            short_url: Some("https://short.ly/abc".to_string()),
588            size: Some(1024000),
589            mime_type: Some("application/pdf".to_string()),
590            created_time: Some("2023-01-01T00:00:00Z".to_string()),
591            modified_time: Some("2023-01-02T00:00:00Z".to_string()),
592            owner_id: Some("owner123".to_string()),
593        };
594
595        let data = ListFilesRespData {
596            has_more: true,
597            page_token: Some("next_page".to_string()),
598            files: vec![file.clone()],
599        };
600
601        assert!(data.has_more);
602        assert_eq!(data.page_token, Some("next_page".to_string()));
603        assert_eq!(data.files.len(), 1);
604        assert_eq!(data.files[0].token, "file_token");
605        assert_eq!(data.files[0].name, "document.pdf");
606        assert_eq!(data.files[0].size, Some(1024000));
607    }
608
609    #[test]
610    fn test_drive_file_optional_fields() {
611        let file = DriveFile {
612            token: "minimal_file".to_string(),
613            name: "simple.txt".to_string(),
614            file_type: "txt".to_string(),
615            parent_token: None,
616            url: None,
617            short_url: None,
618            size: None,
619            mime_type: None,
620            created_time: None,
621            modified_time: None,
622            owner_id: None,
623        };
624
625        assert_eq!(file.token, "minimal_file");
626        assert_eq!(file.name, "simple.txt");
627        assert!(file.parent_token.is_none());
628        assert!(file.url.is_none());
629        assert!(file.size.is_none());
630    }
631
632    #[test]
633    fn test_get_folder_meta_resp_data() {
634        let data = GetFolderMetaRespData {
635            token: "folder_token".to_string(),
636            id: "folder_id".to_string(),
637            name: "My Documents".to_string(),
638            parent_token: Some("parent_folder".to_string()),
639            owner_id: "owner123".to_string(),
640            creator_id: Some("creator456".to_string()),
641            create_time: "2023-01-01T00:00:00Z".to_string(),
642            edit_time: "2023-01-02T00:00:00Z".to_string(),
643            description: Some("Folder description".to_string()),
644            url: "https://example.com/folder".to_string(),
645        };
646
647        assert_eq!(data.token, "folder_token");
648        assert_eq!(data.name, "My Documents");
649        assert_eq!(data.parent_token, Some("parent_folder".to_string()));
650        assert_eq!(data.description, Some("Folder description".to_string()));
651    }
652
653    #[test]
654    fn test_create_folder_resp_data() {
655        let data = CreateFolderRespData {
656            token: "new_folder_token".to_string(),
657            url: "https://example.com/new-folder".to_string(),
658        };
659        assert_eq!(data.token, "new_folder_token");
660        assert_eq!(data.url, "https://example.com/new-folder");
661    }
662
663    #[test]
664    fn test_move_or_delete_folder_resp_data() {
665        let data = MoveOrDeleteFolderRespData {
666            task_id: Some("async_task_123".to_string()),
667        };
668        assert_eq!(data.task_id, Some("async_task_123".to_string()));
669
670        let data_no_task = MoveOrDeleteFolderRespData { task_id: None };
671        assert!(data_no_task.task_id.is_none());
672    }
673
674    #[test]
675    fn test_check_async_task_resp_data() {
676        let success_data = CheckAsyncTaskRespData {
677            status: "SUCCESS".to_string(),
678            error_msg: None,
679        };
680        assert_eq!(success_data.status, "SUCCESS");
681        assert!(success_data.error_msg.is_none());
682
683        let failure_data = CheckAsyncTaskRespData {
684            status: "FAILURE".to_string(),
685            error_msg: Some("Task failed due to insufficient permissions".to_string()),
686        };
687        assert_eq!(failure_data.status, "FAILURE");
688        assert_eq!(
689            failure_data.error_msg,
690            Some("Task failed due to insufficient permissions".to_string())
691        );
692    }
693
694    // === Serialization Tests ===
695
696    #[rstest]
697    #[case("list_files_request")]
698    #[case("get_folder_meta_request")]
699    #[case("create_folder_request")]
700    #[case("move_or_delete_folder_request")]
701    #[case("check_async_task_request")]
702    fn test_request_serialization_roundtrip(#[case] request_type: &str) {
703        match request_type {
704            "list_files_request" => {
705                let original = ListFilesRequest::builder()
706                    .folder_token("test123")
707                    .page_size(50)
708                    .order_by("created_time")
709                    .direction("DESC")
710                    .build();
711                let json = serde_json::to_string(&original).unwrap();
712                let deserialized: ListFilesRequest = serde_json::from_str(&json).unwrap();
713                assert_eq!(original.folder_token, deserialized.folder_token);
714                assert_eq!(original.page_size, deserialized.page_size);
715                assert_eq!(original.order_by, deserialized.order_by);
716            }
717            "get_folder_meta_request" => {
718                let original = GetFolderMetaRequest::new("folder123");
719                let json = serde_json::to_string(&original).unwrap();
720                let deserialized: GetFolderMetaRequest = serde_json::from_str(&json).unwrap();
721                assert_eq!(original.folder_token, deserialized.folder_token);
722            }
723            "create_folder_request" => {
724                let original = CreateFolderRequest::new("Test Folder", "parent123");
725                let json = serde_json::to_string(&original).unwrap();
726                let deserialized: CreateFolderRequest = serde_json::from_str(&json).unwrap();
727                assert_eq!(original.name, deserialized.name);
728                assert_eq!(original.parent_token, deserialized.parent_token);
729            }
730            "move_or_delete_folder_request" => {
731                let original = MoveOrDeleteFolderRequest::move_folder("folder123", "new_parent");
732                let json = serde_json::to_string(&original).unwrap();
733                let deserialized: MoveOrDeleteFolderRequest = serde_json::from_str(&json).unwrap();
734                assert_eq!(original.folder_token, deserialized.folder_token);
735                assert_eq!(original.operation_type, deserialized.operation_type);
736                assert_eq!(original.parent_token, deserialized.parent_token);
737            }
738            "check_async_task_request" => {
739                let original = CheckAsyncTaskRequest::new("task123");
740                let json = serde_json::to_string(&original).unwrap();
741                let deserialized: CheckAsyncTaskRequest = serde_json::from_str(&json).unwrap();
742                assert_eq!(original.task_id, deserialized.task_id);
743            }
744            _ => panic!("Unknown request type: {}", request_type),
745        }
746    }
747
748    #[rstest]
749    #[case("get_root_folder_meta_resp")]
750    #[case("list_files_resp")]
751    #[case("get_folder_meta_resp")]
752    #[case("create_folder_resp")]
753    #[case("move_or_delete_folder_resp")]
754    #[case("check_async_task_resp")]
755    fn test_response_serialization_roundtrip(#[case] response_type: &str) {
756        match response_type {
757            "get_root_folder_meta_resp" => {
758                let original = GetRootFolderMetaRespData {
759                    token: "root123".to_string(),
760                    user_id: "user456".to_string(),
761                };
762                let json = serde_json::to_string(&original).unwrap();
763                let deserialized: GetRootFolderMetaRespData = serde_json::from_str(&json).unwrap();
764                assert_eq!(original.token, deserialized.token);
765                assert_eq!(original.user_id, deserialized.user_id);
766            }
767            "list_files_resp" => {
768                let original = ListFilesRespData {
769                    has_more: false,
770                    page_token: None,
771                    files: vec![],
772                };
773                let json = serde_json::to_string(&original).unwrap();
774                let deserialized: ListFilesRespData = serde_json::from_str(&json).unwrap();
775                assert_eq!(original.has_more, deserialized.has_more);
776                assert_eq!(original.page_token, deserialized.page_token);
777                assert_eq!(original.files.len(), deserialized.files.len());
778            }
779            "get_folder_meta_resp" => {
780                let original = GetFolderMetaRespData {
781                    token: "folder123".to_string(),
782                    id: "id123".to_string(),
783                    name: "Test".to_string(),
784                    parent_token: None,
785                    owner_id: "owner123".to_string(),
786                    creator_id: None,
787                    create_time: "2023-01-01T00:00:00Z".to_string(),
788                    edit_time: "2023-01-01T00:00:00Z".to_string(),
789                    description: None,
790                    url: "https://example.com".to_string(),
791                };
792                let json = serde_json::to_string(&original).unwrap();
793                let deserialized: GetFolderMetaRespData = serde_json::from_str(&json).unwrap();
794                assert_eq!(original.token, deserialized.token);
795                assert_eq!(original.name, deserialized.name);
796            }
797            "create_folder_resp" => {
798                let original = CreateFolderRespData {
799                    token: "new123".to_string(),
800                    url: "https://example.com/new".to_string(),
801                };
802                let json = serde_json::to_string(&original).unwrap();
803                let deserialized: CreateFolderRespData = serde_json::from_str(&json).unwrap();
804                assert_eq!(original.token, deserialized.token);
805                assert_eq!(original.url, deserialized.url);
806            }
807            "move_or_delete_folder_resp" => {
808                let original = MoveOrDeleteFolderRespData {
809                    task_id: Some("task123".to_string()),
810                };
811                let json = serde_json::to_string(&original).unwrap();
812                let deserialized: MoveOrDeleteFolderRespData = serde_json::from_str(&json).unwrap();
813                assert_eq!(original.task_id, deserialized.task_id);
814            }
815            "check_async_task_resp" => {
816                let original = CheckAsyncTaskRespData {
817                    status: "PENDING".to_string(),
818                    error_msg: None,
819                };
820                let json = serde_json::to_string(&original).unwrap();
821                let deserialized: CheckAsyncTaskRespData = serde_json::from_str(&json).unwrap();
822                assert_eq!(original.status, deserialized.status);
823                assert_eq!(original.error_msg, deserialized.error_msg);
824            }
825            _ => panic!("Unknown response type: {}", response_type),
826        }
827    }
828
829    // === ApiResponseTrait Tests ===
830
831    #[rstest]
832    #[case("GetRootFolderMetaRespData")]
833    #[case("ListFilesRespData")]
834    #[case("GetFolderMetaRespData")]
835    #[case("CreateFolderRespData")]
836    #[case("MoveOrDeleteFolderRespData")]
837    #[case("CheckAsyncTaskRespData")]
838    fn test_api_response_trait(#[case] response_type: &str) {
839        let format = match response_type {
840            "GetRootFolderMetaRespData" => GetRootFolderMetaRespData::data_format(),
841            "ListFilesRespData" => ListFilesRespData::data_format(),
842            "GetFolderMetaRespData" => GetFolderMetaRespData::data_format(),
843            "CreateFolderRespData" => CreateFolderRespData::data_format(),
844            "MoveOrDeleteFolderRespData" => MoveOrDeleteFolderRespData::data_format(),
845            "CheckAsyncTaskRespData" => CheckAsyncTaskRespData::data_format(),
846            _ => panic!("Unknown response type: {}", response_type),
847        };
848        assert_eq!(format, ResponseFormat::Data);
849    }
850
851    // === Edge Cases and Validation Tests ===
852
853    #[test]
854    fn test_empty_folder_token() {
855        let request = ListFilesRequest::new("");
856        assert_eq!(request.folder_token, "");
857    }
858
859    #[test]
860    fn test_very_long_folder_token() {
861        let long_token = "a".repeat(1000);
862        let request = ListFilesRequest::new(&long_token);
863        assert_eq!(request.folder_token, long_token);
864    }
865
866    #[test]
867    fn test_unicode_folder_names() {
868        let unicode_name = "文件夹测试🗂️";
869        let request = CreateFolderRequest::new(unicode_name, "parent123");
870        assert_eq!(request.name, unicode_name);
871    }
872
873    #[test]
874    fn test_special_characters_in_names() {
875        let special_name = "Folder with spaces & symbols @#$%";
876        let request = CreateFolderRequest::new(special_name, "parent");
877        assert_eq!(request.name, special_name);
878    }
879
880    #[test]
881    fn test_large_page_size() {
882        let request = ListFilesRequest::builder()
883            .folder_token("test")
884            .page_size(999999)
885            .build();
886        assert_eq!(request.page_size, Some(999999));
887    }
888
889    #[test]
890    fn test_negative_page_size() {
891        let request = ListFilesRequest::builder()
892            .folder_token("test")
893            .page_size(-1)
894            .build();
895        assert_eq!(request.page_size, Some(-1));
896    }
897
898    #[test]
899    fn test_drive_file_with_large_size() {
900        let file = DriveFile {
901            token: "large_file".to_string(),
902            name: "huge_video.mp4".to_string(),
903            file_type: "mp4".to_string(),
904            parent_token: Some("parent".to_string()),
905            url: None,
906            short_url: None,
907            size: Some(i64::MAX),
908            mime_type: Some("video/mp4".to_string()),
909            created_time: None,
910            modified_time: None,
911            owner_id: None,
912        };
913        assert_eq!(file.size, Some(i64::MAX));
914    }
915
916    #[test]
917    fn test_drive_file_zero_size() {
918        let file = DriveFile {
919            token: "empty_file".to_string(),
920            name: "empty.txt".to_string(),
921            file_type: "txt".to_string(),
922            parent_token: None,
923            url: None,
924            short_url: None,
925            size: Some(0),
926            mime_type: Some("text/plain".to_string()),
927            created_time: None,
928            modified_time: None,
929            owner_id: None,
930        };
931        assert_eq!(file.size, Some(0));
932    }
933
934    #[test]
935    fn test_task_status_variations() {
936        let statuses = ["PENDING", "SUCCESS", "FAILURE", "RUNNING", "CANCELLED"];
937        for status in statuses {
938            let resp = CheckAsyncTaskRespData {
939                status: status.to_string(),
940                error_msg: None,
941            };
942            assert_eq!(resp.status, status);
943        }
944    }
945
946    #[test]
947    fn test_long_error_message() {
948        let long_error = "Error: ".repeat(100);
949        let resp = CheckAsyncTaskRespData {
950            status: "FAILURE".to_string(),
951            error_msg: Some(long_error.clone()),
952        };
953        assert_eq!(resp.error_msg, Some(long_error));
954    }
955
956    // === Builder Pattern Edge Cases ===
957
958    #[test]
959    fn test_list_files_builder_chaining() {
960        let builder = ListFilesRequest::builder();
961        let request = builder
962            .folder_token("test")
963            .page_size(10)
964            .page_size(20) // Override previous
965            .order_by("name")
966            .build();
967        assert_eq!(request.page_size, Some(20));
968        assert_eq!(request.order_by, Some("name".to_string()));
969    }
970
971    #[test]
972    fn test_empty_builder() {
973        let request = ListFilesRequest::builder().build();
974        assert_eq!(request.folder_token, "");
975        assert!(request.page_token.is_none());
976        assert!(request.page_size.is_none());
977    }
978
979    // === Sorting and Direction Tests ===
980
981    #[rstest]
982    #[case("created_time", "ASC")]
983    #[case("edited_time", "DESC")]
984    #[case("file_type", "ASC")]
985    #[case("size", "DESC")]
986    #[case("name", "ASC")]
987    fn test_valid_sort_combinations(#[case] order_by: &str, #[case] direction: &str) {
988        let request = ListFilesRequest::builder()
989            .folder_token("test")
990            .order_by(order_by)
991            .direction(direction)
992            .build();
993        assert_eq!(request.order_by, Some(order_by.to_string()));
994        assert_eq!(request.direction, Some(direction.to_string()));
995    }
996
997    #[test]
998    fn test_invalid_sort_parameters() {
999        let request = ListFilesRequest::builder()
1000            .folder_token("test")
1001            .order_by("invalid_field")
1002            .direction("INVALID_DIRECTION")
1003            .build();
1004        assert_eq!(request.order_by, Some("invalid_field".to_string()));
1005        assert_eq!(request.direction, Some("INVALID_DIRECTION".to_string()));
1006    }
1007
1008    // === Clone and Debug Tests ===
1009
1010    #[test]
1011    fn test_request_clone() {
1012        let original = ListFilesRequest::builder()
1013            .folder_token("test")
1014            .page_size(50)
1015            .build();
1016        let cloned = original.clone();
1017        assert_eq!(original.folder_token, cloned.folder_token);
1018        assert_eq!(original.page_size, cloned.page_size);
1019    }
1020
1021    #[test]
1022    fn test_response_debug() {
1023        let data = CreateFolderRespData {
1024            token: "debug_test".to_string(),
1025            url: "https://test.com".to_string(),
1026        };
1027        let debug_str = format!("{:?}", data);
1028        assert!(debug_str.contains("debug_test"));
1029        assert!(debug_str.contains("https://test.com"));
1030    }
1031}