1use std::fmt;
7
8pub type GroveResult<T> = Result<T, GroveError>;
10
11#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13pub enum GroveError {
14 ExtensionNotFound {
16 extension_id:String,
18
19 message:Option<String>,
21 },
22
23 ExtensionLoadFailed {
25 extension_id:String,
27
28 reason:String,
30
31 path:Option<String>,
33 },
34
35 ActivationFailed {
37 extension_id:String,
39
40 reason:String,
42 },
43
44 DeactivationFailed {
46 extension_id:String,
48
49 reason:String,
51 },
52
53 WASMRuntimeError {
55 reason:String,
57
58 module_id:Option<String>,
60 },
61
62 WASMCompilationFailed {
64 reason:String,
66
67 module_path:Option<String>,
69 },
70
71 WASMModuleNotFound {
73 module_id:String,
75 },
76
77 TransportError {
79 transport_type:String,
81
82 reason:String,
84 },
85
86 ConnectionError {
88 endpoint:String,
90
91 reason:String,
93 },
94
95 APIError {
97 api_method:String,
99
100 reason:String,
102
103 error_code:Option<i32>,
105 },
106
107 ConfigurationError {
109 key:String,
111
112 reason:String,
114 },
115
116 IoError {
118 path:Option<String>,
120
121 operation:String,
123
124 reason:String,
126 },
127
128 SerializationError {
130 type_name:String,
132
133 reason:String,
135 },
136
137 DeserializationError {
139 type_name:String,
141
142 reason:String,
144 },
145
146 Timeout {
148 operation:String,
150
151 timeout_ms:u64,
153 },
154
155 InvalidArgument {
157 argument_name:String,
159
160 reason:String,
162 },
163
164 NotImplemented {
166 feature:String,
168 },
169
170 PermissionDenied {
172 resource:String,
174
175 reason:String,
177 },
178
179 ResourceExhausted {
181 resource:String,
183
184 reason:String,
186 },
187
188 InternalError {
190 reason:String,
192
193 #[serde(skip)]
195 backtrace:Option<String>,
196 },
197}
198
199impl GroveError {
200 pub fn extension_not_found(extension_id:impl Into<String>) -> Self {
202 Self::ExtensionNotFound { extension_id:extension_id.into(), message:None }
203 }
204
205 pub fn extension_load_failed(extension_id:impl Into<String>, reason:impl Into<String>) -> Self {
207 Self::ExtensionLoadFailed { extension_id:extension_id.into(), reason:reason.into(), path:None }
208 }
209
210 pub fn activation_failed(extension_id:impl Into<String>, reason:impl Into<String>) -> Self {
212 Self::ActivationFailed { extension_id:extension_id.into(), reason:reason.into() }
213 }
214
215 pub fn wasm_runtime_error(reason:impl Into<String>) -> Self {
217 Self::WASMRuntimeError { reason:reason.into(), module_id:None }
218 }
219
220 pub fn transport_error(transport_type:impl Into<String>, reason:impl Into<String>) -> Self {
222 Self::TransportError { transport_type:transport_type.into(), reason:reason.into() }
223 }
224
225 pub fn connection_error(endpoint:impl Into<String>, reason:impl Into<String>) -> Self {
227 Self::ConnectionError { endpoint:endpoint.into(), reason:reason.into() }
228 }
229
230 pub fn api_error(api_method:impl Into<String>, reason:impl Into<String>) -> Self {
232 Self::APIError { api_method:api_method.into(), reason:reason.into(), error_code:None }
233 }
234
235 pub fn timeout(operation:impl Into<String>, timeout_ms:u64) -> Self {
237 Self::Timeout { operation:operation.into(), timeout_ms }
238 }
239
240 pub fn invalid_argument(argument_name:impl Into<String>, reason:impl Into<String>) -> Self {
242 Self::InvalidArgument { argument_name:argument_name.into(), reason:reason.into() }
243 }
244
245 pub fn not_implemented(feature:impl Into<String>) -> Self { Self::NotImplemented { feature:feature.into() } }
247
248 pub fn error_code(&self) -> &'static str {
250 match self {
251 Self::ExtensionNotFound { .. } => "EXT_NOT_FOUND",
252
253 Self::ExtensionLoadFailed { .. } => "EXT_LOAD_FAILED",
254
255 Self::ActivationFailed { .. } => "ACTIVATION_FAILED",
256
257 Self::DeactivationFailed { .. } => "DEACTIVATION_FAILED",
258
259 Self::WASMRuntimeError { .. } => "WASM_RUNTIME_ERROR",
260
261 Self::WASMCompilationFailed { .. } => "WASM_COMPILATION_FAILED",
262
263 Self::WASMModuleNotFound { .. } => "WASM_MODULE_NOT_FOUND",
264
265 Self::TransportError { .. } => "TRANSPORT_ERROR",
266
267 Self::ConnectionError { .. } => "CONNECTION_ERROR",
268
269 Self::APIError { .. } => "API_ERROR",
270
271 Self::ConfigurationError { .. } => "CONFIGURATION_ERROR",
272
273 Self::IoError { .. } => "IO_ERROR",
274
275 Self::SerializationError { .. } => "SERIALIZATION_ERROR",
276
277 Self::DeserializationError { .. } => "DESERIALIZATION_ERROR",
278
279 Self::Timeout { .. } => "TIMEOUT",
280
281 Self::InvalidArgument { .. } => "INVALID_ARGUMENT",
282
283 Self::NotImplemented { .. } => "NOT_IMPLEMENTED",
284
285 Self::PermissionDenied { .. } => "PERMISSION_DENIED",
286
287 Self::ResourceExhausted { .. } => "RESOURCE_EXHAUSTED",
288
289 Self::InternalError { .. } => "INTERNAL_ERROR",
290 }
291 }
292
293 pub fn is_recoverable(&self) -> bool {
295 matches!(
296 self,
297 Self::Timeout { .. }
298 | Self::TransportError { .. }
299 | Self::ConnectionError { .. }
300 | Self::ResourceExhausted { .. }
301 )
302 }
303
304 pub fn is_transient(&self) -> bool {
306 matches!(
307 self,
308 Self::Timeout { .. } | Self::TransportError { .. } | Self::ConnectionError { .. }
309 )
310 }
311}
312
313impl fmt::Display for GroveError {
314 fn fmt(&self, f:&mut fmt::Formatter<'_>) -> fmt::Result {
315 match self {
316 Self::ExtensionNotFound { extension_id, message } => {
317 if let Some(msg) = message {
318 write!(f, "Extension not found: {} - {}", extension_id, msg)
319 } else {
320 write!(f, "Extension not found: {}", extension_id)
321 }
322 },
323
324 Self::ExtensionLoadFailed { extension_id, reason, path } => {
325 if let Some(path) = path {
326 write!(f, "Failed to load extension #{:?}: {} - {}", path, extension_id, reason)
327 } else {
328 write!(f, "Failed to load extension {}: {}", extension_id, reason)
329 }
330 },
331
332 Self::ActivationFailed { extension_id, reason } => {
333 write!(f, "Activation failed for extension {}: {}", extension_id, reason)
334 },
335
336 Self::DeactivationFailed { extension_id, reason } => {
337 write!(f, "Deactivation failed for extension {}: {}", extension_id, reason)
338 },
339
340 Self::WASMRuntimeError { reason, module_id } => {
341 if let Some(id) = module_id {
342 write!(f, "WASM runtime error for module {}: {}", id, reason)
343 } else {
344 write!(f, "WASM runtime error: {}", reason)
345 }
346 },
347
348 Self::WASMCompilationFailed { reason, module_path } => {
349 if let Some(path) = module_path {
350 write!(f, "WASM compilation failed for {:?}: {}", path, reason)
351 } else {
352 write!(f, "WASM compilation failed: {}", reason)
353 }
354 },
355
356 Self::WASMModuleNotFound { module_id } => {
357 write!(f, "WASM module not found: {}", module_id)
358 },
359
360 Self::TransportError { transport_type, reason } => {
361 write!(f, "Transport error ({:?}): {}", transport_type, reason)
362 },
363
364 Self::ConnectionError { endpoint, reason } => {
365 write!(f, "Connection error to {}: {}", endpoint, reason)
366 },
367
368 Self::APIError { api_method, reason, .. } => {
369 write!(f, "API error for {}: {}", api_method, reason)
370 },
371
372 Self::ConfigurationError { key, reason } => {
373 write!(f, "Configuration error for '{}': {}", key, reason)
374 },
375
376 Self::IoError { operation, reason, .. } => {
377 write!(f, "I/O error for operation '{}': {}", operation, reason)
378 },
379
380 Self::SerializationError { type_name, reason } => {
381 write!(f, "Serialization error for type '{}': {}", type_name, reason)
382 },
383
384 Self::DeserializationError { type_name, reason } => {
385 write!(f, "Deserialization error for type '{}': {}", type_name, reason)
386 },
387
388 Self::Timeout { operation, timeout_ms } => {
389 write!(f, "Timeout after {}ms for operation: {}", timeout_ms, operation)
390 },
391
392 Self::InvalidArgument { argument_name, reason } => {
393 write!(f, "Invalid argument '{}': {}", argument_name, reason)
394 },
395
396 Self::NotImplemented { feature } => {
397 write!(f, "Feature not implemented: {}", feature)
398 },
399
400 Self::PermissionDenied { resource, reason } => {
401 write!(f, "Permission denied for '{}': {}", resource, reason)
402 },
403
404 Self::ResourceExhausted { resource, reason } => {
405 write!(f, "Resource exhausted '{}': {}", resource, reason)
406 },
407
408 Self::InternalError { reason, .. } => {
409 write!(f, "Internal error: {}", reason)
410 },
411 }
412 }
413}
414
415impl std::error::Error for GroveError {}
416
417impl From<std::io::Error> for GroveError {
419 fn from(err:std::io::Error) -> Self {
420 Self::IoError { path:None, operation:"unknown".to_string(), reason:err.to_string() }
421 }
422}
423
424impl From<serde_json::Error> for GroveError {
426 fn from(err:serde_json::Error) -> Self {
427 if err.is_io() {
428 Self::IoError { path:None, operation:"serde_json".to_string(), reason:err.to_string() }
429 } else {
430 Self::DeserializationError { type_name:"unknown".to_string(), reason:err.to_string() }
431 }
432 }
433}
434
435pub trait ResultExt<T> {
437 fn map_grove_error(self, context:impl Into<String>) -> GroveResult<T>;
439}
440
441impl<T, E> ResultExt<T> for Result<T, E>
442where
443 E: std::error::Error + Send + Sync + 'static,
444{
445 fn map_grove_error(self, context:impl Into<String>) -> GroveResult<T> {
446 self.map_err(|e| {
447 GroveError::InternalError {
448 reason:format!("{}: {}", context.into(), e),
449 backtrace:std::backtrace::Backtrace::capture().to_string().into(),
450 }
451 })
452 }
453}
454
455#[cfg(test)]
456mod tests {
457
458 use super::*;
459
460 #[test]
461 fn test_error_creation() {
462 let err = GroveError::extension_not_found("test.ext");
463
464 assert_eq!(err.error_code(), "EXT_NOT_FOUND");
465 }
466
467 #[test]
468 fn test_error_display() {
469 let err = GroveError::activation_failed("test.ext", "timeout");
470
471 assert!(err.to_string().contains("test.ext"));
472
473 assert!(err.to_string().contains("timeout"));
474 }
475
476 #[test]
477 fn test_error_retryable() {
478 let timeout = GroveError::timeout("test", 5000);
479
480 assert!(timeout.is_transient());
481
482 assert!(timeout.is_recoverable());
483
484 let not_found = GroveError::extension_not_found("test.ext");
485
486 assert!(!not_found.is_transient());
487
488 assert!(!not_found.is_recoverable());
489 }
490
491 #[test]
492 fn test_io_error_conversion() {
493 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
494
495 let grove_err = GroveError::from(io_err);
496
497 assert_eq!(grove_err.error_code(), "IO_ERROR");
498 }
499}