1use alien_error::{AlienError, AlienErrorData, ContextError};
2use serde::{Deserialize, Serialize};
3
4pub fn binding_env_var(binding_name: &str) -> String {
9 alien_core::bindings::binding_env_var_name(binding_name)
10}
11
12#[derive(Debug, Clone, AlienErrorData, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub enum ErrorData {
16 #[error(
19 code = "BINDING_NOT_CONFIGURED",
20 message = "No binding configured for '{binding_name}': environment variable '{env_var}' is not set",
21 retryable = "false",
22 internal = "false",
23 http_status_code = 400
24 )]
25 BindingNotConfigured {
26 binding_name: String,
28 env_var: String,
30 },
31
32 #[error(
34 code = "BINDING_CONFIG_INVALID",
35 message = "Binding configuration invalid for binding '{binding_name}' (env var '{env_var}'): {reason}",
36 retryable = "false",
37 internal = "false",
38 http_status_code = 400
39 )]
40 BindingConfigInvalid {
41 binding_name: String,
43 env_var: String,
45 reason: String,
47 },
48
49 #[error(
51 code = "UNSUPPORTED_BINDING_PROVIDER",
52 message = "Binding '{binding_name}' (env var '{env_var}') uses unsupported provider '{provider}'",
53 retryable = "false",
54 internal = "false",
55 http_status_code = 501
56 )]
57 UnsupportedBindingProvider {
58 binding_name: String,
60 env_var: String,
62 provider: String,
64 },
65
66 #[error(
68 code = "STORAGE_OPERATION_FAILED",
69 message = "Storage operation failed for binding '{binding_name}': {operation}",
70 retryable = "true",
71 internal = "false",
72 http_status_code = 502
73 )]
74 StorageOperationFailed {
75 binding_name: String,
77 operation: String,
79 },
80
81 #[error(
83 code = "BUILD_OPERATION_FAILED",
84 message = "Build operation failed for binding '{binding_name}': {operation}",
85 retryable = "true",
86 internal = "false",
87 http_status_code = 502
88 )]
89 BuildOperationFailed {
90 binding_name: String,
92 operation: String,
94 },
95
96 #[error(
98 code = "ENVIRONMENT_VARIABLE_MISSING",
99 message = "Required environment variable '{variable_name}' is missing",
100 retryable = "false",
101 internal = "false",
102 http_status_code = 500
103 )]
104 EnvironmentVariableMissing {
105 variable_name: String,
107 },
108
109 #[error(
111 code = "INVALID_ENVIRONMENT_VARIABLE",
112 message = "Environment variable '{variable_name}' has invalid value '{value}': {reason}",
113 retryable = "false",
114 internal = "false",
115 http_status_code = 500
116 )]
117 InvalidEnvironmentVariable {
118 variable_name: String,
120 value: String,
122 reason: String,
124 },
125
126 #[error(
128 code = "INVALID_CONFIGURATION_URL",
129 message = "Invalid configuration URL '{url}': {reason}",
130 retryable = "false",
131 internal = "false",
132 http_status_code = 400
133 )]
134 InvalidConfigurationUrl {
135 url: String,
137 reason: String,
139 },
140
141 #[error(
143 code = "EVENT_PROCESSING_FAILED",
144 message = "Event processing failed for type '{event_type}': {reason}",
145 retryable = "true",
146 internal = "false",
147 http_status_code = 400
148 )]
149 EventProcessingFailed {
150 event_type: String,
152 reason: String,
154 },
155
156 #[error(
158 code = "GRPC_CONNECTION_FAILED",
159 message = "gRPC connection failed to endpoint '{endpoint}': {reason}",
160 retryable = "true",
161 internal = "false",
162 http_status_code = 502
163 )]
164 GrpcConnectionFailed {
165 endpoint: String,
167 reason: String,
169 },
170
171 #[error(
173 code = "GRPC_SERVICE_UNAVAILABLE",
174 message = "gRPC service '{service}' unavailable at endpoint '{endpoint}': {reason}",
175 retryable = "true",
176 internal = "false",
177 http_status_code = 503
178 )]
179 GrpcServiceUnavailable {
180 service: String,
182 endpoint: String,
184 reason: String,
186 },
187
188 #[error(
190 code = "GRPC_REQUEST_FAILED",
191 message = "gRPC request to service '{service}' method '{method}' failed: {details}",
192 retryable = "true",
193 internal = "false",
194 http_status_code = 502
195 )]
196 GrpcRequestFailed {
197 service: String,
199 method: String,
201 details: String,
203 },
204
205 #[error(
207 code = "SERVER_BIND_FAILED",
208 message = "Failed to bind server to address '{address}': {reason}",
209 retryable = "true",
210 internal = "true",
211 http_status_code = 500
212 )]
213 ServerBindFailed {
214 address: String,
216 reason: String,
218 },
219
220 #[error(
222 code = "AUTHENTICATION_FAILED",
223 message = "Authentication failed for provider '{provider}' and binding '{binding_name}': {reason}",
224 retryable = "true",
225 internal = "false",
226 http_status_code = 401
227 )]
228 AuthenticationFailed {
229 provider: String,
231 binding_name: String,
233 reason: String,
235 },
236
237 #[error(
239 code = "OPERATION_NOT_SUPPORTED",
240 message = "Operation '{operation}' not supported: {reason}",
241 retryable = "false",
242 internal = "false",
243 http_status_code = 501
244 )]
245 OperationNotSupported {
246 operation: String,
248 reason: String,
250 },
251
252 #[error(
254 code = "FEATURE_NOT_ENABLED",
255 message = "Feature '{feature}' is not enabled in this build",
256 retryable = "false",
257 internal = "false",
258 http_status_code = 501
259 )]
260 FeatureNotEnabled {
261 feature: String,
263 },
264
265 #[error(
267 code = "GRPC_CALL_FAILED",
268 message = "gRPC call to service '{service}' method '{method}' failed: {reason}",
269 retryable = "true",
270 internal = "false",
271 http_status_code = 502
272 )]
273 GrpcCallFailed {
274 service: String,
276 method: String,
278 reason: String,
280 },
281
282 #[error(
284 code = "DESERIALIZATION_FAILED",
285 message = "Failed to deserialize {type_name}: {message}",
286 retryable = "false",
287 internal = "false",
288 http_status_code = 400
289 )]
290 DeserializationFailed {
291 message: String,
293 type_name: String,
295 },
296
297 #[error(
299 code = "SERIALIZATION_FAILED",
300 message = "Failed to serialize data: {message}",
301 retryable = "false",
302 internal = "false",
303 http_status_code = 500
304 )]
305 SerializationFailed {
306 message: String,
308 },
309
310 #[error(
312 code = "UNEXPECTED_RESPONSE_FORMAT",
313 message = "Unexpected response format from '{provider}' for binding '{binding_name}': missing field '{field}'. Response: {response_json}",
314 retryable = "false",
315 internal = "false",
316 http_status_code = 502
317 )]
318 UnexpectedResponseFormat {
319 provider: String,
321 binding_name: String,
323 field: String,
325 response_json: String,
327 },
328
329 #[error(
331 code = "CLOUD_PLATFORM_ERROR",
332 message = "Cloud platform error: {message}",
333 retryable = "true",
334 internal = "false",
335 http_status_code = 502
336 )]
337 CloudPlatformError {
338 message: String,
340 resource_id: Option<String>,
342 },
343
344 #[error(
352 code = "POSTGRES_SECRET_RESOLUTION_FAILED",
353 message = "Failed to resolve the password for Postgres binding '{binding_name}' from secret '{secret}': {reason}",
354 retryable = "inherit",
355 internal = "inherit",
356 http_status_code = 502
357 )]
358 PostgresSecretResolutionFailed {
359 binding_name: String,
361 secret: String,
363 reason: String,
365 },
366
367 #[error(
372 code = "POSTGRES_SECRET_VALUE_INVALID",
373 message = "Secret '{secret}' for Postgres binding '{binding_name}' does not contain a valid password: {reason}",
374 retryable = "false",
375 internal = "false",
376 http_status_code = 502
377 )]
378 PostgresSecretValueInvalid {
379 binding_name: String,
381 secret: String,
383 reason: String,
385 },
386
387 #[error(
389 code = "RESOURCE_NOT_FOUND",
390 message = "Resource '{resource_id}' not found",
391 retryable = "false",
392 internal = "false",
393 http_status_code = 404
394 )]
395 ResourceNotFound {
396 resource_id: String,
398 },
399
400 #[error(
402 code = "REMOTE_RESOURCE_NOT_FOUND",
403 message = "{operation_context}: {resource_type} '{resource_name}' not found",
404 retryable = "false",
405 internal = "false",
406 http_status_code = 404
407 )]
408 RemoteResourceNotFound {
409 operation_context: String,
411 resource_type: String,
413 resource_name: String,
415 },
416
417 #[error(
419 code = "REMOTE_RESOURCE_CONFLICT",
420 message = "{operation_context}: Conflict with {resource_type} '{resource_name}' - {conflict_reason}",
421 retryable = "true",
422 internal = "false",
423 http_status_code = 409
424 )]
425 RemoteResourceConflict {
426 operation_context: String,
428 resource_type: String,
430 resource_name: String,
432 conflict_reason: String,
434 },
435
436 #[error(
438 code = "REMOTE_ACCESS_DENIED",
439 message = "{operation_context}: Access denied to {resource_type} '{resource_name}'",
440 retryable = "true",
441 internal = "false",
442 http_status_code = 403
443 )]
444 RemoteAccessDenied {
445 operation_context: String,
447 resource_type: String,
449 resource_name: String,
451 },
452
453 #[error(
455 code = "RATE_LIMIT_EXCEEDED",
456 message = "{operation_context}: Rate limit exceeded - {details}",
457 retryable = "true",
458 internal = "false",
459 http_status_code = 429
460 )]
461 RateLimitExceeded {
462 operation_context: String,
464 details: String,
466 },
467
468 #[error(
470 code = "TIMEOUT",
471 message = "{operation_context}: Operation timed out - {details}",
472 retryable = "true",
473 internal = "false",
474 http_status_code = 408
475 )]
476 Timeout {
477 operation_context: String,
479 details: String,
481 },
482
483 #[error(
485 code = "REMOTE_SERVICE_UNAVAILABLE",
486 message = "{operation_context}: Service unavailable - {details}",
487 retryable = "true",
488 internal = "false",
489 http_status_code = 503
490 )]
491 RemoteServiceUnavailable {
492 operation_context: String,
494 details: String,
496 },
497
498 #[error(
500 code = "QUOTA_EXCEEDED",
501 message = "{operation_context}: Quota exceeded - {details}",
502 retryable = "true",
503 internal = "false",
504 http_status_code = 429
505 )]
506 QuotaExceeded {
507 operation_context: String,
509 details: String,
511 },
512
513 #[error(
515 code = "INVALID_INPUT",
516 message = "{operation_context}: Invalid input - {details}",
517 retryable = "false",
518 internal = "false",
519 http_status_code = 400
520 )]
521 InvalidInput {
522 operation_context: String,
524 details: String,
526 field_name: Option<String>,
528 },
529
530 #[error(
532 code = "AUTHENTICATION_ERROR",
533 message = "{operation_context}: Authentication failed - {details}",
534 retryable = "true",
535 internal = "false",
536 http_status_code = 401
537 )]
538 AuthenticationError {
539 operation_context: String,
541 details: String,
543 },
544
545 #[error(
547 code = "BINDINGS_ERROR",
548 message = "Bindings error: {message}",
549 retryable = "true",
550 internal = "true",
551 http_status_code = 500
552 )]
553 Other {
554 message: String,
556 },
557
558 #[error(
560 code = "PRESIGNED_REQUEST_EXPIRED",
561 message = "Presigned request for path '{path}' expired at {expired_at}",
562 retryable = "false",
563 internal = "false",
564 http_status_code = 403
565 )]
566 PresignedRequestExpired {
567 path: String,
569 expired_at: chrono::DateTime<chrono::Utc>,
571 },
572
573 #[error(
575 code = "HTTP_REQUEST_FAILED",
576 message = "HTTP {method} request to '{url}' failed",
577 retryable = "true",
578 internal = "false",
579 http_status_code = 502
580 )]
581 HttpRequestFailed {
582 url: String,
584 method: String,
586 },
587
588 #[error(
590 code = "LOCAL_FILESYSTEM_ERROR",
591 message = "Local filesystem operation '{operation}' failed for path '{path}'",
592 retryable = "true",
593 internal = "false",
594 http_status_code = 500
595 )]
596 LocalFilesystemError {
597 path: String,
599 operation: String,
601 },
602
603 #[error(
605 code = "client_config_LOAD_FAILED",
606 message = "Failed to load platform configuration for provider '{provider}'",
607 retryable = "false",
608 internal = "false",
609 http_status_code = 400
610 )]
611 ClientConfigLoadFailed {
612 provider: String,
614 },
615
616 #[error(
618 code = "BINDING_SETUP_FAILED",
619 message = "Binding setup failed for type '{binding_type}': {reason}",
620 retryable = "false",
621 internal = "false",
622 http_status_code = 500
623 )]
624 BindingSetupFailed {
625 binding_type: String,
627 reason: String,
629 },
630
631 #[error(
633 code = "KV_OPERATION_FAILED",
634 message = "KV operation '{operation}' failed for key '{key}': {reason}",
635 retryable = "true",
636 internal = "false",
637 http_status_code = 502
638 )]
639 KvOperationFailed {
640 operation: String,
642 key: String,
644 reason: String,
646 },
647
648 #[error(
650 code = "QUEUE_OPERATION_FAILED",
651 message = "Queue operation '{operation}' failed: {reason}",
652 retryable = "true",
653 internal = "false",
654 http_status_code = 502
655 )]
656 QueueOperationFailed {
657 operation: String,
659 reason: String,
661 },
662
663 #[error(
665 code = "REMOTE_ACCESS_FAILED",
666 message = "Remote access failed during operation: {operation}",
667 retryable = "true",
668 internal = "false",
669 http_status_code = 502
670 )]
671 RemoteAccessFailed {
672 operation: String,
674 },
675
676 #[error(
678 code = "CLIENT_CONFIG_INVALID",
679 message = "Client configuration invalid for platform '{platform}': {message}",
680 retryable = "false",
681 internal = "false",
682 http_status_code = 400
683 )]
684 ClientConfigInvalid {
685 platform: alien_core::Platform,
687 message: String,
689 },
690}
691
692impl ErrorData {
693 pub fn config_invalid(binding_name: &str, reason: impl Into<String>) -> Self {
697 ErrorData::BindingConfigInvalid {
698 env_var: binding_env_var(binding_name),
699 binding_name: binding_name.to_string(),
700 reason: reason.into(),
701 }
702 }
703
704 pub fn not_configured(binding_name: &str) -> Self {
707 ErrorData::BindingNotConfigured {
708 binding_name: binding_name.to_string(),
709 env_var: binding_env_var(binding_name),
710 }
711 }
712}
713
714pub type Result<T, E = ErrorData> = alien_error::Result<T, E>;
716
717pub type Error = AlienError<ErrorData>;
719
720pub fn map_cloud_client_error(
747 cloud_error: alien_client_core::Error,
748 operation_context: String,
749 resource_id: Option<String>,
750) -> Error {
751 use alien_client_core::ErrorData as CloudErrorData;
752
753 let error_data = match cloud_error.error.as_ref() {
755 Some(CloudErrorData::RemoteResourceNotFound {
756 resource_type,
757 resource_name,
758 }) => ErrorData::RemoteResourceNotFound {
759 operation_context,
760 resource_type: resource_type.clone(),
761 resource_name: resource_name.clone(),
762 },
763 Some(CloudErrorData::RemoteResourceConflict {
764 resource_type,
765 resource_name,
766 message,
767 }) => ErrorData::RemoteResourceConflict {
768 operation_context,
769 resource_type: resource_type.clone(),
770 resource_name: resource_name.clone(),
771 conflict_reason: message.clone(),
772 },
773 Some(CloudErrorData::RemoteAccessDenied {
774 resource_type,
775 resource_name,
776 }) => ErrorData::RemoteAccessDenied {
777 operation_context,
778 resource_type: resource_type.clone(),
779 resource_name: resource_name.clone(),
780 },
781 Some(CloudErrorData::RateLimitExceeded { message }) => ErrorData::RateLimitExceeded {
782 operation_context,
783 details: message.clone(),
784 },
785 Some(CloudErrorData::Timeout { message }) => ErrorData::Timeout {
786 operation_context,
787 details: message.clone(),
788 },
789 Some(CloudErrorData::RemoteServiceUnavailable { message }) => {
790 ErrorData::RemoteServiceUnavailable {
791 operation_context,
792 details: message.clone(),
793 }
794 }
795 Some(CloudErrorData::QuotaExceeded { message }) => ErrorData::QuotaExceeded {
796 operation_context,
797 details: message.clone(),
798 },
799 Some(CloudErrorData::InvalidInput {
800 message,
801 field_name,
802 }) => ErrorData::InvalidInput {
803 operation_context,
804 details: message.clone(),
805 field_name: field_name.clone(),
806 },
807 Some(CloudErrorData::AuthenticationError { message }) => ErrorData::AuthenticationError {
808 operation_context,
809 details: message.clone(),
810 },
811 _ => ErrorData::CloudPlatformError {
813 message: operation_context,
814 resource_id,
815 },
816 };
817
818 cloud_error.context(error_data)
820}