open_lark/service/im/v1/file/
mod.rs1use reqwest::Method;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5use crate::core::{
6 api_req::ApiRequest,
7 api_resp::{ApiResponseTrait, BaseResponse, ResponseFormat},
8 config::Config,
9 constants::AccessTokenType,
10 endpoints::EndpointBuilder,
11 error::LarkAPIError,
12 http::Transport,
13 req_option::RequestOption,
14 standard_response::StandardResponse,
15 trait_system::executable_builder::ExecutableBuilder,
16 validation::{validate_file_name, validate_upload_file, ValidateBuilder, ValidationResult},
17 SDKResult,
18};
19use crate::impl_full_service;
20use async_trait::async_trait;
21
22pub struct FileService {
24 pub config: Config,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct CreateFileResponse {
30 pub file_key: String,
32}
33
34impl ApiResponseTrait for CreateFileResponse {
35 fn data_format() -> ResponseFormat {
36 ResponseFormat::Data
37 }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct GetFileResponse {
43 pub data: Vec<u8>,
45}
46
47impl ApiResponseTrait for GetFileResponse {
48 fn data_format() -> ResponseFormat {
49 ResponseFormat::Data
50 }
51}
52
53impl FileService {
54 pub fn new(config: Config) -> Self {
55 Self { config }
56 }
57
58 pub async fn create(
60 &self,
61 file_type: &str,
62 file_name: &str,
63 file_data: Vec<u8>,
64 option: Option<RequestOption>,
65 ) -> SDKResult<CreateFileResponse> {
66 let mut query_params = HashMap::new();
67 query_params.insert("file_type", file_type.to_string());
68 query_params.insert("file_name", file_name.to_string());
69
70 let api_req = ApiRequest {
71 http_method: Method::POST,
72 api_path: crate::core::endpoints::im::IM_V1_FILES.to_string(),
73 supported_access_token_types: vec![AccessTokenType::Tenant, AccessTokenType::User],
74 query_params,
75 body: file_data,
76 ..Default::default()
77 };
78
79 let api_resp: BaseResponse<CreateFileResponse> =
80 Transport::request(api_req, &self.config, option).await?;
81 api_resp.into_result()
82 }
83
84 pub async fn get(
86 &self,
87 file_key: &str,
88 option: Option<RequestOption>,
89 ) -> SDKResult<GetFileResponse> {
90 let api_req = ApiRequest {
91 http_method: Method::GET,
92 api_path: EndpointBuilder::replace_param(
93 crate::core::endpoints::im::IM_V1_DOWNLOAD_FILE,
94 "file_key",
95 file_key,
96 ),
97 supported_access_token_types: vec![AccessTokenType::Tenant, AccessTokenType::User],
98 ..Default::default()
99 };
100
101 let api_resp: BaseResponse<GetFileResponse> =
102 Transport::request(api_req, &self.config, option).await?;
103 api_resp.into_result()
104 }
105
106 pub fn upload_builder(&self) -> FileUploadBuilder {
108 FileUploadBuilder::new()
109 }
110
111 pub fn download_builder(&self) -> FileDownloadBuilder {
113 FileDownloadBuilder::new()
114 }
115}
116
117impl_full_service!(FileService, "im.file", "v1");
119
120#[derive(Debug, Clone, Default)]
122pub struct FileUploadRequest {
123 pub file_type: String,
125 pub file_name: String,
127 pub file_data: Vec<u8>,
129}
130
131#[derive(Default)]
133pub struct FileUploadBuilder {
134 request: FileUploadRequest,
135}
136
137impl FileUploadBuilder {
138 pub fn new() -> Self {
139 Self::default()
140 }
141
142 pub fn file_type(mut self, file_type: impl ToString) -> Self {
144 self.request.file_type = file_type.to_string();
145 self
146 }
147
148 pub fn file_name(mut self, file_name: impl ToString) -> Self {
150 self.request.file_name = file_name.to_string();
151 self
152 }
153
154 pub fn file_data(mut self, file_data: Vec<u8>) -> Self {
156 self.request.file_data = file_data;
157 self
158 }
159
160 pub fn build(self) -> SDKResult<FileUploadRequest> {
162 if self.request.file_type.is_empty() {
164 return Err(LarkAPIError::illegal_param(
165 "file_type is required".to_string(),
166 ));
167 }
168
169 let (cleaned_name, name_result) = validate_file_name(&self.request.file_name);
171 if !name_result.is_valid() {
172 return Err(LarkAPIError::illegal_param(format!(
173 "Invalid file_name: {}",
174 name_result.error().unwrap_or("unknown error")
175 )));
176 }
177
178 if self.request.file_data.is_empty() {
180 return Err(LarkAPIError::illegal_param(
181 "file_data cannot be empty".to_string(),
182 ));
183 }
184
185 let upload_result = validate_upload_file(&self.request.file_data, &cleaned_name, true);
187 if !upload_result.is_valid() {
188 return Err(LarkAPIError::illegal_param(format!(
189 "File validation failed: {}",
190 upload_result.error().unwrap_or("unknown error")
191 )));
192 }
193
194 Ok(FileUploadRequest {
195 file_type: self.request.file_type,
196 file_name: cleaned_name,
197 file_data: self.request.file_data,
198 })
199 }
200
201 pub fn build_unvalidated(self) -> FileUploadRequest {
203 self.request
204 }
205}
206
207impl ValidateBuilder for FileUploadBuilder {
208 fn validate(&self) -> ValidationResult {
209 if self.request.file_type.is_empty() {
211 return ValidationResult::Invalid("file_type is required".to_string());
212 }
213
214 let (_, name_result) = validate_file_name(&self.request.file_name);
216 if !name_result.is_valid() {
217 return name_result;
218 }
219
220 if self.request.file_data.is_empty() {
222 return ValidationResult::Invalid("file_data cannot be empty".to_string());
223 }
224
225 validate_upload_file(&self.request.file_data, &self.request.file_name, true)
227 }
228}
229
230#[async_trait]
231impl ExecutableBuilder<FileService, FileUploadRequest, CreateFileResponse> for FileUploadBuilder {
232 fn build(self) -> FileUploadRequest {
233 self.build_unvalidated()
235 }
236
237 async fn execute(self, service: &FileService) -> SDKResult<CreateFileResponse> {
238 let request = self.build_unvalidated();
239 service
240 .create(
241 &request.file_type,
242 &request.file_name,
243 request.file_data,
244 None,
245 )
246 .await
247 }
248
249 async fn execute_with_options(
250 self,
251 service: &FileService,
252 option: RequestOption,
253 ) -> SDKResult<CreateFileResponse> {
254 let request = self.build_unvalidated();
255 service
256 .create(
257 &request.file_type,
258 &request.file_name,
259 request.file_data,
260 Some(option),
261 )
262 .await
263 }
264}
265
266#[derive(Default)]
268pub struct FileDownloadBuilder {
269 file_key: Option<String>,
270}
271
272impl FileDownloadBuilder {
273 pub fn new() -> Self {
274 Self::default()
275 }
276
277 pub fn file_key(mut self, file_key: impl ToString) -> Self {
279 self.file_key = Some(file_key.to_string());
280 self
281 }
282
283 pub fn build(self) -> String {
284 self.file_key.unwrap_or_default()
285 }
286}
287
288#[async_trait]
289impl ExecutableBuilder<FileService, String, GetFileResponse> for FileDownloadBuilder {
290 fn build(self) -> String {
291 self.build()
292 }
293
294 async fn execute(self, service: &FileService) -> SDKResult<GetFileResponse> {
295 let file_key = self.build();
296 service.get(&file_key, None).await
297 }
298
299 async fn execute_with_options(
300 self,
301 service: &FileService,
302 option: RequestOption,
303 ) -> SDKResult<GetFileResponse> {
304 let file_key = self.build();
305 service.get(&file_key, Some(option)).await
306 }
307}
308
309#[cfg(test)]
310#[allow(unused_variables, unused_unsafe)]
311mod tests {
312 use super::*;
313 use crate::core::config::Config;
314
315 fn create_test_config() -> Config {
316 Config::default()
317 }
318
319 #[test]
320 fn test_file_service_creation() {
321 let config = create_test_config();
322 let service = FileService::new(config.clone());
323
324 assert_eq!(service.config.app_id, config.app_id);
325 assert_eq!(service.config.app_secret, config.app_secret);
326 }
327
328 #[test]
329 fn test_file_service_with_custom_config() {
330 let config = Config::builder()
331 .app_id("file_app")
332 .app_secret("file_secret")
333 .req_timeout(std::time::Duration::from_millis(15000))
334 .base_url("https://file.api.com")
335 .build();
336
337 let service = FileService::new(config.clone());
338
339 assert_eq!(service.config.app_id, "file_app");
340 assert_eq!(service.config.app_secret, "file_secret");
341 assert_eq!(service.config.base_url, "https://file.api.com");
342 assert_eq!(
343 service.config.req_timeout,
344 Some(std::time::Duration::from_millis(15000))
345 );
346 }
347
348 #[test]
349 fn test_file_service_config_independence() {
350 let config1 = Config::builder()
351 .app_id("file1")
352 .app_secret("secret1")
353 .build();
354 let config2 = Config::builder()
355 .app_id("file2")
356 .app_secret("secret2")
357 .build();
358
359 let service1 = FileService::new(config1);
360 let service2 = FileService::new(config2);
361
362 assert_eq!(service1.config.app_id, "file1");
363 assert_eq!(service2.config.app_id, "file2");
364 assert_ne!(service1.config.app_id, service2.config.app_id);
365 }
366
367 #[test]
368 fn test_file_service_memory_layout() {
369 let config = create_test_config();
370 let service = FileService::new(config);
371
372 let service_ptr = std::ptr::addr_of!(service) as *const u8;
373 let config_ptr = std::ptr::addr_of!(service.config) as *const u8;
374
375 assert!(
376 !service_ptr.is_null(),
377 "Service should have valid memory address"
378 );
379 assert!(
380 !config_ptr.is_null(),
381 "Config should have valid memory address"
382 );
383 }
384
385 #[test]
386 fn test_file_service_with_different_configurations() {
387 let test_configs = vec![
388 Config::builder()
389 .app_id("file_basic")
390 .app_secret("basic_secret")
391 .build(),
392 Config::builder()
393 .app_id("file_timeout")
394 .app_secret("timeout_secret")
395 .req_timeout(std::time::Duration::from_millis(12000))
396 .build(),
397 Config::builder()
398 .app_id("file_custom")
399 .app_secret("custom_secret")
400 .base_url("https://custom.file.com")
401 .build(),
402 Config::builder()
403 .app_id("file_full")
404 .app_secret("full_secret")
405 .req_timeout(std::time::Duration::from_millis(20000))
406 .base_url("https://full.file.com")
407 .enable_token_cache(false)
408 .build(),
409 ];
410
411 for config in test_configs {
412 let service = FileService::new(config.clone());
413
414 assert_eq!(service.config.app_id, config.app_id);
415 assert_eq!(service.config.app_secret, config.app_secret);
416 assert_eq!(service.config.base_url, config.base_url);
417 assert_eq!(service.config.req_timeout, config.req_timeout);
418 }
419 }
420
421 #[test]
422 fn test_file_service_multiple_instances() {
423 let config = create_test_config();
424 let service1 = FileService::new(config.clone());
425 let service2 = FileService::new(config.clone());
426
427 assert_eq!(service1.config.app_id, service2.config.app_id);
428 assert_eq!(service1.config.app_secret, service2.config.app_secret);
429
430 let ptr1 = std::ptr::addr_of!(service1) as *const u8;
431 let ptr2 = std::ptr::addr_of!(service2) as *const u8;
432 assert_ne!(ptr1, ptr2, "Services should be independent instances");
433 }
434
435 #[test]
436 fn test_file_service_config_cloning() {
437 let original_config = create_test_config();
438 let cloned_config = original_config.clone();
439
440 let service = FileService::new(cloned_config);
441
442 assert_eq!(service.config.app_id, original_config.app_id);
443 assert_eq!(service.config.app_secret, original_config.app_secret);
444 }
445
446 #[test]
447 fn test_file_service_with_empty_config() {
448 let config = Config::default();
449 let service = FileService::new(config);
450
451 assert_eq!(service.config.app_id, "");
452 assert_eq!(service.config.app_secret, "");
453 }
454
455 #[test]
456 fn test_file_service_with_unicode_config() {
457 let config = Config::builder()
458 .app_id("文件应用")
459 .app_secret("文件密钥")
460 .base_url("https://文件.com")
461 .build();
462 let service = FileService::new(config);
463
464 assert_eq!(service.config.app_id, "文件应用");
465 assert_eq!(service.config.app_secret, "文件密钥");
466 assert_eq!(service.config.base_url, "https://文件.com");
467 }
468
469 #[test]
470 fn test_file_service_with_extreme_timeout() {
471 let config = Config::builder()
472 .app_id("file_extreme")
473 .app_secret("extreme_secret")
474 .req_timeout(std::time::Duration::from_secs(7200))
475 .build();
476 let service = FileService::new(config);
477
478 assert_eq!(
479 service.config.req_timeout,
480 Some(std::time::Duration::from_secs(7200))
481 );
482 }
483
484 #[test]
485 fn test_file_service_builder_methods() {
486 let config = create_test_config();
487 let service = FileService::new(config);
488
489 let upload_builder = service.upload_builder();
490 let download_builder = service.download_builder();
491
492 let upload_ptr = std::ptr::addr_of!(upload_builder) as *const u8;
494 let download_ptr = std::ptr::addr_of!(download_builder) as *const u8;
495 assert!(!upload_ptr.is_null());
496 assert!(!download_ptr.is_null());
497 }
498
499 #[test]
500 fn test_file_upload_builder_basic() {
501 let builder = FileUploadBuilder::new()
502 .file_type("image")
503 .file_name("test.jpg")
504 .file_data(vec![1, 2, 3, 4]);
505
506 let request = builder.build_unvalidated();
507 assert_eq!(request.file_type, "image");
508 assert_eq!(request.file_name, "test.jpg");
509 assert_eq!(request.file_data, vec![1, 2, 3, 4]);
510 }
511
512 #[test]
513 fn test_file_upload_builder_chaining() {
514 let request = FileUploadBuilder::new()
515 .file_type("document")
516 .file_name("document.pdf")
517 .file_data(vec![0xFF, 0xFE, 0xFD])
518 .build_unvalidated();
519
520 assert_eq!(request.file_type, "document");
521 assert_eq!(request.file_name, "document.pdf");
522 assert_eq!(request.file_data, vec![0xFF, 0xFE, 0xFD]);
523 }
524
525 #[test]
526 fn test_file_download_builder_basic() {
527 let builder = FileDownloadBuilder::new().file_key("test_key_123");
528
529 let file_key = builder.build();
530 assert_eq!(file_key, "test_key_123");
531 }
532
533 #[test]
534 fn test_file_download_builder_empty() {
535 let builder = FileDownloadBuilder::new();
536 let file_key = builder.build();
537 assert_eq!(file_key, "");
538 }
539
540 #[test]
541 fn test_file_upload_request_default() {
542 let request = FileUploadRequest::default();
543 assert_eq!(request.file_type, "");
544 assert_eq!(request.file_name, "");
545 assert_eq!(request.file_data, Vec::<u8>::new());
546 }
547
548 #[test]
549 fn test_create_file_response_format() {
550 assert_eq!(CreateFileResponse::data_format(), ResponseFormat::Data);
551 }
552
553 #[test]
554 fn test_get_file_response_format() {
555 assert_eq!(GetFileResponse::data_format(), ResponseFormat::Data);
556 }
557}