Skip to main content

opendev_runtime/
error_handler.rs

1//! Error handling and recovery for operations.
2//!
3//! Provides error classification, user action selection, and dangerous
4//! operation confirmation. In a TUI/CLI context the actual prompting is
5//! handled by the caller — this module provides the types and logic.
6//!
7//! Ported from `opendev/core/runtime/monitoring/error_handler.py`.
8
9use serde::{Deserialize, Serialize};
10
11/// Actions user can take on error.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum ErrorAction {
15    /// Retry the operation.
16    Retry,
17    /// Skip this operation and continue with the next.
18    Skip,
19    /// Cancel all remaining operations.
20    Cancel,
21    /// Edit parameters and retry.
22    Edit,
23}
24
25impl ErrorAction {
26    /// Parse from a single-character string (r/s/c/e).
27    pub fn from_char(c: char) -> Option<Self> {
28        match c {
29            'r' => Some(Self::Retry),
30            's' => Some(Self::Skip),
31            'c' => Some(Self::Cancel),
32            'e' => Some(Self::Edit),
33            _ => None,
34        }
35    }
36
37    /// Single-character representation.
38    pub fn as_char(&self) -> char {
39        match self {
40            Self::Retry => 'r',
41            Self::Skip => 's',
42            Self::Cancel => 'c',
43            Self::Edit => 'e',
44        }
45    }
46}
47
48/// Result of error handling.
49#[derive(Debug, Clone)]
50pub struct ErrorResult {
51    pub action: ErrorAction,
52    pub should_retry: bool,
53    pub should_cancel: bool,
54    pub edited_params: Option<serde_json::Value>,
55}
56
57impl ErrorResult {
58    /// Create a retry result.
59    pub fn retry() -> Self {
60        Self {
61            action: ErrorAction::Retry,
62            should_retry: true,
63            should_cancel: false,
64            edited_params: None,
65        }
66    }
67
68    /// Create a skip result.
69    pub fn skip() -> Self {
70        Self {
71            action: ErrorAction::Skip,
72            should_retry: false,
73            should_cancel: false,
74            edited_params: None,
75        }
76    }
77
78    /// Create a cancel result.
79    pub fn cancel() -> Self {
80        Self {
81            action: ErrorAction::Cancel,
82            should_retry: false,
83            should_cancel: true,
84            edited_params: None,
85        }
86    }
87
88    /// Create an edit result with new parameters.
89    pub fn edit(params: serde_json::Value) -> Self {
90        Self {
91            action: ErrorAction::Edit,
92            should_retry: true,
93            should_cancel: false,
94            edited_params: Some(params),
95        }
96    }
97}
98
99/// Information about an operation error for display/handling.
100#[derive(Debug, Clone)]
101pub struct OperationError {
102    /// Human-readable error message.
103    pub message: String,
104    /// Operation type that failed (e.g. "bash_execute", "file_write").
105    pub operation_type: String,
106    /// Target of the operation (file path, command, etc.).
107    pub target: String,
108    /// Whether retry is a valid option.
109    pub allow_retry: bool,
110    /// Whether edit-and-retry is a valid option.
111    pub allow_edit: bool,
112}
113
114/// Build the list of available options for an operation error.
115pub fn available_actions(error: &OperationError) -> Vec<(ErrorAction, &'static str)> {
116    let mut actions = Vec::new();
117    if error.allow_retry {
118        actions.push((ErrorAction::Retry, "Retry"));
119    }
120    if error.allow_edit {
121        actions.push((ErrorAction::Edit, "Edit parameters and retry"));
122    }
123    actions.push((ErrorAction::Skip, "Skip this operation"));
124    actions.push((ErrorAction::Cancel, "Cancel all remaining operations"));
125    actions
126}
127
128/// Resolve a user's choice character into an `ErrorResult`.
129///
130/// Returns `None` if the choice is invalid or not allowed by the error options.
131pub fn resolve_choice(choice: char, error: &OperationError) -> Option<ErrorResult> {
132    match choice {
133        'r' if error.allow_retry => Some(ErrorResult::retry()),
134        's' => Some(ErrorResult::skip()),
135        'c' => Some(ErrorResult::cancel()),
136        'e' if error.allow_edit => {
137            // Edit flow would be handled by the caller; return a placeholder
138            None
139        }
140        _ => None,
141    }
142}
143
144/// Classify whether an error is likely transient and worth retrying.
145pub fn is_transient_error(message: &str) -> bool {
146    let lower = message.to_lowercase();
147    let transient_patterns = [
148        "timeout",
149        "connection reset",
150        "connection refused",
151        "temporarily unavailable",
152        "service unavailable",
153        "bad gateway",
154        "gateway timeout",
155        "rate limit",
156        "too many requests",
157        "overloaded",
158    ];
159    transient_patterns.iter().any(|p| lower.contains(p))
160}
161
162#[cfg(test)]
163#[path = "error_handler_tests.rs"]
164mod tests;