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