Skip to main content

alien_bindings/
error.rs

1use alien_error::{AlienError, AlienErrorData, ContextError};
2use serde::{Deserialize, Serialize};
3
4/// Errors related to alien-bindings operations.
5#[derive(Debug, Clone, AlienErrorData, Serialize, Deserialize)]
6#[serde(rename_all = "camelCase")]
7pub enum ErrorData {
8    /// Binding provider configuration is invalid or missing.
9    #[error(
10        code = "BINDING_CONFIG_INVALID",
11        message = "Binding configuration invalid for binding '{binding_name}': {reason}",
12        retryable = "false",
13        internal = "false",
14        http_status_code = 400
15    )]
16    BindingConfigInvalid {
17        /// Name of the binding
18        binding_name: String,
19        /// Specific reason why the configuration is invalid
20        reason: String,
21    },
22
23    /// Storage operation failed due to provider issues.
24    #[error(
25        code = "STORAGE_OPERATION_FAILED",
26        message = "Storage operation failed for binding '{binding_name}': {operation}",
27        retryable = "true",
28        internal = "false",
29        http_status_code = 502
30    )]
31    StorageOperationFailed {
32        /// Name of the storage binding
33        binding_name: String,
34        /// Description of the operation that failed
35        operation: String,
36    },
37
38    /// Build operation failed due to provider issues.
39    #[error(
40        code = "BUILD_OPERATION_FAILED",
41        message = "Build operation failed for binding '{binding_name}': {operation}",
42        retryable = "true",
43        internal = "false",
44        http_status_code = 502
45    )]
46    BuildOperationFailed {
47        /// Name of the build binding
48        binding_name: String,
49        /// Description of the operation that failed
50        operation: String,
51    },
52
53    /// Required environment variable is missing or invalid.
54    #[error(
55        code = "ENVIRONMENT_VARIABLE_MISSING",
56        message = "Required environment variable '{variable_name}' is missing",
57        retryable = "false",
58        internal = "false",
59        http_status_code = 500
60    )]
61    EnvironmentVariableMissing {
62        /// Name of the missing environment variable
63        variable_name: String,
64    },
65
66    /// Environment variable has an invalid value.
67    #[error(
68        code = "INVALID_ENVIRONMENT_VARIABLE",
69        message = "Environment variable '{variable_name}' has invalid value '{value}': {reason}",
70        retryable = "false",
71        internal = "false",
72        http_status_code = 500
73    )]
74    InvalidEnvironmentVariable {
75        /// Name of the environment variable
76        variable_name: String,
77        /// The invalid value
78        value: String,
79        /// Reason why the value is invalid
80        reason: String,
81    },
82
83    /// Configuration URL is malformed or invalid.
84    #[error(
85        code = "INVALID_CONFIGURATION_URL",
86        message = "Invalid configuration URL '{url}': {reason}",
87        retryable = "false",
88        internal = "false",
89        http_status_code = 400
90    )]
91    InvalidConfigurationUrl {
92        /// The invalid URL
93        url: String,
94        /// Specific reason why the URL is invalid
95        reason: String,
96    },
97
98    /// Event processing failed due to malformed or unsupported data.
99    #[error(
100        code = "EVENT_PROCESSING_FAILED",
101        message = "Event processing failed for type '{event_type}': {reason}",
102        retryable = "true",
103        internal = "false",
104        http_status_code = 400
105    )]
106    EventProcessingFailed {
107        /// Type of event that failed to process
108        event_type: String,
109        /// Specific reason for the processing failure
110        reason: String,
111    },
112
113    /// gRPC connection failed or became unavailable.
114    #[error(
115        code = "GRPC_CONNECTION_FAILED",
116        message = "gRPC connection failed to endpoint '{endpoint}': {reason}",
117        retryable = "true",
118        internal = "false",
119        http_status_code = 502
120    )]
121    GrpcConnectionFailed {
122        /// The gRPC endpoint that failed to connect
123        endpoint: String,
124        /// Reason for the connection failure
125        reason: String,
126    },
127
128    /// gRPC service unavailable or returned error.
129    #[error(
130        code = "GRPC_SERVICE_UNAVAILABLE",
131        message = "gRPC service '{service}' unavailable at endpoint '{endpoint}': {reason}",
132        retryable = "true",
133        internal = "false",
134        http_status_code = 503
135    )]
136    GrpcServiceUnavailable {
137        /// Name of the gRPC service
138        service: String,
139        /// The gRPC endpoint
140        endpoint: String,
141        /// Reason for service unavailability
142        reason: String,
143    },
144
145    /// gRPC request failed with an error status.
146    #[error(
147        code = "GRPC_REQUEST_FAILED",
148        message = "gRPC request to service '{service}' method '{method}' failed: {details}",
149        retryable = "true",
150        internal = "false",
151        http_status_code = 502
152    )]
153    GrpcRequestFailed {
154        /// Name of the gRPC service
155        service: String,
156        /// Name of the gRPC method
157        method: String,
158        /// Error details from the gRPC status
159        details: String,
160    },
161
162    /// Server failed to bind to the specified address.
163    #[error(
164        code = "SERVER_BIND_FAILED",
165        message = "Failed to bind server to address '{address}': {reason}",
166        retryable = "true",
167        internal = "true",
168        http_status_code = 500
169    )]
170    ServerBindFailed {
171        /// The address that failed to bind
172        address: String,
173        /// Reason for the bind failure
174        reason: String,
175    },
176
177    /// Authentication failed for the configured provider.
178    #[error(
179        code = "AUTHENTICATION_FAILED",
180        message = "Authentication failed for provider '{provider}' and binding '{binding_name}': {reason}",
181        retryable = "true",
182        internal = "false",
183        http_status_code = 401
184    )]
185    AuthenticationFailed {
186        /// Name of the provider (aws, gcp, azure, etc.)
187        provider: String,
188        /// Name of the binding
189        binding_name: String,
190        /// Reason for authentication failure
191        reason: String,
192    },
193
194    /// Operation not supported by the configured provider.
195    #[error(
196        code = "OPERATION_NOT_SUPPORTED",
197        message = "Operation '{operation}' not supported: {reason}",
198        retryable = "false",
199        internal = "false",
200        http_status_code = 501
201    )]
202    OperationNotSupported {
203        /// Name of the unsupported operation
204        operation: String,
205        /// Reason why the operation is not supported
206        reason: String,
207    },
208
209    /// Feature is not enabled in the compiled binary.
210    #[error(
211        code = "FEATURE_NOT_ENABLED",
212        message = "Feature '{feature}' is not enabled in this build",
213        retryable = "false",
214        internal = "false",
215        http_status_code = 501
216    )]
217    FeatureNotEnabled {
218        /// Name of the feature that is not enabled
219        feature: String,
220    },
221
222    /// gRPC call failed.
223    #[error(
224        code = "GRPC_CALL_FAILED",
225        message = "gRPC call to service '{service}' method '{method}' failed: {reason}",
226        retryable = "true",
227        internal = "false",
228        http_status_code = 502
229    )]
230    GrpcCallFailed {
231        /// Name of the gRPC service
232        service: String,
233        /// Name of the gRPC method
234        method: String,
235        /// Reason for the call failure
236        reason: String,
237    },
238
239    /// Deserialization of data failed.
240    #[error(
241        code = "DESERIALIZATION_FAILED",
242        message = "Failed to deserialize {type_name}: {message}",
243        retryable = "false",
244        internal = "false",
245        http_status_code = 400
246    )]
247    DeserializationFailed {
248        /// Human-readable error message
249        message: String,
250        /// Name of the type being deserialized
251        type_name: String,
252    },
253
254    /// Serialization of data failed.
255    #[error(
256        code = "SERIALIZATION_FAILED",
257        message = "Failed to serialize data: {message}",
258        retryable = "false",
259        internal = "false",
260        http_status_code = 500
261    )]
262    SerializationFailed {
263        /// Human-readable error message
264        message: String,
265    },
266
267    /// Operation is not implemented yet.
268    #[error(
269        code = "NOT_IMPLEMENTED",
270        message = "Operation '{operation}' is not implemented: {reason}",
271        retryable = "false",
272        internal = "false",
273        http_status_code = 501
274    )]
275    NotImplemented {
276        /// Name of the operation that is not implemented
277        operation: String,
278        /// Reason why the operation is not implemented
279        reason: String,
280    },
281
282    /// Response format from provider API is unexpected or missing required fields.
283    #[error(
284        code = "UNEXPECTED_RESPONSE_FORMAT",
285        message = "Unexpected response format from '{provider}' for binding '{binding_name}': missing field '{field}'. Response: {response_json}",
286        retryable = "false",
287        internal = "false",
288        http_status_code = 502
289    )]
290    UnexpectedResponseFormat {
291        /// Name of the provider (aws, gcp, azure, etc.)
292        provider: String,
293        /// Name of the binding
294        binding_name: String,
295        /// Name of the missing or malformed field
296        field: String,
297        /// The full response JSON for debugging
298        response_json: String,
299    },
300
301    /// Cloud platform API error.
302    #[error(
303        code = "CLOUD_PLATFORM_ERROR",
304        message = "Cloud platform error: {message}",
305        retryable = "true",
306        internal = "false",
307        http_status_code = 502
308    )]
309    CloudPlatformError {
310        /// Human-readable description of the error
311        message: String,
312        /// Optional resource ID that was involved in the error
313        resource_id: Option<String>,
314    },
315
316    /// Resource not found in the cloud platform.
317    #[error(
318        code = "RESOURCE_NOT_FOUND",
319        message = "Resource '{resource_id}' not found",
320        retryable = "false",
321        internal = "false",
322        http_status_code = 404
323    )]
324    ResourceNotFound {
325        /// ID of the resource that was not found
326        resource_id: String,
327    },
328
329    /// The requested remote resource does not exist.
330    #[error(
331        code = "REMOTE_RESOURCE_NOT_FOUND",
332        message = "{operation_context}: {resource_type} '{resource_name}' not found",
333        retryable = "false",
334        internal = "false",
335        http_status_code = 404
336    )]
337    RemoteResourceNotFound {
338        /// Context of the operation that failed (e.g., "Failed to get ECR repository details")
339        operation_context: String,
340        /// Type of the resource that was not found
341        resource_type: String,
342        /// Name of the resource that was not found
343        resource_name: String,
344    },
345
346    /// Operation conflicts with current remote resource state.
347    #[error(
348        code = "REMOTE_RESOURCE_CONFLICT",
349        message = "{operation_context}: Conflict with {resource_type} '{resource_name}' - {conflict_reason}",
350        retryable = "true",
351        internal = "false",
352        http_status_code = 409
353    )]
354    RemoteResourceConflict {
355        /// Context of the operation that failed
356        operation_context: String,
357        /// Type of the resource that has a conflict
358        resource_type: String,
359        /// Name of the resource that has a conflict
360        resource_name: String,
361        /// Specific reason for the conflict
362        conflict_reason: String,
363    },
364
365    /// Access denied due to insufficient permissions.
366    #[error(
367        code = "REMOTE_ACCESS_DENIED",
368        message = "{operation_context}: Access denied to {resource_type} '{resource_name}'",
369        retryable = "true",
370        internal = "false",
371        http_status_code = 403
372    )]
373    RemoteAccessDenied {
374        /// Context of the operation that failed
375        operation_context: String,
376        /// Type of the resource access was denied to
377        resource_type: String,
378        /// Name of the resource access was denied to
379        resource_name: String,
380    },
381
382    /// Request rate limit exceeded.
383    #[error(
384        code = "RATE_LIMIT_EXCEEDED",
385        message = "{operation_context}: Rate limit exceeded - {details}",
386        retryable = "true",
387        internal = "false",
388        http_status_code = 429
389    )]
390    RateLimitExceeded {
391        /// Context of the operation that failed
392        operation_context: String,
393        /// Additional details about the rate limit
394        details: String,
395    },
396
397    /// Operation exceeded the allowed timeout.
398    #[error(
399        code = "TIMEOUT",
400        message = "{operation_context}: Operation timed out - {details}",
401        retryable = "true",
402        internal = "false",
403        http_status_code = 408
404    )]
405    Timeout {
406        /// Context of the operation that failed
407        operation_context: String,
408        /// Additional details about the timeout
409        details: String,
410    },
411
412    /// Remote service is temporarily unavailable.
413    #[error(
414        code = "REMOTE_SERVICE_UNAVAILABLE",
415        message = "{operation_context}: Service unavailable - {details}",
416        retryable = "true",
417        internal = "false",
418        http_status_code = 503
419    )]
420    RemoteServiceUnavailable {
421        /// Context of the operation that failed
422        operation_context: String,
423        /// Additional details about the service unavailability
424        details: String,
425    },
426
427    /// Quota or resource limits have been exceeded.
428    #[error(
429        code = "QUOTA_EXCEEDED",
430        message = "{operation_context}: Quota exceeded - {details}",
431        retryable = "true",
432        internal = "false",
433        http_status_code = 429
434    )]
435    QuotaExceeded {
436        /// Context of the operation that failed
437        operation_context: String,
438        /// Additional details about the quota violation
439        details: String,
440    },
441
442    /// Invalid or malformed input parameters provided to the operation.
443    #[error(
444        code = "INVALID_INPUT",
445        message = "{operation_context}: Invalid input - {details}",
446        retryable = "false",
447        internal = "false",
448        http_status_code = 400
449    )]
450    InvalidInput {
451        /// Context of the operation that failed
452        operation_context: String,
453        /// Details about what input was invalid
454        details: String,
455        /// Optional field name that was invalid
456        field_name: Option<String>,
457    },
458
459    /// Authentication with cloud provider failed.
460    #[error(
461        code = "AUTHENTICATION_ERROR",
462        message = "{operation_context}: Authentication failed - {details}",
463        retryable = "true",
464        internal = "false",
465        http_status_code = 401
466    )]
467    AuthenticationError {
468        /// Context of the operation that failed
469        operation_context: String,
470        /// Details about the authentication failure
471        details: String,
472    },
473
474    /// Generic bindings error for uncommon cases.
475    #[error(
476        code = "BINDINGS_ERROR",
477        message = "Bindings error: {message}",
478        retryable = "true",
479        internal = "true",
480        http_status_code = 500
481    )]
482    Other {
483        /// Human-readable description of the error
484        message: String,
485    },
486
487    /// Presigned request has expired and can no longer be used.
488    #[error(
489        code = "PRESIGNED_REQUEST_EXPIRED",
490        message = "Presigned request for path '{path}' expired at {expired_at}",
491        retryable = "false",
492        internal = "false",
493        http_status_code = 403
494    )]
495    PresignedRequestExpired {
496        /// Path that the presigned request was for
497        path: String,
498        /// When the request expired
499        expired_at: chrono::DateTime<chrono::Utc>,
500    },
501
502    /// HTTP request to external service failed.
503    #[error(
504        code = "HTTP_REQUEST_FAILED",
505        message = "HTTP {method} request to '{url}' failed",
506        retryable = "true",
507        internal = "false",
508        http_status_code = 502
509    )]
510    HttpRequestFailed {
511        /// URL that was requested
512        url: String,
513        /// HTTP method that was used
514        method: String,
515    },
516
517    /// Local filesystem operation failed.
518    #[error(
519        code = "LOCAL_FILESYSTEM_ERROR",
520        message = "Local filesystem operation '{operation}' failed for path '{path}'",
521        retryable = "true",
522        internal = "false",
523        http_status_code = 500
524    )]
525    LocalFilesystemError {
526        /// Path that the operation was performed on
527        path: String,
528        /// Operation that failed
529        operation: String,
530    },
531
532    /// Failed to load platform configuration for the provider.
533    #[error(
534        code = "client_config_LOAD_FAILED",
535        message = "Failed to load platform configuration for provider '{provider}'",
536        retryable = "false",
537        internal = "false",
538        http_status_code = 400
539    )]
540    ClientConfigLoadFailed {
541        /// Name of the provider (aws, gcp, azure, etc.)
542        provider: String,
543    },
544
545    /// Binding setup failed during initialization.
546    #[error(
547        code = "BINDING_SETUP_FAILED",
548        message = "Binding setup failed for type '{binding_type}': {reason}",
549        retryable = "false",
550        internal = "false",
551        http_status_code = 500
552    )]
553    BindingSetupFailed {
554        /// Type of binding being set up
555        binding_type: String,
556        /// Reason for the setup failure
557        reason: String,
558    },
559
560    /// KV operation failed.
561    #[error(
562        code = "KV_OPERATION_FAILED",
563        message = "KV operation '{operation}' failed for key '{key}': {reason}",
564        retryable = "true",
565        internal = "false",
566        http_status_code = 502
567    )]
568    KvOperationFailed {
569        /// The KV operation that failed
570        operation: String,
571        /// The key involved in the operation
572        key: String,
573        /// Reason for the operation failure
574        reason: String,
575    },
576
577    /// Remote access to deployment resources failed.
578    #[error(
579        code = "REMOTE_ACCESS_FAILED",
580        message = "Remote access failed during operation: {operation}",
581        retryable = "true",
582        internal = "false",
583        http_status_code = 502
584    )]
585    RemoteAccessFailed {
586        /// Description of the operation that failed
587        operation: String,
588    },
589
590    /// Client configuration is invalid or missing for the platform.
591    #[error(
592        code = "CLIENT_CONFIG_INVALID",
593        message = "Client configuration invalid for platform '{platform}': {message}",
594        retryable = "false",
595        internal = "false",
596        http_status_code = 400
597    )]
598    ClientConfigInvalid {
599        /// The platform that was expected
600        platform: alien_core::Platform,
601        /// Description of the configuration issue
602        message: String,
603    },
604}
605
606/// Convenient alias with default error type `ErrorData`.
607pub type Result<T, E = ErrorData> = alien_error::Result<T, E>;
608
609/// Convenience alias representing a constructed AlienError with our `ErrorData` payload.
610pub type Error = AlienError<ErrorData>;
611
612/// Maps an `alien_client_core::Error` to an appropriate `alien_bindings::Error`.
613///
614/// Important error types (like resource not found, access denied, etc.) are mapped
615/// to their corresponding variants in alien-bindings while preserving the operation context.
616/// Less important errors are wrapped in `CloudPlatformError`.
617///
618/// # Arguments
619/// * `cloud_error` - The error from cloud client crates
620/// * `operation_context` - Description of the operation that failed (e.g., "Failed to get ECR repository details")
621/// * `resource_id` - Optional resource ID for fallback error context
622///
623/// # Example
624/// ```rust
625/// use alien_bindings::error::map_cloud_client_error;
626///
627/// async fn example() {
628///     // This would be an actual cloud client operation
629///     let result = some_cloud_operation().await
630///         .map_err(|e| map_cloud_client_error(e, "Failed to get ECR repository details".to_string(), Some("my-repo".to_string())));
631/// }
632///
633/// async fn some_cloud_operation() -> Result<(), alien_client_core::Error> {
634///     // Mock implementation
635///     Ok(())
636/// }
637/// ```
638pub fn map_cloud_client_error(
639    cloud_error: alien_client_core::Error,
640    operation_context: String,
641    resource_id: Option<String>,
642) -> Error {
643    use alien_client_core::ErrorData as CloudErrorData;
644
645    // Check the error type first to determine the right context to add
646    let error_data = match cloud_error.error.as_ref() {
647        Some(CloudErrorData::RemoteResourceNotFound {
648            resource_type,
649            resource_name,
650        }) => ErrorData::RemoteResourceNotFound {
651            operation_context,
652            resource_type: resource_type.clone(),
653            resource_name: resource_name.clone(),
654        },
655        Some(CloudErrorData::RemoteResourceConflict {
656            resource_type,
657            resource_name,
658            message,
659        }) => ErrorData::RemoteResourceConflict {
660            operation_context,
661            resource_type: resource_type.clone(),
662            resource_name: resource_name.clone(),
663            conflict_reason: message.clone(),
664        },
665        Some(CloudErrorData::RemoteAccessDenied {
666            resource_type,
667            resource_name,
668        }) => ErrorData::RemoteAccessDenied {
669            operation_context,
670            resource_type: resource_type.clone(),
671            resource_name: resource_name.clone(),
672        },
673        Some(CloudErrorData::RateLimitExceeded { message }) => ErrorData::RateLimitExceeded {
674            operation_context,
675            details: message.clone(),
676        },
677        Some(CloudErrorData::Timeout { message }) => ErrorData::Timeout {
678            operation_context,
679            details: message.clone(),
680        },
681        Some(CloudErrorData::RemoteServiceUnavailable { message }) => {
682            ErrorData::RemoteServiceUnavailable {
683                operation_context,
684                details: message.clone(),
685            }
686        }
687        Some(CloudErrorData::QuotaExceeded { message }) => ErrorData::QuotaExceeded {
688            operation_context,
689            details: message.clone(),
690        },
691        Some(CloudErrorData::InvalidInput {
692            message,
693            field_name,
694        }) => ErrorData::InvalidInput {
695            operation_context,
696            details: message.clone(),
697            field_name: field_name.clone(),
698        },
699        Some(CloudErrorData::AuthenticationError { message }) => ErrorData::AuthenticationError {
700            operation_context,
701            details: message.clone(),
702        },
703        // For other error types or None, wrap in CloudPlatformError
704        _ => ErrorData::CloudPlatformError {
705            message: operation_context,
706            resource_id,
707        },
708    };
709
710    // Now add the context to the cloud error
711    cloud_error.context(error_data)
712}