open_lark/core/
error_helper.rs

1use std::time::Duration;
2
3use crate::core::{
4    api_resp::BaseResponse,
5    error::LarkAPIError,
6    error_codes::{ErrorCategory, LarkErrorCode},
7};
8
9/// 错误处理助手工具
10pub struct ErrorHelper;
11
12impl ErrorHelper {
13    /// 根据错误类型提供智能处理建议
14    pub fn handle_error(error: &LarkAPIError) -> ErrorHandlingAdvice {
15        let mut advice = ErrorHandlingAdvice::default();
16
17        match error {
18            LarkAPIError::ApiError { code, message, .. } => {
19                if let Some(error_code) = LarkErrorCode::from_code(*code) {
20                    advice = Self::handle_api_error(error_code, message);
21                } else {
22                    advice.message = format!("未知API错误: {message} (错误码: {code})");
23                    advice.category = ErrorHandlingCategory::Unknown;
24                }
25            }
26            LarkAPIError::RequestError(req_err) => {
27                advice = Self::handle_request_error(req_err);
28            }
29            LarkAPIError::MissingAccessToken => {
30                advice.message = "缺少访问令牌".to_string();
31                advice.category = ErrorHandlingCategory::Authentication;
32                advice.actions.push("配置正确的访问令牌".to_string());
33                advice.is_recoverable = true;
34            }
35            LarkAPIError::IllegalParamError(msg) => {
36                advice.message = format!("参数错误: {msg}");
37                advice.category = ErrorHandlingCategory::ClientError;
38                advice.actions.push("检查请求参数格式和内容".to_string());
39                advice.is_recoverable = true;
40            }
41            _ => {
42                advice.message = format!("系统错误: {error}");
43                advice.category = ErrorHandlingCategory::SystemError;
44            }
45        }
46
47        advice
48    }
49
50    /// 处理API错误
51    fn handle_api_error(error_code: LarkErrorCode, _message: &str) -> ErrorHandlingAdvice {
52        let mut advice = ErrorHandlingAdvice {
53            error_code: Some(error_code),
54            message: error_code.detailed_description().to_string(),
55            ..Default::default()
56        };
57
58        match error_code.category() {
59            ErrorCategory::Authentication => {
60                advice.category = ErrorHandlingCategory::Authentication;
61                advice.is_recoverable = true;
62                advice.actions.extend(vec![
63                    "重新获取访问令牌".to_string(),
64                    "检查应用配置".to_string(),
65                ]);
66            }
67            ErrorCategory::Permission => {
68                advice.category = ErrorHandlingCategory::Permission;
69                advice.is_recoverable = true;
70                advice.actions.extend(vec![
71                    "检查应用权限配置".to_string(),
72                    "联系管理员添加必要权限".to_string(),
73                ]);
74            }
75            ErrorCategory::RateLimit => {
76                advice.category = ErrorHandlingCategory::RateLimit;
77                advice.is_recoverable = true;
78                advice.is_retryable = true;
79                advice.retry_delay = error_code.suggested_retry_delay();
80                advice.actions.push("降低请求频率或稍后重试".to_string());
81            }
82            ErrorCategory::Server => {
83                advice.category = ErrorHandlingCategory::ServerError;
84                advice.is_recoverable = true;
85                advice.is_retryable = true;
86                advice.retry_delay = error_code.suggested_retry_delay();
87                advice.actions.push("稍后重试或联系技术支持".to_string());
88            }
89            ErrorCategory::Network => {
90                advice.category = ErrorHandlingCategory::NetworkError;
91                advice.is_recoverable = true;
92                advice.is_retryable = true;
93                advice.actions.push("检查网络连接".to_string());
94            }
95            _ => {
96                advice.category = ErrorHandlingCategory::ClientError;
97                advice.actions.push("检查请求参数和调用方式".to_string());
98            }
99        }
100
101        if let Some(help_url) = error_code.help_url() {
102            advice.help_url = Some(help_url.to_string());
103        }
104
105        advice
106    }
107
108    /// 处理网络请求错误
109    fn handle_request_error(req_err: &str) -> ErrorHandlingAdvice {
110        let mut advice = ErrorHandlingAdvice {
111            category: ErrorHandlingCategory::NetworkError,
112            is_recoverable: true,
113            ..Default::default()
114        };
115
116        if req_err.contains("timeout") || req_err.contains("timed out") {
117            advice.message = "请求超时".to_string();
118            advice.is_retryable = true;
119            advice.retry_delay = Some(5);
120            advice.actions.extend(vec![
121                "增加请求超时时间".to_string(),
122                "检查网络连接状况".to_string(),
123            ]);
124        } else if req_err.contains("connect") || req_err.contains("connection") {
125            advice.message = "连接失败".to_string();
126            advice.is_retryable = true;
127            advice.retry_delay = Some(10);
128            advice.actions.extend(vec![
129                "检查网络连接".to_string(),
130                "确认代理设置".to_string(),
131                "检查防火墙配置".to_string(),
132            ]);
133        } else if req_err.contains("request") {
134            advice.message = "请求构建失败".to_string();
135            advice.actions.push("检查请求参数格式".to_string());
136        } else {
137            advice.message = format!("网络错误: {req_err}");
138            advice.actions.push("检查网络连接和服务状态".to_string());
139        }
140
141        advice
142    }
143
144    /// 根据响应创建处理建议
145    pub fn analyze_response<T>(response: &BaseResponse<T>) -> Option<ErrorHandlingAdvice> {
146        if response.success() {
147            return None;
148        }
149
150        let mut advice = ErrorHandlingAdvice::default();
151
152        if let Some(error_code) = response.error_code() {
153            advice = Self::handle_api_error(error_code, response.msg());
154        } else {
155            advice.message = format!("{} (错误码: {})", response.msg(), response.code());
156            advice.category = ErrorHandlingCategory::Unknown;
157        }
158
159        Some(advice)
160    }
161
162    /// 创建重试策略
163    pub fn create_retry_strategy(error: &LarkAPIError) -> Option<RetryStrategy> {
164        if !error.is_retryable() {
165            return None;
166        }
167
168        let mut strategy = RetryStrategy::default();
169
170        match error {
171            LarkAPIError::ApiError { code, .. } => {
172                if let Some(error_code) = LarkErrorCode::from_code(*code) {
173                    strategy.max_attempts = match error_code {
174                        LarkErrorCode::TooManyRequests => 3,
175                        LarkErrorCode::InternalServerError => 5,
176                        LarkErrorCode::ServiceUnavailable => 3,
177                        LarkErrorCode::GatewayTimeout => 3,
178                        _ => 3,
179                    };
180                    strategy.base_delay =
181                        Duration::from_secs(error_code.suggested_retry_delay().unwrap_or(5));
182                }
183            }
184            LarkAPIError::RequestError(req_err) => {
185                if req_err.contains("timeout") || req_err.contains("timed out") {
186                    strategy.max_attempts = 3;
187                    strategy.base_delay = Duration::from_secs(5);
188                } else if req_err.contains("connect") || req_err.contains("connection") {
189                    strategy.max_attempts = 5;
190                    strategy.base_delay = Duration::from_secs(10);
191                }
192            }
193            _ => {
194                strategy.max_attempts = 3;
195                strategy.base_delay = Duration::from_secs(5);
196            }
197        }
198
199        Some(strategy)
200    }
201
202    /// 格式化错误信息供用户显示
203    pub fn format_user_error(error: &LarkAPIError) -> String {
204        match error {
205            LarkAPIError::ApiError { code, .. } => {
206                if let Some(error_code) = LarkErrorCode::from_code(*code) {
207                    error_code.detailed_description().to_string()
208                } else {
209                    format!("API调用失败,错误码: {code}")
210                }
211            }
212            _ => error.user_friendly_message(),
213        }
214    }
215
216    /// 创建错误上下文信息
217    pub fn create_error_context(error: &LarkAPIError) -> ErrorContext {
218        let advice = Self::handle_error(error);
219        ErrorContext {
220            error_message: error.to_string(),
221            user_friendly_message: Self::format_user_error(error),
222            category: advice.category,
223            is_recoverable: advice.is_recoverable,
224            is_retryable: advice.is_retryable,
225            suggested_actions: advice.actions,
226            help_url: advice.help_url,
227            retry_strategy: Self::create_retry_strategy(error),
228        }
229    }
230}
231
232/// 错误处理建议
233#[derive(Debug, Clone)]
234pub struct ErrorHandlingAdvice {
235    /// 错误消息
236    pub message: String,
237    /// 错误类别
238    pub category: ErrorHandlingCategory,
239    /// 错误码(如果是API错误)
240    pub error_code: Option<LarkErrorCode>,
241    /// 是否可恢复
242    pub is_recoverable: bool,
243    /// 是否可重试
244    pub is_retryable: bool,
245    /// 建议的重试延迟(秒)
246    pub retry_delay: Option<u64>,
247    /// 建议的操作
248    pub actions: Vec<String>,
249    /// 帮助文档链接
250    pub help_url: Option<String>,
251}
252
253impl Default for ErrorHandlingAdvice {
254    fn default() -> Self {
255        Self {
256            message: String::new(),
257            category: ErrorHandlingCategory::Unknown,
258            error_code: None,
259            is_recoverable: false,
260            is_retryable: false,
261            retry_delay: None,
262            actions: Vec::new(),
263            help_url: None,
264        }
265    }
266}
267
268/// 错误处理类别
269#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
270pub enum ErrorHandlingCategory {
271    /// 认证错误
272    Authentication,
273    /// 权限错误
274    Permission,
275    /// 客户端错误
276    ClientError,
277    /// 服务器错误
278    ServerError,
279    /// 网络错误
280    NetworkError,
281    /// 限流错误
282    RateLimit,
283    /// 系统错误
284    SystemError,
285    /// 未知错误
286    Unknown,
287}
288
289/// 重试策略
290#[derive(Debug, Clone)]
291pub struct RetryStrategy {
292    /// 最大重试次数
293    pub max_attempts: u32,
294    /// 基础延迟时间
295    pub base_delay: Duration,
296    /// 是否使用指数退避
297    pub use_exponential_backoff: bool,
298    /// 最大延迟时间
299    pub max_delay: Duration,
300}
301
302impl Default for RetryStrategy {
303    fn default() -> Self {
304        Self {
305            max_attempts: 3,
306            base_delay: Duration::from_secs(5),
307            use_exponential_backoff: true,
308            max_delay: Duration::from_secs(60),
309        }
310    }
311}
312
313impl RetryStrategy {
314    /// 计算指定尝试次数的延迟时间
315    pub fn calculate_delay(&self, attempt: u32) -> Duration {
316        if !self.use_exponential_backoff {
317            return self.base_delay;
318        }
319
320        let multiplier = 2_u32.pow(attempt);
321        let delay = self.base_delay * multiplier;
322        std::cmp::min(delay, self.max_delay)
323    }
324}
325
326/// 错误上下文信息
327#[derive(Debug, Clone)]
328pub struct ErrorContext {
329    /// 原始错误消息
330    pub error_message: String,
331    /// 用户友好的错误消息
332    pub user_friendly_message: String,
333    /// 错误类别
334    pub category: ErrorHandlingCategory,
335    /// 是否可恢复
336    pub is_recoverable: bool,
337    /// 是否可重试
338    pub is_retryable: bool,
339    /// 建议的操作
340    pub suggested_actions: Vec<String>,
341    /// 帮助文档链接
342    pub help_url: Option<String>,
343    /// 重试策略
344    pub retry_strategy: Option<RetryStrategy>,
345}
346
347impl ErrorContext {
348    /// 打印详细的错误信息
349    pub fn print_details(&self) {
350        println!("❌ 错误: {}", self.user_friendly_message);
351        println!("类别: {:?}", self.category);
352
353        if self.is_recoverable {
354            println!("✅ 此错误可以恢复");
355        } else {
356            println!("⚠️ 此错误可能需要人工干预");
357        }
358
359        if self.is_retryable {
360            println!("🔄 此错误可以重试");
361            if let Some(strategy) = &self.retry_strategy {
362                println!("   建议最大重试次数: {}", strategy.max_attempts);
363                println!("   基础延迟时间: {:?}", strategy.base_delay);
364            }
365        }
366
367        if !self.suggested_actions.is_empty() {
368            println!("\n💡 建议操作:");
369            for (i, action) in self.suggested_actions.iter().enumerate() {
370                println!("   {}. {}", i + 1, action);
371            }
372        }
373
374        if let Some(url) = &self.help_url {
375            println!("\n🔗 帮助文档: {url}");
376        }
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383    use rstest::rstest;
384
385    #[test]
386    fn test_error_helper_api_error() {
387        let error = LarkAPIError::api_error(403, "Forbidden", None);
388        let advice = ErrorHelper::handle_error(&error);
389
390        assert_eq!(advice.category, ErrorHandlingCategory::Permission);
391        assert!(advice.is_recoverable);
392        assert!(!advice.actions.is_empty());
393    }
394
395    #[test]
396    fn test_retry_strategy() {
397        let error = LarkAPIError::api_error(429, "Too Many Requests", None);
398        let strategy = ErrorHelper::create_retry_strategy(&error);
399
400        assert!(strategy.is_some());
401        let strategy = strategy.unwrap();
402        assert_eq!(strategy.max_attempts, 3);
403    }
404
405    #[test]
406    fn test_error_context() {
407        let error = LarkAPIError::MissingAccessToken;
408        let context = ErrorHelper::create_error_context(&error);
409
410        assert_eq!(context.category, ErrorHandlingCategory::Authentication);
411        assert!(context.is_recoverable);
412    }
413
414    // New comprehensive tests
415
416    #[rstest]
417    #[case(
418        LarkAPIError::MissingAccessToken,
419        ErrorHandlingCategory::Authentication,
420        true,
421        false
422    )]
423    #[case(LarkAPIError::IllegalParamError("invalid param".to_string()), ErrorHandlingCategory::ClientError, true, false)]
424    fn test_handle_error_various_types(
425        #[case] error: LarkAPIError,
426        #[case] expected_category: ErrorHandlingCategory,
427        #[case] expected_recoverable: bool,
428        #[case] expected_retryable: bool,
429    ) {
430        let advice = ErrorHelper::handle_error(&error);
431
432        assert_eq!(advice.category, expected_category);
433        assert_eq!(advice.is_recoverable, expected_recoverable);
434        assert_eq!(advice.is_retryable, expected_retryable);
435        assert!(!advice.message.is_empty());
436    }
437
438    #[rstest]
439    #[case("timeout error", true, Some(5))]
440    #[case("connection failed", true, Some(10))]
441    #[case("invalid request", false, None)]
442    #[case("other network error", false, None)]
443    fn test_handle_request_error(
444        #[case] error_msg: &str,
445        #[case] expected_retryable: bool,
446        #[case] expected_delay: Option<u64>,
447    ) {
448        let advice = ErrorHelper::handle_request_error(error_msg);
449
450        assert_eq!(advice.category, ErrorHandlingCategory::NetworkError);
451        assert!(advice.is_recoverable);
452        assert_eq!(advice.is_retryable, expected_retryable);
453        assert_eq!(advice.retry_delay, expected_delay);
454        assert!(!advice.actions.is_empty());
455    }
456
457    #[test]
458    fn test_handle_api_error_with_different_error_codes() {
459        // Test authentication error - use a valid error code
460        let error_code = LarkErrorCode::AccessTokenInvalid; // 99991671
461        let advice = ErrorHelper::handle_api_error(error_code, "Invalid token");
462
463        assert_eq!(advice.category, ErrorHandlingCategory::Authentication);
464        assert!(advice.is_recoverable);
465        assert!(!advice.actions.is_empty());
466        assert!(advice.actions.iter().any(|a| a.contains("访问令牌")));
467
468        // Test rate limit error
469        let error_code = LarkErrorCode::TooManyRequests;
470        let advice = ErrorHelper::handle_api_error(error_code, "Rate limited");
471
472        assert_eq!(advice.category, ErrorHandlingCategory::RateLimit);
473        assert!(advice.is_recoverable);
474        assert!(advice.is_retryable);
475        assert!(advice.retry_delay.is_some());
476    }
477
478    #[test]
479    fn test_analyze_response_success() {
480        let response: BaseResponse<()> = BaseResponse {
481            raw_response: crate::core::api_resp::RawResponse {
482                code: 0,
483                msg: "ok".to_string(),
484                err: None,
485            },
486            data: None,
487        };
488
489        let advice = ErrorHelper::analyze_response(&response);
490        assert!(advice.is_none());
491    }
492
493    #[test]
494    fn test_analyze_response_error() {
495        let response: BaseResponse<()> = BaseResponse {
496            raw_response: crate::core::api_resp::RawResponse {
497                code: 400,
498                msg: "Bad Request".to_string(),
499                err: None,
500            },
501            data: None,
502        };
503
504        let advice = ErrorHelper::analyze_response(&response);
505        assert!(advice.is_some());
506
507        let advice = advice.unwrap();
508        assert!(!advice.message.is_empty());
509        // 400 is BadRequest, which should be categorized as ClientError
510        assert_eq!(advice.category, ErrorHandlingCategory::ClientError);
511    }
512
513    #[test]
514    fn test_create_retry_strategy_various_errors() {
515        // Test retryable error
516        let error = LarkAPIError::api_error(429, "Too Many Requests", None);
517        let strategy = ErrorHelper::create_retry_strategy(&error);
518        assert!(strategy.is_some());
519
520        // Test non-retryable error
521        let error = LarkAPIError::IllegalParamError("invalid".to_string());
522        let strategy = ErrorHelper::create_retry_strategy(&error);
523        assert!(strategy.is_none());
524
525        // Test timeout request error
526        let error = LarkAPIError::RequestError("timeout error".to_string());
527        let strategy = ErrorHelper::create_retry_strategy(&error);
528        assert!(strategy.is_some());
529
530        let strategy = strategy.unwrap();
531        assert_eq!(strategy.max_attempts, 3);
532        assert_eq!(strategy.base_delay, Duration::from_secs(5));
533    }
534
535    #[test]
536    fn test_format_user_error() {
537        // Test API error with known code
538        let error = LarkAPIError::api_error(403, "Forbidden", None);
539        let message = ErrorHelper::format_user_error(&error);
540        assert!(!message.is_empty());
541
542        // Test API error with unknown code
543        let error = LarkAPIError::api_error(99999, "Unknown", None);
544        let message = ErrorHelper::format_user_error(&error);
545        assert!(message.contains("99999"));
546
547        // Test other error types
548        let error = LarkAPIError::MissingAccessToken;
549        let message = ErrorHelper::format_user_error(&error);
550        assert!(!message.is_empty());
551    }
552
553    #[test]
554    fn test_create_error_context_complete() {
555        let error = LarkAPIError::api_error(500, "Internal Server Error", None);
556        let context = ErrorHelper::create_error_context(&error);
557
558        assert!(!context.error_message.is_empty());
559        assert!(!context.user_friendly_message.is_empty());
560        assert!(!context.suggested_actions.is_empty());
561
562        // Should be retryable for server errors
563        assert!(context.is_retryable);
564        assert!(context.retry_strategy.is_some());
565    }
566
567    #[test]
568    fn test_error_handling_advice_default() {
569        let advice = ErrorHandlingAdvice::default();
570
571        assert!(advice.message.is_empty());
572        assert_eq!(advice.category, ErrorHandlingCategory::Unknown);
573        assert!(advice.error_code.is_none());
574        assert!(!advice.is_recoverable);
575        assert!(!advice.is_retryable);
576        assert!(advice.retry_delay.is_none());
577        assert!(advice.actions.is_empty());
578        assert!(advice.help_url.is_none());
579    }
580
581    #[test]
582    fn test_retry_strategy_default() {
583        let strategy = RetryStrategy::default();
584
585        assert_eq!(strategy.max_attempts, 3);
586        assert_eq!(strategy.base_delay, Duration::from_secs(5));
587        assert!(strategy.use_exponential_backoff);
588        assert_eq!(strategy.max_delay, Duration::from_secs(60));
589    }
590
591    #[test]
592    fn test_retry_strategy_calculate_delay() {
593        let strategy = RetryStrategy::default();
594
595        // Test exponential backoff
596        assert_eq!(strategy.calculate_delay(0), Duration::from_secs(5));
597        assert_eq!(strategy.calculate_delay(1), Duration::from_secs(10));
598        assert_eq!(strategy.calculate_delay(2), Duration::from_secs(20));
599        assert_eq!(strategy.calculate_delay(3), Duration::from_secs(40));
600
601        // Test max delay limit
602        assert_eq!(strategy.calculate_delay(10), Duration::from_secs(60));
603
604        // Test without exponential backoff
605        let strategy = RetryStrategy {
606            use_exponential_backoff: false,
607            ..Default::default()
608        };
609        assert_eq!(strategy.calculate_delay(5), Duration::from_secs(5));
610    }
611
612    #[test]
613    fn test_error_context_print_details() {
614        let context = ErrorContext {
615            error_message: "Original error".to_string(),
616            user_friendly_message: "User friendly error".to_string(),
617            category: ErrorHandlingCategory::NetworkError,
618            is_recoverable: true,
619            is_retryable: true,
620            suggested_actions: vec!["Action 1".to_string(), "Action 2".to_string()],
621            help_url: Some("https://example.com/help".to_string()),
622            retry_strategy: Some(RetryStrategy::default()),
623        };
624
625        // This test just ensures print_details doesn't panic
626        // In a real environment, you might want to capture stdout to verify output
627        context.print_details();
628    }
629
630    #[test]
631    fn test_error_handling_category_equality() {
632        assert_eq!(
633            ErrorHandlingCategory::Authentication,
634            ErrorHandlingCategory::Authentication
635        );
636        assert_ne!(
637            ErrorHandlingCategory::Authentication,
638            ErrorHandlingCategory::Permission
639        );
640    }
641
642    #[test]
643    fn test_unknown_api_error_code() {
644        let error = LarkAPIError::api_error(99999, "Unknown error", None);
645        let advice = ErrorHelper::handle_error(&error);
646
647        assert_eq!(advice.category, ErrorHandlingCategory::Unknown);
648        assert!(advice.message.contains("未知API错误"));
649        assert!(advice.message.contains("99999"));
650    }
651
652    #[test]
653    fn test_request_error_patterns() {
654        // Test various timeout patterns
655        let timeout_errors = vec!["request timeout", "connection timed out", "read timeout"];
656
657        for error_msg in timeout_errors {
658            let advice = ErrorHelper::handle_request_error(error_msg);
659            assert!(advice.is_retryable);
660            assert_eq!(advice.retry_delay, Some(5));
661        }
662
663        // Test various connection patterns
664        let connection_errors = vec![
665            "connection refused",
666            "cannot connect to server",
667            "connection reset",
668        ];
669
670        for error_msg in connection_errors {
671            let advice = ErrorHelper::handle_request_error(error_msg);
672            assert!(advice.is_retryable);
673            assert_eq!(advice.retry_delay, Some(10));
674        }
675    }
676}