Skip to main content

rustack_dynamodb_model/
error.rs

1//! DynamoDB error types.
2//!
3//! DynamoDB errors use JSON format with a `__type` field containing the
4//! fully-qualified error type name.
5
6use std::{collections::HashMap, fmt};
7
8use crate::{attribute_value::AttributeValue, types::CancellationReason};
9
10/// Well-known DynamoDB error codes.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
12#[non_exhaustive]
13pub enum DynamoDBErrorCode {
14    /// Table already exists.
15    ResourceInUseException,
16    /// Table not found.
17    ResourceNotFoundException,
18    /// Condition check failed.
19    ConditionalCheckFailedException,
20    /// Transaction canceled.
21    TransactionCanceledException,
22    /// Transaction conflict.
23    TransactionConflictException,
24    /// Transaction in progress.
25    TransactionInProgressException,
26    /// Idempotent parameter mismatch.
27    IdempotentParameterMismatchException,
28    /// Item collection size limit exceeded.
29    ItemCollectionSizeLimitExceededException,
30    /// Provisioned throughput exceeded.
31    ProvisionedThroughputExceededException,
32    /// Request limit exceeded.
33    RequestLimitExceeded,
34    /// Validation error.
35    #[default]
36    ValidationException,
37    /// Serialization error.
38    SerializationException,
39    /// Internal server error.
40    InternalServerError,
41    /// Missing action.
42    MissingAction,
43    /// Access denied.
44    AccessDeniedException,
45    /// Unknown operation.
46    UnrecognizedClientException,
47}
48
49impl DynamoDBErrorCode {
50    /// Returns the fully-qualified error type string for JSON `__type` field.
51    #[must_use]
52    pub fn error_type(&self) -> &'static str {
53        match self {
54            Self::ResourceInUseException => {
55                "com.amazonaws.dynamodb.v20120810#ResourceInUseException"
56            }
57            Self::ResourceNotFoundException => {
58                "com.amazonaws.dynamodb.v20120810#ResourceNotFoundException"
59            }
60            Self::ConditionalCheckFailedException => {
61                "com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException"
62            }
63            Self::TransactionCanceledException => {
64                "com.amazonaws.dynamodb.v20120810#TransactionCanceledException"
65            }
66            Self::TransactionConflictException => {
67                "com.amazonaws.dynamodb.v20120810#TransactionConflictException"
68            }
69            Self::TransactionInProgressException => {
70                "com.amazonaws.dynamodb.v20120810#TransactionInProgressException"
71            }
72            Self::IdempotentParameterMismatchException => {
73                "com.amazonaws.dynamodb.v20120810#IdempotentParameterMismatchException"
74            }
75            Self::ItemCollectionSizeLimitExceededException => {
76                "com.amazonaws.dynamodb.v20120810#ItemCollectionSizeLimitExceededException"
77            }
78            Self::ProvisionedThroughputExceededException => {
79                "com.amazonaws.dynamodb.v20120810#ProvisionedThroughputExceededException"
80            }
81            Self::RequestLimitExceeded => "com.amazonaws.dynamodb.v20120810#RequestLimitExceeded",
82            Self::ValidationException => "com.amazon.coral.validate#ValidationException",
83            Self::SerializationException => {
84                "com.amazonaws.dynamodb.v20120810#SerializationException"
85            }
86            Self::InternalServerError => "com.amazonaws.dynamodb.v20120810#InternalServerError",
87            Self::MissingAction => "com.amazonaws.dynamodb.v20120810#MissingAction",
88            Self::AccessDeniedException => "com.amazonaws.dynamodb.v20120810#AccessDeniedException",
89            Self::UnrecognizedClientException => {
90                "com.amazonaws.dynamodb.v20120810#UnrecognizedClientException"
91            }
92        }
93    }
94
95    /// Returns the short error code string.
96    #[must_use]
97    pub fn as_str(&self) -> &'static str {
98        match self {
99            Self::ResourceInUseException => "ResourceInUseException",
100            Self::ResourceNotFoundException => "ResourceNotFoundException",
101            Self::ConditionalCheckFailedException => "ConditionalCheckFailedException",
102            Self::TransactionCanceledException => "TransactionCanceledException",
103            Self::TransactionConflictException => "TransactionConflictException",
104            Self::TransactionInProgressException => "TransactionInProgressException",
105            Self::IdempotentParameterMismatchException => "IdempotentParameterMismatchException",
106            Self::ItemCollectionSizeLimitExceededException => {
107                "ItemCollectionSizeLimitExceededException"
108            }
109            Self::ProvisionedThroughputExceededException => {
110                "ProvisionedThroughputExceededException"
111            }
112            Self::RequestLimitExceeded => "RequestLimitExceeded",
113            Self::ValidationException => "ValidationException",
114            Self::SerializationException => "SerializationException",
115            Self::InternalServerError => "InternalServerError",
116            Self::MissingAction => "MissingAction",
117            Self::AccessDeniedException => "AccessDeniedException",
118            Self::UnrecognizedClientException => "UnrecognizedClientException",
119        }
120    }
121
122    /// Returns the default HTTP status code for this error.
123    #[must_use]
124    pub fn default_status_code(&self) -> http::StatusCode {
125        match self {
126            Self::InternalServerError => http::StatusCode::INTERNAL_SERVER_ERROR,
127            _ => http::StatusCode::BAD_REQUEST,
128        }
129    }
130}
131
132impl fmt::Display for DynamoDBErrorCode {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        f.write_str(self.as_str())
135    }
136}
137
138/// A DynamoDB error response.
139#[derive(Debug)]
140pub struct DynamoDBError {
141    /// The error code.
142    pub code: DynamoDBErrorCode,
143    /// A human-readable error message.
144    pub message: String,
145    /// The HTTP status code.
146    pub status_code: http::StatusCode,
147    /// The underlying source error, if any.
148    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
149    /// The existing item to return in the error response (used by
150    /// `ReturnValuesOnConditionCheckFailure=ALL_OLD`).
151    pub item: Option<HashMap<String, AttributeValue>>,
152    /// Cancellation reasons for `TransactionCanceledException`.
153    pub cancellation_reasons: Vec<CancellationReason>,
154}
155
156impl fmt::Display for DynamoDBError {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        write!(f, "DynamoDBError({}): {}", self.code, self.message)
159    }
160}
161
162impl std::error::Error for DynamoDBError {
163    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
164        self.source
165            .as_ref()
166            .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
167    }
168}
169
170impl DynamoDBError {
171    /// Create a new `DynamoDBError` from an error code.
172    #[must_use]
173    pub fn new(code: DynamoDBErrorCode) -> Self {
174        Self {
175            status_code: code.default_status_code(),
176            message: code.as_str().to_owned(),
177            code,
178            source: None,
179            item: None,
180            cancellation_reasons: Vec::new(),
181        }
182    }
183
184    /// Create a new `DynamoDBError` with a custom message.
185    #[must_use]
186    pub fn with_message(code: DynamoDBErrorCode, message: impl Into<String>) -> Self {
187        Self {
188            status_code: code.default_status_code(),
189            message: message.into(),
190            code,
191            source: None,
192            item: None,
193            cancellation_reasons: Vec::new(),
194        }
195    }
196
197    /// Set the source error.
198    #[must_use]
199    pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
200        self.source = Some(Box::new(source));
201        self
202    }
203
204    /// Attach an existing item to the error response (for
205    /// `ReturnValuesOnConditionCheckFailure=ALL_OLD`).
206    #[must_use]
207    pub fn with_item(mut self, item: HashMap<String, AttributeValue>) -> Self {
208        self.item = Some(item);
209        self
210    }
211
212    /// Attach cancellation reasons to a `TransactionCanceledException`.
213    #[must_use]
214    pub fn with_cancellation_reasons(mut self, reasons: Vec<CancellationReason>) -> Self {
215        self.cancellation_reasons = reasons;
216        self
217    }
218
219    /// Returns the `__type` string for the JSON error response.
220    #[must_use]
221    pub fn error_type(&self) -> &'static str {
222        self.code.error_type()
223    }
224
225    // -- Convenience constructors --
226
227    /// Table already exists.
228    #[must_use]
229    pub fn resource_in_use(message: impl Into<String>) -> Self {
230        Self::with_message(DynamoDBErrorCode::ResourceInUseException, message)
231    }
232
233    /// Table or resource not found.
234    #[must_use]
235    pub fn resource_not_found(message: impl Into<String>) -> Self {
236        Self::with_message(DynamoDBErrorCode::ResourceNotFoundException, message)
237    }
238
239    /// Condition expression evaluated to false.
240    #[must_use]
241    pub fn conditional_check_failed(message: impl Into<String>) -> Self {
242        Self::with_message(DynamoDBErrorCode::ConditionalCheckFailedException, message)
243    }
244
245    /// Validation error.
246    #[must_use]
247    pub fn validation(message: impl Into<String>) -> Self {
248        Self::with_message(DynamoDBErrorCode::ValidationException, message)
249    }
250
251    /// Serialization error.
252    #[must_use]
253    pub fn serialization_exception(message: impl Into<String>) -> Self {
254        Self::with_message(DynamoDBErrorCode::SerializationException, message)
255    }
256
257    /// Internal server error.
258    #[must_use]
259    pub fn internal_error(message: impl Into<String>) -> Self {
260        Self::with_message(DynamoDBErrorCode::InternalServerError, message)
261    }
262
263    /// Missing action header.
264    #[must_use]
265    pub fn missing_action() -> Self {
266        Self::with_message(
267            DynamoDBErrorCode::MissingAction,
268            "Missing required header: X-Amz-Target",
269        )
270    }
271
272    /// Transaction cancelled with cancellation reasons.
273    #[must_use]
274    pub fn transaction_cancelled(reasons: Vec<CancellationReason>) -> Self {
275        Self::with_message(
276            DynamoDBErrorCode::TransactionCanceledException,
277            "Transaction cancelled, please refer cancellation reasons for specific reasons [See \
278             the CancellationReasons field]",
279        )
280        .with_cancellation_reasons(reasons)
281    }
282
283    /// Unknown operation.
284    #[must_use]
285    pub fn unknown_operation(target: &str) -> Self {
286        Self::with_message(
287            DynamoDBErrorCode::UnrecognizedClientException,
288            format!("Unrecognized operation: {target}"),
289        )
290    }
291}
292
293/// Create a `DynamoDBError` from an error code.
294///
295/// # Examples
296///
297/// ```
298/// use rustack_dynamodb_model::dynamodb_error;
299/// use rustack_dynamodb_model::error::DynamoDBErrorCode;
300///
301/// let err = dynamodb_error!(ValidationException);
302/// assert_eq!(err.code, DynamoDBErrorCode::ValidationException);
303///
304/// let err = dynamodb_error!(ResourceNotFoundException, "Table not found");
305/// assert_eq!(err.message, "Table not found");
306/// ```
307#[macro_export]
308macro_rules! dynamodb_error {
309    ($code:ident) => {
310        $crate::error::DynamoDBError::new($crate::error::DynamoDBErrorCode::$code)
311    };
312    ($code:ident, $msg:expr) => {
313        $crate::error::DynamoDBError::with_message($crate::error::DynamoDBErrorCode::$code, $msg)
314    };
315}