open_lark/service/cloud_docs/sheets/v2/data_operation/
write_image.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{
4    core::{
5        api_req::ApiRequest,
6        api_resp::{ApiResponseTrait, BaseResponse, ResponseFormat},
7        constants::AccessTokenType,
8        endpoints::cloud_docs::*,
9        req_option, SDKResult,
10    },
11    service::cloud_docs::sheets::v2::SpreadsheetService,
12};
13
14/// 写入图片
15#[derive(Serialize, Debug, Default)]
16pub struct WriteImageRequest {
17    #[serde(skip)]
18    api_request: ApiRequest,
19    #[serde(skip)]
20    spreadsheet_token: String,
21    /// 查询范围 range=`<sheetId>!<开始格子>:<结束格子>`
22    /// 如:xxxx!A1:D5,详见在线表格开发指南。此处限定为一个格子,如: xxxx!A1:A1
23    range: String,
24    /// 需要写入的图片二进制流,支持 "PNG", "JPEG", "JPG", "GIF", "BMP", "JFIF", "EXIF", "TIFF",
25    /// "BPG", "HEIC" 等图片格式
26    image: Vec<u8>,
27    /// 写入的图片名字
28    name: String,
29}
30
31impl WriteImageRequest {
32    pub fn builder() -> WriteImageRequestBuilder {
33        WriteImageRequestBuilder::default()
34    }
35}
36
37#[derive(Default)]
38pub struct WriteImageRequestBuilder {
39    request: WriteImageRequest,
40}
41
42impl WriteImageRequestBuilder {
43    pub fn spreadsheet_token(mut self, spreadsheet_token: impl ToString) -> Self {
44        self.request.spreadsheet_token = spreadsheet_token.to_string();
45        self
46    }
47
48    pub fn range(mut self, range: impl ToString) -> Self {
49        self.request.range = range.to_string();
50        self
51    }
52
53    pub fn image(mut self, image: Vec<u8>) -> Self {
54        self.request.image = image;
55        self
56    }
57
58    pub fn name(mut self, name: impl ToString) -> Self {
59        self.request.name = name.to_string();
60        self
61    }
62
63    pub fn build(mut self) -> WriteImageRequest {
64        self.request.api_request.body = serde_json::to_vec(&self.request).unwrap();
65        self.request
66    }
67}
68
69/// 写入图片响应体
70#[derive(Debug, Deserialize)]
71pub struct WriteImageResponse {
72    /// spreadsheet 的 token
73    #[serde(rename = "spreadsheetToken")]
74    pub spread_sheet_token: String,
75    /// spreadsheet 的版本号
76    pub revision: i32,
77    /// 写入图片的range
78    #[serde(rename = "updateRange")]
79    pub update_range: String,
80}
81
82impl ApiResponseTrait for WriteImageResponse {
83    fn data_format() -> ResponseFormat {
84        ResponseFormat::Data
85    }
86}
87
88impl SpreadsheetService {
89    /// 该接口用于根据 spreadsheetToken 和 range 向单个格子写入图片。
90    pub async fn write_image(
91        &self,
92        request: WriteImageRequest,
93        option: Option<req_option::RequestOption>,
94    ) -> SDKResult<BaseResponse<WriteImageResponse>> {
95        let mut api_req = request.api_request;
96        api_req.api_path =
97            SHEETS_V2_SPREADSHEET_VALUES_IMAGE.replace("{}", &request.spreadsheet_token);
98        api_req.http_method = reqwest::Method::POST;
99        api_req.supported_access_token_types = vec![AccessTokenType::Tenant, AccessTokenType::App];
100
101        let api_resp = crate::core::http::Transport::request(api_req, &self.config, option).await?;
102
103        Ok(api_resp)
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use crate::{
110        core::{config::Config, constants::AppType},
111        service::cloud_docs::sheets::v2::{
112            data_operation::{WriteImageRequest, WriteImageResponse},
113            SpreadsheetService,
114        },
115    };
116
117    fn create_service() -> SpreadsheetService {
118        let config = Config::builder()
119            .app_id("test_app_id")
120            .app_secret("test_app_secret")
121            .app_type(AppType::SelfBuild)
122            .build();
123        SpreadsheetService { config }
124    }
125
126    fn create_test_image_data() -> Vec<u8> {
127        // Simple 1x1 PNG image (smallest valid PNG)
128        vec![
129            0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
130            0x00, 0x00, 0x00, 0x0D, // IHDR chunk length
131            0x49, 0x48, 0x44, 0x52, // IHDR chunk type
132            0x00, 0x00, 0x00, 0x01, // Width: 1
133            0x00, 0x00, 0x00, 0x01, // Height: 1
134            0x08, 0x06, 0x00, 0x00,
135            0x00, // Bit depth, color type, compression, filter, interlace
136            0x1F, 0x15, 0xC4, 0x89, // IHDR CRC
137            0x00, 0x00, 0x00, 0x0A, // IDAT chunk length
138            0x49, 0x44, 0x41, 0x54, // IDAT chunk type
139            0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00,
140            0x01, // Compressed image data
141            0x0D, 0x0A, 0x2D, 0xB4, // IDAT CRC
142            0x00, 0x00, 0x00, 0x00, // IEND chunk length
143            0x49, 0x45, 0x4E, 0x44, // IEND chunk type
144            0xAE, 0x42, 0x60, 0x82, // IEND CRC
145        ]
146    }
147
148    fn create_jpeg_header() -> Vec<u8> {
149        // JPEG file signature
150        vec![0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46]
151    }
152
153    #[test]
154    fn test_write_image_builder_default() {
155        let request = WriteImageRequest::builder().build();
156
157        assert_eq!(request.spreadsheet_token, "");
158        assert_eq!(request.range, "");
159        assert_eq!(request.image, Vec::<u8>::new());
160        assert_eq!(request.name, "");
161    }
162
163    #[test]
164    fn test_write_image_builder_basic() {
165        let image_data = create_test_image_data();
166        let request = WriteImageRequest::builder()
167            .spreadsheet_token("test_token")
168            .range("Sheet1!A1:A1")
169            .image(image_data.clone())
170            .name("test_image.png")
171            .build();
172
173        assert_eq!(request.spreadsheet_token, "test_token");
174        assert_eq!(request.range, "Sheet1!A1:A1");
175        assert_eq!(request.image, image_data);
176        assert_eq!(request.name, "test_image.png");
177    }
178
179    #[test]
180    fn test_write_image_builder_all_options() {
181        let image_data = create_test_image_data();
182        let request = WriteImageRequest::builder()
183            .spreadsheet_token("spreadsheet_abc123")
184            .range("Photos!B2:B2")
185            .image(image_data.clone())
186            .name("company_logo.png")
187            .build();
188
189        assert_eq!(request.spreadsheet_token, "spreadsheet_abc123");
190        assert_eq!(request.range, "Photos!B2:B2");
191        assert_eq!(request.image, image_data);
192        assert_eq!(request.name, "company_logo.png");
193    }
194
195    #[test]
196    fn test_write_image_builder_chaining() {
197        let image_data = create_test_image_data();
198        let request = WriteImageRequest::builder()
199            .spreadsheet_token("chain_test")
200            .range("Gallery!C3:C3")
201            .name("chained_image.png")
202            .image(image_data.clone())
203            .build();
204
205        assert_eq!(request.spreadsheet_token, "chain_test");
206        assert_eq!(request.range, "Gallery!C3:C3");
207        assert_eq!(request.image, image_data);
208        assert_eq!(request.name, "chained_image.png");
209    }
210
211    #[test]
212    fn test_write_image_with_different_formats() {
213        let png_data = create_test_image_data();
214        let jpeg_data = create_jpeg_header();
215
216        let png_request = WriteImageRequest::builder()
217            .spreadsheet_token("format_test")
218            .range("Images!A1:A1")
219            .image(png_data.clone())
220            .name("image.png")
221            .build();
222
223        let jpeg_request = WriteImageRequest::builder()
224            .spreadsheet_token("format_test")
225            .range("Images!B1:B1")
226            .image(jpeg_data.clone())
227            .name("image.jpg")
228            .build();
229
230        assert_eq!(png_request.image, png_data);
231        assert_eq!(jpeg_request.image, jpeg_data);
232        assert_eq!(png_request.name, "image.png");
233        assert_eq!(jpeg_request.name, "image.jpg");
234    }
235
236    #[test]
237    fn test_write_image_with_unicode_names() {
238        let image_data = create_test_image_data();
239        let request = WriteImageRequest::builder()
240            .spreadsheet_token("unicode_test")
241            .range("图片!A1:A1")
242            .image(image_data.clone())
243            .name("公司标志.png")
244            .build();
245
246        assert_eq!(request.spreadsheet_token, "unicode_test");
247        assert_eq!(request.range, "图片!A1:A1");
248        assert_eq!(request.image, image_data);
249        assert_eq!(request.name, "公司标志.png");
250    }
251
252    #[test]
253    fn test_write_image_with_special_characters() {
254        let image_data = create_test_image_data();
255        let request = WriteImageRequest::builder()
256            .spreadsheet_token("special_chars_test")
257            .range("'Sheet With Spaces'!A1:A1")
258            .image(image_data.clone())
259            .name("image@2x!#$%^&*().png")
260            .build();
261
262        assert_eq!(request.spreadsheet_token, "special_chars_test");
263        assert_eq!(request.range, "'Sheet With Spaces'!A1:A1");
264        assert_eq!(request.image, image_data);
265        assert_eq!(request.name, "image@2x!#$%^&*().png");
266    }
267
268    #[test]
269    fn test_write_image_empty_image() {
270        let request = WriteImageRequest::builder()
271            .spreadsheet_token("empty_test")
272            .range("Sheet1!A1:A1")
273            .image(vec![])
274            .name("empty.png")
275            .build();
276
277        assert_eq!(request.spreadsheet_token, "empty_test");
278        assert_eq!(request.range, "Sheet1!A1:A1");
279        assert_eq!(request.image, Vec::<u8>::new());
280        assert_eq!(request.name, "empty.png");
281    }
282
283    #[test]
284    fn test_write_image_large_data() {
285        let large_image_data = vec![0u8; 1024 * 1024]; // 1MB of data
286        let request = WriteImageRequest::builder()
287            .spreadsheet_token("large_image_test")
288            .range("BigImages!A1:A1")
289            .image(large_image_data.clone())
290            .name("large_image.png")
291            .build();
292
293        assert_eq!(request.spreadsheet_token, "large_image_test");
294        assert_eq!(request.range, "BigImages!A1:A1");
295        assert_eq!(request.image.len(), 1024 * 1024);
296        assert_eq!(request.name, "large_image.png");
297    }
298
299    #[test]
300    fn test_write_image_different_ranges() {
301        let image_data = create_test_image_data();
302
303        let ranges = [
304            "Sheet1!A1:A1",
305            "Sheet2!B5:B5",
306            "DataSheet!Z100:Z100",
307            "第一页!C10:C10",
308        ];
309
310        for (i, range) in ranges.iter().enumerate() {
311            let request = WriteImageRequest::builder()
312                .spreadsheet_token("range_test")
313                .range(range)
314                .image(image_data.clone())
315                .name(format!("image_{}.png", i))
316                .build();
317
318            assert_eq!(request.range, *range);
319            assert_eq!(request.name, format!("image_{}.png", i));
320        }
321    }
322
323    #[test]
324    fn test_write_image_serialization() {
325        let image_data = create_test_image_data();
326        let request = WriteImageRequest::builder()
327            .spreadsheet_token("serialization_test")
328            .range("Sheet1!A1:A1")
329            .image(image_data.clone())
330            .name("test_image.png")
331            .build();
332
333        let serialized = serde_json::to_string(&request);
334        assert!(serialized.is_ok());
335
336        let json_value: serde_json::Value = serde_json::from_str(&serialized.unwrap()).unwrap();
337        assert_eq!(json_value["range"], "Sheet1!A1:A1");
338        assert_eq!(json_value["name"], "test_image.png");
339        // Note: image data is binary and serialized as array of numbers
340        assert!(json_value["image"].is_array());
341    }
342
343    #[test]
344    fn test_write_image_response_deserialization() {
345        let response_json = serde_json::json!({
346            "spreadsheetToken": "test_token_123",
347            "revision": 456,
348            "updateRange": "Sheet1!A1:A1"
349        });
350
351        let response: WriteImageResponse = serde_json::from_value(response_json).unwrap();
352
353        assert_eq!(response.spread_sheet_token, "test_token_123");
354        assert_eq!(response.revision, 456);
355        assert_eq!(response.update_range, "Sheet1!A1:A1");
356    }
357
358    #[test]
359    fn test_write_image_various_extensions() {
360        let image_data = create_test_image_data();
361
362        let extensions = vec!["png", "jpg", "jpeg", "gif", "bmp", "tiff", "heic", "webp"];
363
364        for ext in extensions {
365            let request = WriteImageRequest::builder()
366                .spreadsheet_token("extension_test")
367                .range("Images!A1:A1")
368                .image(image_data.clone())
369                .name(format!("test_image.{}", ext))
370                .build();
371
372            assert_eq!(request.name, format!("test_image.{}", ext));
373            assert_eq!(request.image, image_data);
374        }
375    }
376
377    #[test]
378    fn test_write_image_long_filename() {
379        let image_data = create_test_image_data();
380        let long_name = "a".repeat(255) + ".png";
381
382        let request = WriteImageRequest::builder()
383            .spreadsheet_token("long_name_test")
384            .range("Sheet1!A1:A1")
385            .image(image_data.clone())
386            .name(&long_name)
387            .build();
388
389        assert_eq!(request.name, long_name);
390        assert_eq!(request.image, image_data);
391    }
392
393    #[test]
394    fn test_write_image_binary_data_integrity() {
395        let original_data = vec![0x89, 0x50, 0x4E, 0x47, 0xFF, 0x00, 0xAA, 0xBB];
396
397        let request = WriteImageRequest::builder()
398            .spreadsheet_token("binary_test")
399            .range("Sheet1!A1:A1")
400            .image(original_data.clone())
401            .name("binary_test.png")
402            .build();
403
404        assert_eq!(request.image, original_data);
405        // Verify each byte is preserved
406        for (i, &byte) in original_data.iter().enumerate() {
407            assert_eq!(request.image[i], byte);
408        }
409    }
410
411    #[test]
412    fn test_write_image_service_creation() {
413        let service = create_service();
414        assert_eq!(service.config.app_id, "test_app_id");
415        assert_eq!(service.config.app_secret, "test_app_secret");
416        assert!(matches!(service.config.app_type, AppType::SelfBuild));
417    }
418
419    #[test]
420    fn test_write_image_complex_range_references() {
421        let image_data = create_test_image_data();
422
423        let complex_ranges = vec![
424            "Sheet1!A1:A1",
425            "'Sheet with spaces'!B2:B2",
426            "工作表1!C3:C3",
427            "Sheet-Name_123!D4:D4",
428        ];
429
430        for range in complex_ranges {
431            let request = WriteImageRequest::builder()
432                .spreadsheet_token("complex_range_test")
433                .range(range)
434                .image(image_data.clone())
435                .name("test.png")
436                .build();
437
438            assert_eq!(request.range, range);
439        }
440    }
441
442    #[test]
443    fn test_write_image_different_image_sizes() {
444        let spreadsheet_token = "size_test";
445
446        // Test different image sizes
447        let sizes = vec![1, 10, 100, 1000, 10000];
448
449        for size in sizes {
450            let image_data = vec![0xFFu8; size];
451            let request = WriteImageRequest::builder()
452                .spreadsheet_token(spreadsheet_token)
453                .range("Sheet1!A1:A1")
454                .image(image_data.clone())
455                .name(format!("image_{}bytes.png", size))
456                .build();
457
458            assert_eq!(request.image.len(), size);
459            assert_eq!(request.name, format!("image_{}bytes.png", size));
460        }
461    }
462
463    #[test]
464    fn test_write_image_metadata_only() {
465        // Test building request with metadata but no image data yet
466        let request = WriteImageRequest::builder()
467            .spreadsheet_token("metadata_test")
468            .range("Sheet1!A1:A1")
469            .name("placeholder.png")
470            .build();
471
472        assert_eq!(request.spreadsheet_token, "metadata_test");
473        assert_eq!(request.range, "Sheet1!A1:A1");
474        assert_eq!(request.name, "placeholder.png");
475        assert_eq!(request.image, Vec::<u8>::new());
476    }
477
478    #[test]
479    fn test_write_image_very_long_token() {
480        let very_long_token = "a".repeat(1000);
481        let image_data = create_test_image_data();
482
483        let request = WriteImageRequest::builder()
484            .spreadsheet_token(&very_long_token)
485            .range("Sheet1!A1:A1")
486            .image(image_data.clone())
487            .name("test.png")
488            .build();
489
490        assert_eq!(request.spreadsheet_token, very_long_token);
491        assert_eq!(request.image, image_data);
492    }
493
494    #[test]
495    fn test_write_image_response_struct_debug() {
496        let response = WriteImageResponse {
497            spread_sheet_token: "debug_test".to_string(),
498            revision: 123,
499            update_range: "Sheet1!A1:A1".to_string(),
500        };
501
502        let debug_str = format!("{:?}", response);
503        assert!(debug_str.contains("debug_test"));
504        assert!(debug_str.contains("123"));
505        assert!(debug_str.contains("Sheet1!A1:A1"));
506    }
507
508    #[test]
509    fn test_write_image_builder_overwrites() {
510        let image_data1 = vec![1, 2, 3];
511        let image_data2 = vec![4, 5, 6];
512
513        let request = WriteImageRequest::builder()
514            .spreadsheet_token("original_token")
515            .spreadsheet_token("final_token") // Should overwrite
516            .range("Sheet1!A1:A1")
517            .range("Sheet2!B2:B2") // Should overwrite
518            .image(image_data1)
519            .image(image_data2.clone()) // Should overwrite
520            .name("original.png")
521            .name("final.png") // Should overwrite
522            .build();
523
524        assert_eq!(request.spreadsheet_token, "final_token");
525        assert_eq!(request.range, "Sheet2!B2:B2");
526        assert_eq!(request.image, image_data2);
527        assert_eq!(request.name, "final.png");
528    }
529}