1use serde::Serialize;
2use std::collections::HashMap;
3use std::fmt;
4
5#[derive(Debug, Clone, Default, Serialize)]
10pub struct CancellationReason {
11 #[serde(rename = "Code")]
12 pub code: String,
13 #[serde(rename = "Message", skip_serializing_if = "Option::is_none")]
14 pub message: Option<String>,
15 #[serde(rename = "Item", skip_serializing_if = "Option::is_none")]
16 pub item: Option<HashMap<String, crate::types::AttributeValue>>,
17}
18
19#[derive(Debug, thiserror::Error)]
24pub enum DynoxideError {
25 #[error("{0}")]
27 ResourceNotFoundException(String),
28
29 #[error("{0}")]
31 ResourceInUseException(String),
32
33 #[error("{0}")]
35 ValidationException(String),
36
37 #[error("{0}")]
40 ConditionalCheckFailedException(
41 String,
42 Option<HashMap<String, crate::types::AttributeValue>>,
43 ),
44
45 #[error("{0}")]
48 TransactionCanceledException(String, Vec<CancellationReason>),
49
50 #[error("{0}")]
52 ItemCollectionSizeLimitExceededException(String),
53
54 #[error("{0}")]
56 DuplicateItemException(String),
57
58 #[error("{0}")]
60 ProvisionedThroughputExceededException(String),
61
62 #[error("{0}")]
64 SerializationException(String),
65
66 #[error("{0}")]
68 LimitExceededException(String),
69
70 #[error("{0}")]
72 AccessDeniedException(String),
73
74 #[error("{0}")]
76 IdempotentParameterMismatchException(String),
77
78 #[error("{0}")]
80 InternalServerError(String),
81
82 #[error("Conversion error: {0}")]
84 ConversionError(#[from] crate::types::ConversionError),
85
86 #[error("Internal error: {0}")]
88 SqliteError(#[from] rusqlite::Error),
89}
90
91impl DynoxideError {
92 pub fn error_type(&self) -> &'static str {
94 match self {
95 DynoxideError::ResourceNotFoundException(_) => {
96 "com.amazonaws.dynamodb.v20120810#ResourceNotFoundException"
97 }
98 DynoxideError::ResourceInUseException(_) => {
99 "com.amazonaws.dynamodb.v20120810#ResourceInUseException"
100 }
101 DynoxideError::ValidationException(_) => {
102 "com.amazon.coral.validate#ValidationException"
103 }
104 DynoxideError::ConditionalCheckFailedException(..) => {
105 "com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException"
106 }
107 DynoxideError::TransactionCanceledException(..) => {
108 "com.amazonaws.dynamodb.v20120810#TransactionCanceledException"
109 }
110 DynoxideError::DuplicateItemException(_) => {
111 "com.amazonaws.dynamodb.v20120810#DuplicateItemException"
112 }
113 DynoxideError::ItemCollectionSizeLimitExceededException(_) => {
114 "com.amazonaws.dynamodb.v20120810#ItemCollectionSizeLimitExceededException"
115 }
116 DynoxideError::ProvisionedThroughputExceededException(_) => {
117 "com.amazonaws.dynamodb.v20120810#ProvisionedThroughputExceededException"
118 }
119 DynoxideError::SerializationException(_) => {
120 "com.amazon.coral.service#SerializationException"
121 }
122 DynoxideError::LimitExceededException(_) => {
123 "com.amazonaws.dynamodb.v20120810#LimitExceededException"
124 }
125 DynoxideError::AccessDeniedException(_) => {
126 "com.amazonaws.dynamodb.v20120810#AccessDeniedException"
127 }
128 DynoxideError::IdempotentParameterMismatchException(_) => {
129 "com.amazonaws.dynamodb.v20120810#IdempotentParameterMismatchException"
130 }
131 DynoxideError::ConversionError(_) => "com.amazon.coral.validate#ValidationException",
132 DynoxideError::InternalServerError(_) | DynoxideError::SqliteError(_) => {
133 "com.amazonaws.dynamodb.v20120810#InternalServerError"
134 }
135 }
136 }
137
138 pub fn short_error_code(&self) -> &'static str {
143 match self {
144 DynoxideError::ResourceNotFoundException(_) => "ResourceNotFound",
145 DynoxideError::ResourceInUseException(_) => "ResourceInUse",
146 DynoxideError::ValidationException(_) | DynoxideError::ConversionError(_) => {
147 "ValidationError"
148 }
149 DynoxideError::ConditionalCheckFailedException(..) => "ConditionalCheckFailed",
150 DynoxideError::TransactionCanceledException(..) => "TransactionConflict",
151 DynoxideError::DuplicateItemException(_) => "DuplicateItem",
152 DynoxideError::ItemCollectionSizeLimitExceededException(_) => {
153 "ItemCollectionSizeLimitExceeded"
154 }
155 DynoxideError::ProvisionedThroughputExceededException(_) => {
156 "ProvisionedThroughputExceeded"
157 }
158 DynoxideError::AccessDeniedException(_) => "AccessDenied",
159 DynoxideError::IdempotentParameterMismatchException(_) => "IdempotentParameterMismatch",
160 DynoxideError::SerializationException(_) => "SerializationError",
161 DynoxideError::LimitExceededException(_) => "RequestLimitExceeded",
162 DynoxideError::InternalServerError(_) | DynoxideError::SqliteError(_) => {
163 "InternalServerError"
164 }
165 }
166 }
167
168 pub fn status_code(&self) -> u16 {
170 match self {
171 DynoxideError::InternalServerError(_) | DynoxideError::SqliteError(_) => 500,
172 _ => 400,
173 }
174 }
175
176 pub fn to_response(&self) -> ErrorResponse {
178 let item = if let DynoxideError::ConditionalCheckFailedException(_, item) = self {
179 item.clone()
180 } else {
181 None
182 };
183 ErrorResponse {
184 error_type: self.error_type().to_string(),
185 message: self.to_string(),
186 item,
187 }
188 }
189
190 pub fn to_json(&self) -> String {
196 let error_type = self.error_type();
197 let message = self.to_string();
198
199 match self {
200 DynoxideError::TransactionCanceledException(_, reasons) => {
201 let mut m = serde_json::Map::new();
202 m.insert(
203 "__type".to_string(),
204 serde_json::Value::String(error_type.to_string()),
205 );
206 m.insert("Message".to_string(), serde_json::Value::String(message));
207 if let Ok(reasons_val) = serde_json::to_value(reasons) {
208 m.insert("CancellationReasons".to_string(), reasons_val);
209 }
210 serde_json::to_string(&m).unwrap_or_default()
211 }
212 DynoxideError::SerializationException(_) => {
213 let mut m = serde_json::Map::new();
214 m.insert(
215 "__type".to_string(),
216 serde_json::Value::String(error_type.to_string()),
217 );
218 m.insert("Message".to_string(), serde_json::Value::String(message));
219 serde_json::to_string(&m).unwrap_or_default()
220 }
221 _ => {
222 let resp = self.to_response();
223 serde_json::to_string(&resp).unwrap_or_default()
224 }
225 }
226 }
227}
228
229#[derive(Debug, Serialize)]
231pub struct ErrorResponse {
232 #[serde(rename = "__type")]
233 pub error_type: String,
234 #[serde(rename = "message")]
235 pub message: String,
236 #[serde(rename = "Item", skip_serializing_if = "Option::is_none")]
237 pub item: Option<HashMap<String, crate::types::AttributeValue>>,
238}
239
240impl fmt::Display for ErrorResponse {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 write!(f, "{}", serde_json::to_string(self).unwrap_or_default())
243 }
244}
245
246pub type Result<T> = std::result::Result<T, DynoxideError>;
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn test_error_response_format() {
255 let err = DynoxideError::ResourceNotFoundException(
256 "Requested resource not found: Table: NonExistent not found".to_string(),
257 );
258 let resp = err.to_response();
259 let json = serde_json::to_string(&resp).unwrap();
260
261 assert!(json.contains("\"__type\""));
262 assert!(json.contains("ResourceNotFoundException"));
263 assert!(json.contains("NonExistent not found"));
264 }
265
266 #[test]
267 fn test_status_codes() {
268 assert_eq!(
269 DynoxideError::ResourceNotFoundException("".into()).status_code(),
270 400
271 );
272 assert_eq!(
273 DynoxideError::ResourceInUseException("".into()).status_code(),
274 400
275 );
276 assert_eq!(
277 DynoxideError::ValidationException("".into()).status_code(),
278 400
279 );
280 assert_eq!(
281 DynoxideError::ConditionalCheckFailedException("".into(), None).status_code(),
282 400
283 );
284 assert_eq!(
285 DynoxideError::TransactionCanceledException("".into(), vec![]).status_code(),
286 400
287 );
288 assert_eq!(
289 DynoxideError::InternalServerError("".into()).status_code(),
290 500
291 );
292 }
293
294 #[test]
295 fn test_error_type_strings() {
296 let err = DynoxideError::ValidationException("bad input".into());
297 assert_eq!(
298 err.error_type(),
299 "com.amazon.coral.validate#ValidationException"
300 );
301 }
302
303 #[test]
304 fn test_sqlite_error_maps_to_internal() {
305 let sqlite_err = rusqlite::Error::QueryReturnedNoRows;
306 let err = DynoxideError::from(sqlite_err);
307 assert_eq!(err.status_code(), 500);
308 assert!(err.error_type().contains("InternalServerError"));
309 }
310
311 #[test]
312 fn test_error_response_json_structure() {
313 let err = DynoxideError::ValidationException("1 validation error detected".to_string());
314 let resp = err.to_response();
315 let json: serde_json::Value = serde_json::to_value(&resp).unwrap();
316
317 assert!(json.get("__type").is_some());
318 assert!(json.get("message").is_some());
319 assert_eq!(
320 json["__type"],
321 "com.amazon.coral.validate#ValidationException"
322 );
323 assert_eq!(json["message"], "1 validation error detected");
324 }
325
326 #[test]
327 fn test_short_error_codes() {
328 assert_eq!(
329 DynoxideError::ResourceNotFoundException("".into()).short_error_code(),
330 "ResourceNotFound"
331 );
332 assert_eq!(
333 DynoxideError::ValidationException("".into()).short_error_code(),
334 "ValidationError"
335 );
336 assert_eq!(
337 DynoxideError::ConditionalCheckFailedException("".into(), None).short_error_code(),
338 "ConditionalCheckFailed"
339 );
340 assert_eq!(
341 DynoxideError::DuplicateItemException("".into()).short_error_code(),
342 "DuplicateItem"
343 );
344 assert_eq!(
345 DynoxideError::InternalServerError("".into()).short_error_code(),
346 "InternalServerError"
347 );
348 }
349
350 #[test]
351 fn test_transaction_cancelled_json_has_cancellation_reasons() {
352 let reasons = vec![
353 CancellationReason {
354 code: "ConditionalCheckFailed".to_string(),
355 message: Some("The conditional request failed".to_string()),
356 item: None,
357 },
358 CancellationReason {
359 code: "None".to_string(),
360 message: None,
361 item: None,
362 },
363 ];
364 let err = DynoxideError::TransactionCanceledException(
365 "Transaction cancelled, please refer cancellation reasons for specific reasons [ConditionalCheckFailed, None]".to_string(),
366 reasons,
367 );
368 let json_str = err.to_json();
369 let json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
370
371 assert!(json.get("CancellationReasons").is_some());
373 let reasons = json["CancellationReasons"].as_array().unwrap();
374 assert_eq!(reasons.len(), 2);
375 assert_eq!(reasons[0]["Code"], "ConditionalCheckFailed");
376 assert_eq!(reasons[1]["Code"], "None");
377
378 assert!(json.get("Message").is_some());
380 assert!(json.get("message").is_none());
381 }
382}