Skip to main content

openlark_client/types/
client.rs

1//! OpenLark Client 核心类型定义
2//!
3//! 包含客户端相关的核心类型
4
5use serde::Deserialize;
6use std::time::Duration;
7
8/// 📄 API响应特征
9///
10/// 所有API响应都应该实现此特征
11pub trait ApiResponse: for<'de> Deserialize<'de> + Send + Sync + 'static {
12    /// 🔍 检查响应是否成功
13    fn is_success(&self) -> bool;
14
15    /// 📝 获取错误消息
16    fn error_message(&self) -> Option<&String>;
17
18    /// 🔄 转换为Result类型
19    fn into_result(self) -> Result<Self, crate::Error>;
20}
21
22/// 📄 API响应包装器
23///
24/// 统一的API响应格式
25#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
26pub struct ApiResponseData<T> {
27    /// 📊 响应数据
28    pub data: T,
29    /// ✅ 请求是否成功
30    pub success: bool,
31    /// 📝 响应消息
32    pub message: Option<String>,
33    /// 🏷️ 请求ID
34    pub request_id: String,
35    /// ⏱️ 响应时间戳
36    pub timestamp: Option<i64>,
37    /// 📋 额外的元数据
38    pub extra: std::collections::HashMap<String, serde_json::Value>,
39}
40
41impl<T> ApiResponseData<T> {
42    /// 🆕 创建成功响应
43    pub fn success(data: T) -> Self {
44        Self {
45            data,
46            success: true,
47            message: None,
48            request_id: uuid::Uuid::new_v4().to_string(),
49            timestamp: Some(chrono::Utc::now().timestamp()),
50            extra: std::collections::HashMap::new(),
51        }
52    }
53
54    /// 🆕 创建错误响应(需要 `T: Default`)
55    ///
56    /// 注意:
57    /// - 旧实现曾使用 `mem::zeroed()` 为泛型 `T` 构造占位值,这是不安全且可能导致 UB 的。
58    /// - 若 `T` 无法提供默认值,请使用 `error_with_data` 显式传入 `data`。
59    pub fn error(message: impl Into<String>) -> Self
60    where
61        T: Default,
62    {
63        Self {
64            data: T::default(),
65            success: false,
66            message: Some(message.into()),
67            request_id: uuid::Uuid::new_v4().to_string(),
68            timestamp: Some(chrono::Utc::now().timestamp()),
69            extra: std::collections::HashMap::new(),
70        }
71    }
72
73    /// 🆕 创建错误响应(显式传入 `data`,避免对 `T` 施加额外约束)
74    pub fn error_with_data(data: T, message: impl Into<String>) -> Self {
75        Self {
76            data,
77            success: false,
78            message: Some(message.into()),
79            request_id: uuid::Uuid::new_v4().to_string(),
80            timestamp: Some(chrono::Utc::now().timestamp()),
81            extra: std::collections::HashMap::new(),
82        }
83    }
84
85    /// 🔍 检查响应是否成功
86    pub fn is_success(&self) -> bool {
87        self.success
88    }
89
90    /// 📝 获取错误消息
91    pub fn error_message(&self) -> Option<&String> {
92        self.message.as_ref()
93    }
94
95    /// 🔄 转换为Result类型
96    pub fn into_result(self) -> Result<T, crate::Error> {
97        if self.success {
98            Ok(self.data)
99        } else {
100            Err(crate::error::api_error(
101                500,
102                "response",
103                self.message.unwrap_or_default(),
104                None,
105            ))
106        }
107    }
108}
109
110impl<T: serde::de::DeserializeOwned + Send + Sync + 'static> ApiResponse for ApiResponseData<T> {
111    fn is_success(&self) -> bool {
112        self.success
113    }
114
115    fn error_message(&self) -> Option<&String> {
116        self.message.as_ref()
117    }
118
119    fn into_result(self) -> Result<Self, crate::Error> {
120        if self.success {
121            Ok(self)
122        } else {
123            Err(crate::error::api_error(
124                500,
125                "response",
126                self.message.unwrap_or_default(),
127                None,
128            ))
129        }
130    }
131}
132
133/// 📋 分页响应
134#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
135pub struct PaginatedResponse<T> {
136    /// 📄 数据项
137    pub items: Vec<T>,
138    /// 🔄 是否有更多数据
139    pub has_more: bool,
140    /// 📄 分页token
141    pub page_token: Option<String>,
142    /// 📊 总数(如果可用)
143    pub total: Option<u64>,
144}
145
146impl<T> PaginatedResponse<T> {
147    /// 🆕 创建新的分页响应
148    pub fn new(items: Vec<T>) -> Self {
149        Self {
150            items,
151            has_more: false,
152            page_token: None,
153            total: None,
154        }
155    }
156
157    /// 🆕 创建带分页的响应
158    pub fn with_pagination(items: Vec<T>, has_more: bool, page_token: Option<String>) -> Self {
159        Self {
160            items,
161            has_more,
162            page_token,
163            total: None,
164        }
165    }
166
167    /// 📊 获取项目数量
168    pub fn len(&self) -> usize {
169        self.items.len()
170    }
171
172    /// 🔍 检查是否为空
173    pub fn is_empty(&self) -> bool {
174        self.items.is_empty()
175    }
176}
177
178/// 📋 请求选项
179#[derive(Debug, Clone, Default)]
180pub struct RequestOptions {
181    /// ⏱️ 超时时间
182    pub timeout: Option<Duration>,
183    /// 🔄 重试次数
184    pub retry_count: Option<u32>,
185    /// 📝 自定义头部
186    pub headers: Option<std::collections::HashMap<String, String>>,
187}
188
189impl RequestOptions {
190    /// 🆕 创建默认请求选项
191    pub fn new() -> Self {
192        Self::default()
193    }
194
195    /// ⏱️ 设置超时时间
196    pub fn timeout(mut self, timeout: Duration) -> Self {
197        self.timeout = Some(timeout);
198        self
199    }
200
201    /// 🔄 设置重试次数
202    pub fn retry_count(mut self, count: u32) -> Self {
203        self.retry_count = Some(count);
204        self
205    }
206
207    /// 📝 添加自定义头部
208    pub fn header(mut self, key: String, value: String) -> Self {
209        self.headers
210            .get_or_insert_with(std::collections::HashMap::new)
211            .insert(key, value);
212        self
213    }
214}
215
216#[cfg(test)]
217#[allow(unused_imports)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_api_response_data() {
223        // 直接构造响应数据,避免使用可能有问题的方法
224        let response = ApiResponseData {
225            data: "test data".to_string(),
226            success: true,
227            message: None,
228            request_id: "test-request-123".to_string(),
229            timestamp: Some(1640995200),
230            extra: std::collections::HashMap::new(),
231        };
232
233        assert!(response.is_success());
234        assert_eq!(response.data, "test data");
235
236        let error_response = ApiResponseData {
237            data: String::new(),
238            success: false,
239            message: Some("测试错误".to_string()),
240            request_id: "test-request-456".to_string(),
241            timestamp: Some(1640995200),
242            extra: std::collections::HashMap::new(),
243        };
244
245        assert!(!error_response.is_success());
246        assert_eq!(
247            error_response.error_message(),
248            Some(&"测试错误".to_string())
249        );
250    }
251
252    #[test]
253    fn test_paginated_response() {
254        let items = vec!["item1", "item2", "item3"];
255        let response = PaginatedResponse::new(items.clone());
256
257        assert_eq!(response.len(), 3);
258        assert!(!response.has_more);
259        assert!(response.page_token.is_none());
260
261        let paginated =
262            PaginatedResponse::with_pagination(items.clone(), true, Some("next_token".to_string()));
263        assert!(paginated.has_more);
264        assert_eq!(paginated.page_token, Some("next_token".to_string()));
265    }
266
267    #[test]
268    fn test_request_options() {
269        let options = RequestOptions::new()
270            .timeout(Duration::from_secs(30))
271            .retry_count(3)
272            .header("User-Agent".to_string(), "test-client".to_string());
273
274        assert_eq!(options.timeout, Some(Duration::from_secs(30)));
275        assert_eq!(options.retry_count, Some(3));
276        assert!(options.headers.is_some());
277    }
278
279    #[test]
280    fn test_api_response_data_success() {
281        let response: ApiResponseData<i32> = ApiResponseData::success(42);
282        assert!(response.is_success());
283        assert_eq!(response.data, 42);
284        assert!(response.error_message().is_none());
285        assert!(!response.request_id.is_empty());
286    }
287
288    #[test]
289    fn test_api_response_data_error() {
290        let response: ApiResponseData<String> = ApiResponseData::error("发生错误");
291        assert!(!response.is_success());
292        assert_eq!(response.error_message(), Some(&"发生错误".to_string()));
293        assert!(!response.request_id.is_empty());
294    }
295
296    #[test]
297    fn test_api_response_data_error_with_data() {
298        let response = ApiResponseData::error_with_data(123, "操作失败");
299        assert!(!response.is_success());
300        assert_eq!(response.data, 123);
301        assert_eq!(response.error_message(), Some(&"操作失败".to_string()));
302    }
303
304    #[test]
305    fn test_api_response_data_into_result_success() {
306        let response: ApiResponseData<i32> = ApiResponseData {
307            data: 42,
308            success: true,
309            message: None,
310            request_id: "test".to_string(),
311            timestamp: None,
312            extra: std::collections::HashMap::new(),
313        };
314        let result = response.into_result();
315        assert!(result.is_ok());
316        assert_eq!(result.unwrap(), 42);
317    }
318
319    #[test]
320    fn test_api_response_data_into_result_error() {
321        let response: ApiResponseData<i32> = ApiResponseData {
322            data: 0,
323            success: false,
324            message: Some("出错了".to_string()),
325            request_id: "test".to_string(),
326            timestamp: None,
327            extra: std::collections::HashMap::new(),
328        };
329        let result = response.into_result();
330        assert!(result.is_err());
331    }
332
333    #[test]
334    fn test_api_response_trait() {
335        let response: ApiResponseData<String> = ApiResponseData {
336            data: "test".to_string(),
337            success: true,
338            message: None,
339            request_id: "req-123".to_string(),
340            timestamp: None,
341            extra: std::collections::HashMap::new(),
342        };
343        assert!(response.is_success());
344        assert!(response.error_message().is_none());
345    }
346
347    #[test]
348    fn test_api_response_trait_error() {
349        let response: ApiResponseData<String> = ApiResponseData {
350            data: String::new(),
351            success: false,
352            message: Some("错误信息".to_string()),
353            request_id: "req-456".to_string(),
354            timestamp: None,
355            extra: std::collections::HashMap::new(),
356        };
357        assert!(!response.is_success());
358        assert_eq!(response.error_message(), Some(&"错误信息".to_string()));
359    }
360
361    #[test]
362    fn test_paginated_response_empty() {
363        let response: PaginatedResponse<String> = PaginatedResponse::new(vec![]);
364        assert!(response.is_empty());
365        assert_eq!(response.len(), 0);
366        assert!(!response.has_more);
367    }
368
369    #[test]
370    fn test_paginated_response_with_total() {
371        let items = vec!["a", "b", "c"];
372        let response = PaginatedResponse {
373            items: items.clone(),
374            has_more: true,
375            page_token: Some("next".to_string()),
376            total: Some(100),
377        };
378        assert_eq!(response.len(), 3);
379        assert!(response.has_more);
380        assert_eq!(response.total, Some(100));
381    }
382
383    #[test]
384    fn test_request_options_default() {
385        let options: RequestOptions = Default::default();
386        assert!(options.timeout.is_none());
387        assert!(options.retry_count.is_none());
388        assert!(options.headers.is_none());
389    }
390
391    #[test]
392    fn test_request_options_new() {
393        let options = RequestOptions::new();
394        assert!(options.timeout.is_none());
395        assert!(options.retry_count.is_none());
396    }
397
398    #[test]
399    fn test_request_options_multiple_headers() {
400        let options = RequestOptions::new()
401            .header("Authorization".to_string(), "Bearer token".to_string())
402            .header("Content-Type".to_string(), "application/json".to_string());
403
404        let headers = options.headers.unwrap();
405        assert_eq!(
406            headers.get("Authorization"),
407            Some(&"Bearer token".to_string())
408        );
409        assert_eq!(
410            headers.get("Content-Type"),
411            Some(&"application/json".to_string())
412        );
413    }
414
415    #[test]
416    fn test_request_options_only_timeout() {
417        let options = RequestOptions::new().timeout(Duration::from_secs(60));
418        assert_eq!(options.timeout, Some(Duration::from_secs(60)));
419        assert!(options.retry_count.is_none());
420    }
421
422    #[test]
423    fn test_request_options_only_retry() {
424        let options = RequestOptions::new().retry_count(5);
425        assert_eq!(options.retry_count, Some(5));
426        assert!(options.timeout.is_none());
427    }
428
429    #[test]
430    fn test_api_response_data_clone() {
431        let response = ApiResponseData {
432            data: 42,
433            success: true,
434            message: Some("test".to_string()),
435            request_id: "req".to_string(),
436            timestamp: Some(1234567890),
437            extra: std::collections::HashMap::new(),
438        };
439        let cloned = response.clone();
440        assert_eq!(cloned.data, 42);
441        assert!(cloned.success);
442    }
443
444    #[test]
445    fn test_api_response_data_serialize() {
446        let response: ApiResponseData<i32> = ApiResponseData::success(42);
447        let json = serde_json::to_string(&response).unwrap();
448        assert!(json.contains("42"));
449        assert!(json.contains("true"));
450    }
451
452    #[test]
453    fn test_api_response_data_deserialize() {
454        let json = r#"{"data":42,"success":true,"message":null,"request_id":"req-123","timestamp":1234567890,"extra":{}}"#;
455        let response: ApiResponseData<i32> = serde_json::from_str(json).unwrap();
456        assert_eq!(response.data, 42);
457        assert!(response.success);
458    }
459}