Skip to main content

alien_bindings/
error.rs

1use alien_error::{AlienError, AlienErrorData, ContextError};
2use serde::{Deserialize, Serialize};
3
4/// Derives the exact `ALIEN_<NAME>_BINDING` environment variable name that would have
5/// configured a binding with the given name — the same derivation `alien-core` uses to
6/// generate binding env vars, so this is guaranteed to match `parse_bindings_from_env`'s
7/// reverse parsing (see `provider.rs`).
8pub fn binding_env_var(binding_name: &str) -> String {
9    alien_core::bindings::binding_env_var_name(binding_name)
10}
11
12/// Errors related to alien-bindings operations.
13#[derive(Debug, Clone, AlienErrorData, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub enum ErrorData {
16    /// Plaintext or context is outside the portable Key binding limits.
17    #[error(
18        code = "KEY_INPUT_INVALID",
19        message = "Key input is invalid: {reason}",
20        retryable = "false",
21        internal = "false",
22        http_status_code = 400
23    )]
24    KeyInputInvalid { reason: String },
25
26    /// Decrypted provider data is not a valid Alien Key frame.
27    #[error(
28        code = "KEY_CIPHERTEXT_INVALID",
29        message = "Key ciphertext is invalid: {reason}",
30        retryable = "false",
31        internal = "false",
32        http_status_code = 400
33    )]
34    KeyCiphertextInvalid { reason: String },
35
36    /// No binding configuration was found for the requested binding name (the
37    /// `ALIEN_<NAME>_BINDING` environment variable was not set).
38    #[error(
39        code = "BINDING_NOT_CONFIGURED",
40        message = "No binding configured for '{binding_name}': environment variable '{env_var}' is not set",
41        retryable = "false",
42        internal = "false",
43        http_status_code = 400
44    )]
45    BindingNotConfigured {
46        /// Name of the binding that was requested
47        binding_name: String,
48        /// The exact environment variable name that would configure this binding
49        env_var: String,
50    },
51
52    /// Binding provider configuration is invalid or missing.
53    #[error(
54        code = "BINDING_CONFIG_INVALID",
55        message = "Binding configuration invalid for binding '{binding_name}' (env var '{env_var}'): {reason}",
56        retryable = "false",
57        internal = "false",
58        http_status_code = 400
59    )]
60    BindingConfigInvalid {
61        /// Name of the binding
62        binding_name: String,
63        /// The exact environment variable name that configures this binding
64        env_var: String,
65        /// Specific reason why the configuration is invalid
66        reason: String,
67    },
68
69    /// Binding configuration named a provider that this build does not support.
70    #[error(
71        code = "UNSUPPORTED_BINDING_PROVIDER",
72        message = "Binding '{binding_name}' (env var '{env_var}') uses unsupported provider '{provider}'",
73        retryable = "false",
74        internal = "false",
75        http_status_code = 501
76    )]
77    UnsupportedBindingProvider {
78        /// Name of the binding
79        binding_name: String,
80        /// The exact environment variable name that configures this binding
81        env_var: String,
82        /// The provider string that is not supported
83        provider: String,
84    },
85
86    /// Storage operation failed due to provider issues.
87    #[error(
88        code = "STORAGE_OPERATION_FAILED",
89        message = "Storage operation failed for binding '{binding_name}': {operation}",
90        retryable = "true",
91        internal = "false",
92        http_status_code = 502
93    )]
94    StorageOperationFailed {
95        /// Name of the storage binding
96        binding_name: String,
97        /// Description of the operation that failed
98        operation: String,
99    },
100
101    /// Storage credentials are missing permission for an operation.
102    #[error(
103        code = "STORAGE_ACCESS_DENIED",
104        message = "Storage access denied for binding '{binding_name}' while attempting to {operation}",
105        retryable = "false",
106        internal = "false",
107        http_status_code = 403,
108        hint = "Check that the customer's Storage connection is active and its Access identity still has the required permissions."
109    )]
110    StorageAccessDenied {
111        /// Name of the storage binding.
112        binding_name: String,
113        /// Provider-independent operation name.
114        operation: String,
115    },
116
117    /// The requested object does not exist.
118    #[error(
119        code = "STORAGE_OBJECT_NOT_FOUND",
120        message = "Storage object not found for binding '{binding_name}' while attempting to {operation}",
121        retryable = "false",
122        internal = "false",
123        http_status_code = 404
124    )]
125    StorageObjectNotFound {
126        /// Name of the storage binding.
127        binding_name: String,
128        /// Provider-independent operation name.
129        operation: String,
130    },
131
132    /// Build operation failed due to provider issues.
133    #[error(
134        code = "BUILD_OPERATION_FAILED",
135        message = "Build operation failed for binding '{binding_name}': {operation}",
136        retryable = "true",
137        internal = "false",
138        http_status_code = 502
139    )]
140    BuildOperationFailed {
141        /// Name of the build binding
142        binding_name: String,
143        /// Description of the operation that failed
144        operation: String,
145    },
146
147    /// Required environment variable is missing or invalid.
148    #[error(
149        code = "ENVIRONMENT_VARIABLE_MISSING",
150        message = "Required environment variable '{variable_name}' is missing",
151        retryable = "false",
152        internal = "false",
153        http_status_code = 500
154    )]
155    EnvironmentVariableMissing {
156        /// Name of the missing environment variable
157        variable_name: String,
158    },
159
160    /// Environment variable has an invalid value.
161    #[error(
162        code = "INVALID_ENVIRONMENT_VARIABLE",
163        message = "Environment variable '{variable_name}' has invalid value '{value}': {reason}",
164        retryable = "false",
165        internal = "false",
166        http_status_code = 500
167    )]
168    InvalidEnvironmentVariable {
169        /// Name of the environment variable
170        variable_name: String,
171        /// The invalid value
172        value: String,
173        /// Reason why the value is invalid
174        reason: String,
175    },
176
177    /// Configuration URL is malformed or invalid.
178    #[error(
179        code = "INVALID_CONFIGURATION_URL",
180        message = "Invalid configuration URL '{url}': {reason}",
181        retryable = "false",
182        internal = "false",
183        http_status_code = 400
184    )]
185    InvalidConfigurationUrl {
186        /// The invalid URL
187        url: String,
188        /// Specific reason why the URL is invalid
189        reason: String,
190    },
191
192    /// Event processing failed due to malformed or unsupported data.
193    #[error(
194        code = "EVENT_PROCESSING_FAILED",
195        message = "Event processing failed for type '{event_type}': {reason}",
196        retryable = "true",
197        internal = "false",
198        http_status_code = 400
199    )]
200    EventProcessingFailed {
201        /// Type of event that failed to process
202        event_type: String,
203        /// Specific reason for the processing failure
204        reason: String,
205    },
206
207    /// gRPC connection failed or became unavailable.
208    #[error(
209        code = "GRPC_CONNECTION_FAILED",
210        message = "gRPC connection failed to endpoint '{endpoint}': {reason}",
211        retryable = "true",
212        internal = "false",
213        http_status_code = 502
214    )]
215    GrpcConnectionFailed {
216        /// The gRPC endpoint that failed to connect
217        endpoint: String,
218        /// Reason for the connection failure
219        reason: String,
220    },
221
222    /// gRPC service unavailable or returned error.
223    #[error(
224        code = "GRPC_SERVICE_UNAVAILABLE",
225        message = "gRPC service '{service}' unavailable at endpoint '{endpoint}': {reason}",
226        retryable = "true",
227        internal = "false",
228        http_status_code = 503
229    )]
230    GrpcServiceUnavailable {
231        /// Name of the gRPC service
232        service: String,
233        /// The gRPC endpoint
234        endpoint: String,
235        /// Reason for service unavailability
236        reason: String,
237    },
238
239    /// gRPC request failed with an error status.
240    #[error(
241        code = "GRPC_REQUEST_FAILED",
242        message = "gRPC request to service '{service}' method '{method}' failed: {details}",
243        retryable = "true",
244        internal = "false",
245        http_status_code = 502
246    )]
247    GrpcRequestFailed {
248        /// Name of the gRPC service
249        service: String,
250        /// Name of the gRPC method
251        method: String,
252        /// Error details from the gRPC status
253        details: String,
254    },
255
256    /// Server failed to bind to the specified address.
257    #[error(
258        code = "SERVER_BIND_FAILED",
259        message = "Failed to bind server to address '{address}': {reason}",
260        retryable = "true",
261        internal = "true",
262        http_status_code = 500
263    )]
264    ServerBindFailed {
265        /// The address that failed to bind
266        address: String,
267        /// Reason for the bind failure
268        reason: String,
269    },
270
271    /// Authentication failed for the configured provider.
272    #[error(
273        code = "AUTHENTICATION_FAILED",
274        message = "Authentication failed for provider '{provider}' and binding '{binding_name}': {reason}",
275        retryable = "true",
276        internal = "false",
277        http_status_code = 401
278    )]
279    AuthenticationFailed {
280        /// Name of the provider (aws, gcp, azure, etc.)
281        provider: String,
282        /// Name of the binding
283        binding_name: String,
284        /// Reason for authentication failure
285        reason: String,
286    },
287
288    /// Operation not supported by the configured provider.
289    #[error(
290        code = "OPERATION_NOT_SUPPORTED",
291        message = "Operation '{operation}' not supported: {reason}",
292        retryable = "false",
293        internal = "false",
294        http_status_code = 501
295    )]
296    OperationNotSupported {
297        /// Name of the unsupported operation
298        operation: String,
299        /// Reason why the operation is not supported
300        reason: String,
301    },
302
303    /// A command run inside a sandbox did not complete.
304    #[error(
305        code = "SANDBOX_COMMAND_FAILED",
306        message = "Sandbox command failed ({failure}): {reason}",
307        retryable = "false",
308        internal = "false",
309        http_status_code = 400
310    )]
311    SandboxCommandFailed {
312        /// The agent's own cause, kept as a field so a caller can branch on it
313        failure: String,
314        /// Human-readable detail from the agent
315        reason: String,
316    },
317
318    /// The sandbox agent could not be reached, or the connection dropped mid-response.
319    ///
320    /// Visibility is inherited rather than declared public: what this wraps is often the cloud
321    /// client's catch-all, which carries the raw request and response text of the call that
322    /// failed. Marking the wrapper external does not redact a source, so a fixed `false` here
323    /// would carry that text out to a caller.
324    #[error(
325        code = "SANDBOX_UNREACHABLE",
326        message = "Sandbox operation '{operation}' could not reach the agent: {reason}",
327        retryable = "true",
328        internal = "inherit",
329        http_status_code = 503
330    )]
331    SandboxUnreachable {
332        /// Operation that was in flight
333        operation: String,
334        /// What went wrong on the wire
335        reason: String,
336    },
337
338    /// Feature is not enabled in the compiled binary.
339    #[error(
340        code = "FEATURE_NOT_ENABLED",
341        message = "Feature '{feature}' is not enabled in this build",
342        retryable = "false",
343        internal = "false",
344        http_status_code = 501
345    )]
346    FeatureNotEnabled {
347        /// Name of the feature that is not enabled
348        feature: String,
349    },
350
351    /// gRPC call failed.
352    #[error(
353        code = "GRPC_CALL_FAILED",
354        message = "gRPC call to service '{service}' method '{method}' failed: {reason}",
355        retryable = "true",
356        internal = "false",
357        http_status_code = 502
358    )]
359    GrpcCallFailed {
360        /// Name of the gRPC service
361        service: String,
362        /// Name of the gRPC method
363        method: String,
364        /// Reason for the call failure
365        reason: String,
366    },
367
368    /// Deserialization of data failed.
369    #[error(
370        code = "DESERIALIZATION_FAILED",
371        message = "Failed to deserialize {type_name}: {message}",
372        retryable = "false",
373        internal = "false",
374        http_status_code = 400
375    )]
376    DeserializationFailed {
377        /// Human-readable error message
378        message: String,
379        /// Name of the type being deserialized
380        type_name: String,
381    },
382
383    /// Serialization of data failed.
384    #[error(
385        code = "SERIALIZATION_FAILED",
386        message = "Failed to serialize data: {message}",
387        retryable = "false",
388        internal = "false",
389        http_status_code = 500
390    )]
391    SerializationFailed {
392        /// Human-readable error message
393        message: String,
394    },
395
396    /// Response format from provider API is unexpected or missing required fields.
397    #[error(
398        code = "UNEXPECTED_RESPONSE_FORMAT",
399        message = "Unexpected response format from '{provider}' for binding '{binding_name}': missing field '{field}'. Response: {response_json}",
400        retryable = "false",
401        internal = "false",
402        http_status_code = 502
403    )]
404    UnexpectedResponseFormat {
405        /// Name of the provider (aws, gcp, azure, etc.)
406        provider: String,
407        /// Name of the binding
408        binding_name: String,
409        /// Name of the missing or malformed field
410        field: String,
411        /// The full response JSON for debugging
412        response_json: String,
413    },
414
415    /// Cloud platform API error.
416    #[error(
417        code = "CLOUD_PLATFORM_ERROR",
418        message = "Cloud platform error: {message}",
419        retryable = "true",
420        internal = "false",
421        http_status_code = 502
422    )]
423    CloudPlatformError {
424        /// Human-readable description of the error
425        message: String,
426        /// Optional resource ID that was involved in the error
427        resource_id: Option<String>,
428    },
429
430    /// Reading a cloud Postgres binding's password from its secret store failed.
431    ///
432    /// The retry and visibility metadata comes from the provider error being wrapped:
433    /// throttling remains retryable, while permanent provider failures do not become
434    /// retryable merely because they happened during secret resolution.
435    ///
436    /// `secret` is the locator, never the secret value.
437    #[error(
438        code = "POSTGRES_SECRET_RESOLUTION_FAILED",
439        message = "Failed to resolve the password for Postgres binding '{binding_name}' from secret '{secret}': {reason}",
440        retryable = "inherit",
441        internal = "inherit",
442        http_status_code = 502
443    )]
444    PostgresSecretResolutionFailed {
445        /// Name of the Postgres binding whose password could not be resolved
446        binding_name: String,
447        /// The secret locator that was read (an ARN / name / URI — never the value)
448        secret: String,
449        /// What went wrong while reading the secret
450        reason: String,
451    },
452
453    /// A cloud Postgres secret was read, but did not contain a usable password.
454    ///
455    /// Provider responses with missing, empty, or malformed values cannot become valid
456    /// by retrying the same version. `secret` is the locator, never the secret value.
457    #[error(
458        code = "POSTGRES_SECRET_VALUE_INVALID",
459        message = "Secret '{secret}' for Postgres binding '{binding_name}' does not contain a valid password: {reason}",
460        retryable = "false",
461        internal = "false",
462        http_status_code = 502
463    )]
464    PostgresSecretValueInvalid {
465        /// Name of the Postgres binding whose password value was invalid
466        binding_name: String,
467        /// The secret locator that was read (an ARN / name / URI — never the value)
468        secret: String,
469        /// Why the returned value cannot be used as a password
470        reason: String,
471    },
472
473    /// Resource not found in the cloud platform.
474    #[error(
475        code = "RESOURCE_NOT_FOUND",
476        message = "Resource '{resource_id}' not found",
477        retryable = "false",
478        internal = "false",
479        http_status_code = 404
480    )]
481    ResourceNotFound {
482        /// ID of the resource that was not found
483        resource_id: String,
484    },
485
486    /// The requested remote resource does not exist.
487    #[error(
488        code = "REMOTE_RESOURCE_NOT_FOUND",
489        message = "{operation_context}: {resource_type} '{resource_name}' not found",
490        retryable = "false",
491        internal = "false",
492        http_status_code = 404
493    )]
494    RemoteResourceNotFound {
495        /// Context of the operation that failed (e.g., "Failed to get ECR repository details")
496        operation_context: String,
497        /// Type of the resource that was not found
498        resource_type: String,
499        /// Name of the resource that was not found
500        resource_name: String,
501    },
502
503    /// Operation conflicts with current remote resource state.
504    #[error(
505        code = "REMOTE_RESOURCE_CONFLICT",
506        message = "{operation_context}: Conflict with {resource_type} '{resource_name}' - {conflict_reason}",
507        retryable = "true",
508        internal = "false",
509        http_status_code = 409
510    )]
511    RemoteResourceConflict {
512        /// Context of the operation that failed
513        operation_context: String,
514        /// Type of the resource that has a conflict
515        resource_type: String,
516        /// Name of the resource that has a conflict
517        resource_name: String,
518        /// Specific reason for the conflict
519        conflict_reason: String,
520    },
521
522    /// Access denied due to insufficient permissions.
523    #[error(
524        code = "REMOTE_ACCESS_DENIED",
525        message = "{operation_context}: Access denied to {resource_type} '{resource_name}'",
526        retryable = "true",
527        internal = "false",
528        http_status_code = 403
529    )]
530    RemoteAccessDenied {
531        /// Context of the operation that failed
532        operation_context: String,
533        /// Type of the resource access was denied to
534        resource_type: String,
535        /// Name of the resource access was denied to
536        resource_name: String,
537    },
538
539    /// Request rate limit exceeded.
540    #[error(
541        code = "RATE_LIMIT_EXCEEDED",
542        message = "{operation_context}: Rate limit exceeded - {details}",
543        retryable = "true",
544        internal = "false",
545        http_status_code = 429
546    )]
547    RateLimitExceeded {
548        /// Context of the operation that failed
549        operation_context: String,
550        /// Additional details about the rate limit
551        details: String,
552    },
553
554    /// Operation exceeded the allowed timeout.
555    #[error(
556        code = "TIMEOUT",
557        message = "{operation_context}: Operation timed out - {details}",
558        retryable = "true",
559        internal = "false",
560        http_status_code = 408
561    )]
562    Timeout {
563        /// Context of the operation that failed
564        operation_context: String,
565        /// Additional details about the timeout
566        details: String,
567    },
568
569    /// Remote service is temporarily unavailable.
570    #[error(
571        code = "REMOTE_SERVICE_UNAVAILABLE",
572        message = "{operation_context}: Service unavailable - {details}",
573        retryable = "true",
574        internal = "false",
575        http_status_code = 503
576    )]
577    RemoteServiceUnavailable {
578        /// Context of the operation that failed
579        operation_context: String,
580        /// Additional details about the service unavailability
581        details: String,
582    },
583
584    /// Quota or resource limits have been exceeded.
585    #[error(
586        code = "QUOTA_EXCEEDED",
587        message = "{operation_context}: Quota exceeded - {details}",
588        retryable = "true",
589        internal = "false",
590        http_status_code = 429
591    )]
592    QuotaExceeded {
593        /// Context of the operation that failed
594        operation_context: String,
595        /// Additional details about the quota violation
596        details: String,
597    },
598
599    /// Invalid or malformed input parameters provided to the operation.
600    #[error(
601        code = "INVALID_INPUT",
602        message = "{operation_context}: Invalid input - {details}",
603        retryable = "false",
604        internal = "false",
605        http_status_code = 400
606    )]
607    InvalidInput {
608        /// Context of the operation that failed
609        operation_context: String,
610        /// Details about what input was invalid
611        details: String,
612        /// Optional field name that was invalid
613        field_name: Option<String>,
614    },
615
616    /// Authentication with cloud provider failed.
617    #[error(
618        code = "AUTHENTICATION_ERROR",
619        message = "{operation_context}: Authentication failed - {details}",
620        retryable = "true",
621        internal = "false",
622        http_status_code = 401
623    )]
624    AuthenticationError {
625        /// Context of the operation that failed
626        operation_context: String,
627        /// Details about the authentication failure
628        details: String,
629    },
630
631    /// Generic bindings error for uncommon cases.
632    #[error(
633        code = "BINDINGS_ERROR",
634        message = "Bindings error: {message}",
635        retryable = "true",
636        internal = "true",
637        http_status_code = 500
638    )]
639    Other {
640        /// Human-readable description of the error
641        message: String,
642    },
643
644    /// Presigned request has expired and can no longer be used.
645    #[error(
646        code = "PRESIGNED_REQUEST_EXPIRED",
647        message = "Presigned request for path '{path}' expired at {expired_at}",
648        retryable = "false",
649        internal = "false",
650        http_status_code = 403
651    )]
652    PresignedRequestExpired {
653        /// Path that the presigned request was for
654        path: String,
655        /// When the request expired
656        expired_at: chrono::DateTime<chrono::Utc>,
657    },
658
659    /// HTTP request to external service failed.
660    #[error(
661        code = "HTTP_REQUEST_FAILED",
662        message = "HTTP {method} request to '{url}' failed",
663        retryable = "true",
664        internal = "false",
665        http_status_code = 502
666    )]
667    HttpRequestFailed {
668        /// URL that was requested
669        url: String,
670        /// HTTP method that was used
671        method: String,
672    },
673
674    /// Local filesystem operation failed.
675    #[error(
676        code = "LOCAL_FILESYSTEM_ERROR",
677        message = "Local filesystem operation '{operation}' failed for path '{path}'",
678        retryable = "true",
679        internal = "false",
680        http_status_code = 500
681    )]
682    LocalFilesystemError {
683        /// Path that the operation was performed on
684        path: String,
685        /// Operation that failed
686        operation: String,
687    },
688
689    /// Failed to load platform configuration for the provider.
690    #[error(
691        code = "client_config_LOAD_FAILED",
692        message = "Failed to load platform configuration for provider '{provider}'",
693        retryable = "false",
694        internal = "false",
695        http_status_code = 400
696    )]
697    ClientConfigLoadFailed {
698        /// Name of the provider (aws, gcp, azure, etc.)
699        provider: String,
700    },
701
702    /// Binding setup failed during initialization.
703    #[error(
704        code = "BINDING_SETUP_FAILED",
705        message = "Binding setup failed for type '{binding_type}': {reason}",
706        retryable = "false",
707        internal = "false",
708        http_status_code = 500
709    )]
710    BindingSetupFailed {
711        /// Type of binding being set up
712        binding_type: String,
713        /// Reason for the setup failure
714        reason: String,
715    },
716
717    /// KV operation failed.
718    #[error(
719        code = "KV_OPERATION_FAILED",
720        message = "KV operation '{operation}' failed for key '{key}': {reason}",
721        retryable = "true",
722        internal = "false",
723        http_status_code = 502
724    )]
725    KvOperationFailed {
726        /// The KV operation that failed
727        operation: String,
728        /// The key involved in the operation
729        key: String,
730        /// Reason for the operation failure
731        reason: String,
732    },
733
734    /// Queue operation failed.
735    #[error(
736        code = "QUEUE_OPERATION_FAILED",
737        message = "Queue operation '{operation}' failed: {reason}",
738        retryable = "true",
739        internal = "false",
740        http_status_code = 502
741    )]
742    QueueOperationFailed {
743        /// The queue operation that failed
744        operation: String,
745        /// Reason for the operation failure
746        reason: String,
747    },
748
749    /// Remote access to deployment resources failed.
750    #[error(
751        code = "REMOTE_ACCESS_FAILED",
752        message = "Remote access failed during operation: {operation}",
753        retryable = "true",
754        internal = "false",
755        http_status_code = 502
756    )]
757    RemoteAccessFailed {
758        /// Description of the operation that failed
759        operation: String,
760    },
761
762    /// Client configuration is invalid or missing for the platform.
763    #[error(
764        code = "CLIENT_CONFIG_INVALID",
765        message = "Client configuration invalid for platform '{platform}': {message}",
766        retryable = "false",
767        internal = "false",
768        http_status_code = 400
769    )]
770    ClientConfigInvalid {
771        /// The platform that was expected
772        platform: alien_core::Platform,
773        /// Description of the configuration issue
774        message: String,
775    },
776}
777
778impl ErrorData {
779    /// Construct a [`ErrorData::BindingConfigInvalid`] for `binding_name`,
780    /// deriving the exact `ALIEN_<NAME>_BINDING` env var name internally so call
781    /// sites cannot forget it (the omission that repeatedly broke bindings).
782    pub fn config_invalid(binding_name: &str, reason: impl Into<String>) -> Self {
783        ErrorData::BindingConfigInvalid {
784            env_var: binding_env_var(binding_name),
785            binding_name: binding_name.to_string(),
786            reason: reason.into(),
787        }
788    }
789
790    /// Construct a [`ErrorData::BindingNotConfigured`] for `binding_name`,
791    /// deriving the exact `ALIEN_<NAME>_BINDING` env var name internally.
792    pub fn not_configured(binding_name: &str) -> Self {
793        ErrorData::BindingNotConfigured {
794            binding_name: binding_name.to_string(),
795            env_var: binding_env_var(binding_name),
796        }
797    }
798}
799
800/// Convenient alias with default error type `ErrorData`.
801pub type Result<T, E = ErrorData> = alien_error::Result<T, E>;
802
803/// Convenience alias representing a constructed AlienError with our `ErrorData` payload.
804pub type Error = AlienError<ErrorData>;
805
806/// Maps an `alien_client_core::Error` to an appropriate `alien_bindings::Error`.
807///
808/// Important error types (like resource not found, access denied, etc.) are mapped
809/// to their corresponding variants in alien-bindings while preserving the operation context.
810/// Less important errors are wrapped in `CloudPlatformError`.
811///
812/// # Arguments
813/// * `cloud_error` - The error from cloud client crates
814/// * `operation_context` - Description of the operation that failed (e.g., "Failed to get ECR repository details")
815/// * `resource_id` - Optional resource ID for fallback error context
816///
817/// # Example
818/// ```rust
819/// use alien_bindings::error::map_cloud_client_error;
820///
821/// async fn example() {
822///     // This would be an actual cloud client operation
823///     let result = some_cloud_operation().await
824///         .map_err(|e| map_cloud_client_error(e, "Failed to get ECR repository details".to_string(), Some("my-repo".to_string())));
825/// }
826///
827/// async fn some_cloud_operation() -> Result<(), alien_client_core::Error> {
828///     // Mock implementation
829///     Ok(())
830/// }
831/// ```
832pub fn map_cloud_client_error(
833    cloud_error: alien_client_core::Error,
834    operation_context: String,
835    resource_id: Option<String>,
836) -> Error {
837    use alien_client_core::ErrorData as CloudErrorData;
838
839    // Check the error type first to determine the right context to add
840    let error_data = match cloud_error.error.as_ref() {
841        Some(CloudErrorData::RemoteResourceNotFound {
842            resource_type,
843            resource_name,
844        }) => ErrorData::RemoteResourceNotFound {
845            operation_context,
846            resource_type: resource_type.clone(),
847            resource_name: resource_name.clone(),
848        },
849        Some(CloudErrorData::RemoteResourceConflict {
850            resource_type,
851            resource_name,
852            message,
853        }) => ErrorData::RemoteResourceConflict {
854            operation_context,
855            resource_type: resource_type.clone(),
856            resource_name: resource_name.clone(),
857            conflict_reason: message.clone(),
858        },
859        Some(CloudErrorData::RemoteAccessDenied {
860            resource_type,
861            resource_name,
862        }) => ErrorData::RemoteAccessDenied {
863            operation_context,
864            resource_type: resource_type.clone(),
865            resource_name: resource_name.clone(),
866        },
867        Some(CloudErrorData::RateLimitExceeded { message }) => ErrorData::RateLimitExceeded {
868            operation_context,
869            details: message.clone(),
870        },
871        Some(CloudErrorData::Timeout { message }) => ErrorData::Timeout {
872            operation_context,
873            details: message.clone(),
874        },
875        Some(CloudErrorData::RemoteServiceUnavailable { message }) => {
876            ErrorData::RemoteServiceUnavailable {
877                operation_context,
878                details: message.clone(),
879            }
880        }
881        Some(CloudErrorData::QuotaExceeded { message }) => ErrorData::QuotaExceeded {
882            operation_context,
883            details: message.clone(),
884        },
885        Some(CloudErrorData::InvalidInput {
886            message,
887            field_name,
888        }) => ErrorData::InvalidInput {
889            operation_context,
890            details: message.clone(),
891            field_name: field_name.clone(),
892        },
893        Some(CloudErrorData::AuthenticationError { message }) => ErrorData::AuthenticationError {
894            operation_context,
895            details: message.clone(),
896        },
897        // For other error types or None, wrap in CloudPlatformError
898        _ => ErrorData::CloudPlatformError {
899            message: operation_context,
900            resource_id,
901        },
902    };
903
904    // Now add the context to the cloud error
905    cloud_error.context(error_data)
906}