1use crate::pipeline::PipelineError;
44use crate::protocol::ProtocolError;
45use thiserror::Error;
46
47#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct ErrorContext {
53 pub field_path: Option<String>,
56 pub details: Option<String>,
58 pub source: Option<String>,
60 pub hint: Option<String>,
62 pub request_id: Option<String>,
64 pub status_code: Option<u16>,
66 pub error_code: Option<String>,
68 pub retryable: Option<bool>,
70 pub fallbackable: Option<bool>,
72}
73
74impl ErrorContext {
75 pub fn new() -> Self {
76 Self {
77 field_path: None,
78 details: None,
79 source: None,
80 hint: None,
81 request_id: None,
82 status_code: None,
83 error_code: None,
84 retryable: None,
85 fallbackable: None,
86 }
87 }
88
89 pub fn with_field_path(mut self, path: impl Into<String>) -> Self {
90 self.field_path = Some(path.into());
91 self
92 }
93
94 pub fn with_details(mut self, details: impl Into<String>) -> Self {
95 self.details = Some(details.into());
96 self
97 }
98
99 pub fn with_source(mut self, source: impl Into<String>) -> Self {
100 self.source = Some(source.into());
101 self
102 }
103
104 pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
105 self.hint = Some(hint.into());
106 self
107 }
108
109 pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
110 self.request_id = Some(id.into());
111 self
112 }
113
114 pub fn with_status_code(mut self, code: u16) -> Self {
115 self.status_code = Some(code);
116 self
117 }
118
119 pub fn with_error_code(mut self, code: impl Into<String>) -> Self {
120 self.error_code = Some(code.into());
121 self
122 }
123
124 pub fn with_retryable(mut self, retryable: bool) -> Self {
125 self.retryable = Some(retryable);
126 self
127 }
128
129 pub fn with_fallbackable(mut self, fallbackable: bool) -> Self {
130 self.fallbackable = Some(fallbackable);
131 self
132 }
133}
134
135impl Default for ErrorContext {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141#[derive(Debug, Error)]
144pub enum Error {
145 #[error("Protocol specification error: {0}")]
146 Protocol(#[from] ProtocolError),
147
148 #[error("Pipeline processing error: {0}")]
149 Pipeline(#[from] PipelineError),
150
151 #[error("Configuration error: {message}{}", format_context(.context))]
152 Configuration {
153 message: String,
154 context: ErrorContext,
155 },
156
157 #[error("Validation error: {message}{}", format_context(.context))]
158 Validation {
159 message: String,
160 context: ErrorContext,
161 },
162
163 #[error("Runtime error: {message}{}", format_context(.context))]
164 Runtime {
165 message: String,
166 context: ErrorContext,
167 },
168
169 #[error("Network transport error: {0}")]
170 Transport(#[from] crate::transport::TransportError),
171
172 #[error("I/O error: {0}")]
173 Io(#[from] std::io::Error),
174
175 #[error("Serialization error: {0}")]
176 Serialization(#[from] serde_json::Error),
177
178 #[error("Remote error: HTTP {status} ({class}): {message}{}", format_optional_context(.context))]
179 Remote {
180 status: u16,
181 class: String,
182 message: String,
183 retryable: bool,
184 fallbackable: bool,
185 retry_after_ms: Option<u32>,
186 context: Option<ErrorContext>,
187 },
188
189 #[error("Unknown error: {message}{}", format_context(.context))]
190 Unknown {
191 message: String,
192 context: ErrorContext,
193 },
194}
195
196fn format_context(ctx: &ErrorContext) -> String {
198 let mut parts = Vec::new();
199 if let Some(ref field) = ctx.field_path {
200 parts.push(format!("field: {}", field));
201 }
202 if let Some(ref details) = ctx.details {
203 parts.push(format!("details: {}", details));
204 }
205 if let Some(ref source) = ctx.source {
206 parts.push(format!("source: {}", source));
207 }
208 if let Some(ref id) = ctx.request_id {
209 parts.push(format!("request_id: {}", id));
210 }
211 if let Some(code) = ctx.status_code {
212 parts.push(format!("status: {}", code));
213 }
214 if let Some(ref code) = ctx.error_code {
215 parts.push(format!("error_code: {}", code));
216 }
217 if let Some(retryable) = ctx.retryable {
218 parts.push(format!("retryable: {}", retryable));
219 }
220 if let Some(fallbackable) = ctx.fallbackable {
221 parts.push(format!("fallbackable: {}", fallbackable));
222 }
223
224 let ctx_str = if parts.is_empty() {
225 String::new()
226 } else {
227 format!(" [{}]", parts.join(", "))
228 };
229
230 if let Some(ref hint) = ctx.hint {
231 format!("{}\n💡 Hint: {}", ctx_str, hint)
232 } else {
233 ctx_str
234 }
235}
236
237fn format_optional_context(ctx: &Option<ErrorContext>) -> String {
238 ctx.as_ref().map(format_context).unwrap_or_default()
239}
240
241impl Error {
242 pub fn runtime_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
244 Error::Runtime {
245 message: msg.into(),
246 context,
247 }
248 }
249
250 pub fn validation_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
252 Error::Validation {
253 message: msg.into(),
254 context,
255 }
256 }
257
258 pub fn configuration_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
260 Error::Configuration {
261 message: msg.into(),
262 context,
263 }
264 }
265
266 pub fn unknown_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
268 Error::Unknown {
269 message: msg.into(),
270 context,
271 }
272 }
273
274 pub fn validation(msg: impl Into<String>) -> Self {
276 Self::validation_with_context(msg, ErrorContext::new())
277 }
278
279 pub fn configuration(msg: impl Into<String>) -> Self {
281 Self::configuration_with_context(msg, ErrorContext::new())
282 }
283
284 pub fn network_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
286 Error::Runtime {
287 message: format!("Network error: {}", msg.into()),
288 context,
289 }
290 }
291
292 pub fn api_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
294 Error::Runtime {
295 message: format!("API error: {}", msg.into()),
296 context,
297 }
298 }
299
300 pub fn parsing(msg: impl Into<String>) -> Self {
302 Error::Validation {
303 message: format!("Parsing error: {}", msg.into()),
304 context: ErrorContext::new().with_source("parsing"),
305 }
306 }
307
308 pub fn context(&self) -> Option<&ErrorContext> {
310 match self {
311 Error::Configuration { context, .. }
312 | Error::Validation { context, .. }
313 | Error::Runtime { context, .. }
314 | Error::Unknown { context, .. } => Some(context),
315 Error::Remote {
316 context: Some(ref c),
317 ..
318 } => Some(c),
319 _ => None,
320 }
321 }
322
323 pub fn with_context(mut self, new_ctx: ErrorContext) -> Self {
325 match &mut self {
326 Error::Configuration {
327 ref mut context, ..
328 }
329 | Error::Validation {
330 ref mut context, ..
331 }
332 | Error::Runtime {
333 ref mut context, ..
334 }
335 | Error::Unknown {
336 ref mut context, ..
337 } => {
338 *context = new_ctx;
339 }
340 Error::Remote {
341 ref mut context, ..
342 } => {
343 *context = Some(new_ctx);
344 }
345 _ => {}
346 }
347 self
348 }
349}
350
351pub use crate::pipeline::PipelineError as Pipeline;
353pub use crate::protocol::ProtocolError as Protocol;