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)]
28#[non_exhaustive]
29pub enum DynoxideError {
30 #[error("{0}")]
32 ResourceNotFoundException(String),
33
34 #[error("{0}")]
36 ResourceInUseException(String),
37
38 #[error("{0}")]
40 ValidationException(String),
41
42 #[error("{0}")]
45 ConditionalCheckFailedException(
46 String,
47 Option<HashMap<String, crate::types::AttributeValue>>,
48 ),
49
50 #[error("{0}")]
53 TransactionCanceledException(String, Vec<CancellationReason>),
54
55 #[error("{0}")]
57 ItemCollectionSizeLimitExceededException(String),
58
59 #[error("{0}")]
61 DuplicateItemException(String),
62
63 #[error("{0}")]
65 ProvisionedThroughputExceededException(String),
66
67 #[error("{0}")]
69 SerializationException(String),
70
71 #[error("{0}")]
73 LimitExceededException(String),
74
75 #[error("{0}")]
77 AccessDeniedException(String),
78
79 #[error("{0}")]
81 IdempotentParameterMismatchException(String),
82
83 #[error("{0}")]
85 InternalServerError(String),
86
87 #[error("Conversion error: {0}")]
89 ConversionError(#[from] crate::types::ConversionError),
90
91 #[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
93 #[error("Internal error: {0}")]
94 SqliteError(#[from] rusqlite::Error),
95}
96
97impl From<crate::storage_backend::BackendError> for DynoxideError {
113 fn from(err: crate::storage_backend::BackendError) -> Self {
114 use crate::storage_backend::BackendError;
115 match err {
116 BackendError::Validation(msg) => DynoxideError::ValidationException(msg),
117 other => DynoxideError::InternalServerError(other.to_string()),
118 }
119 }
120}
121
122impl DynoxideError {
123 pub fn error_type(&self) -> &'static str {
125 match self {
126 DynoxideError::ResourceNotFoundException(_) => {
127 "com.amazonaws.dynamodb.v20120810#ResourceNotFoundException"
128 }
129 DynoxideError::ResourceInUseException(_) => {
130 "com.amazonaws.dynamodb.v20120810#ResourceInUseException"
131 }
132 DynoxideError::ValidationException(_) => {
133 "com.amazon.coral.validate#ValidationException"
134 }
135 DynoxideError::ConditionalCheckFailedException(..) => {
136 "com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException"
137 }
138 DynoxideError::TransactionCanceledException(..) => {
139 "com.amazonaws.dynamodb.v20120810#TransactionCanceledException"
140 }
141 DynoxideError::DuplicateItemException(_) => {
142 "com.amazonaws.dynamodb.v20120810#DuplicateItemException"
143 }
144 DynoxideError::ItemCollectionSizeLimitExceededException(_) => {
145 "com.amazonaws.dynamodb.v20120810#ItemCollectionSizeLimitExceededException"
146 }
147 DynoxideError::ProvisionedThroughputExceededException(_) => {
148 "com.amazonaws.dynamodb.v20120810#ProvisionedThroughputExceededException"
149 }
150 DynoxideError::SerializationException(_) => {
151 "com.amazon.coral.service#SerializationException"
152 }
153 DynoxideError::LimitExceededException(_) => {
154 "com.amazonaws.dynamodb.v20120810#LimitExceededException"
155 }
156 DynoxideError::AccessDeniedException(_) => {
157 "com.amazonaws.dynamodb.v20120810#AccessDeniedException"
158 }
159 DynoxideError::IdempotentParameterMismatchException(_) => {
160 "com.amazonaws.dynamodb.v20120810#IdempotentParameterMismatchException"
161 }
162 DynoxideError::ConversionError(_) => "com.amazon.coral.validate#ValidationException",
163 DynoxideError::InternalServerError(_) => {
164 "com.amazonaws.dynamodb.v20120810#InternalServerError"
165 }
166 #[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
167 DynoxideError::SqliteError(_) => "com.amazonaws.dynamodb.v20120810#InternalServerError",
168 }
169 }
170
171 pub fn short_error_code(&self) -> &'static str {
176 match self {
177 DynoxideError::ResourceNotFoundException(_) => "ResourceNotFound",
178 DynoxideError::ResourceInUseException(_) => "ResourceInUse",
179 DynoxideError::ValidationException(_) | DynoxideError::ConversionError(_) => {
180 "ValidationError"
181 }
182 DynoxideError::ConditionalCheckFailedException(..) => "ConditionalCheckFailed",
183 DynoxideError::TransactionCanceledException(..) => "TransactionConflict",
184 DynoxideError::DuplicateItemException(_) => "DuplicateItem",
185 DynoxideError::ItemCollectionSizeLimitExceededException(_) => {
186 "ItemCollectionSizeLimitExceeded"
187 }
188 DynoxideError::ProvisionedThroughputExceededException(_) => {
189 "ProvisionedThroughputExceeded"
190 }
191 DynoxideError::AccessDeniedException(_) => "AccessDenied",
192 DynoxideError::IdempotentParameterMismatchException(_) => "IdempotentParameterMismatch",
193 DynoxideError::SerializationException(_) => "SerializationError",
194 DynoxideError::LimitExceededException(_) => "RequestLimitExceeded",
195 DynoxideError::InternalServerError(_) => "InternalServerError",
196 #[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
197 DynoxideError::SqliteError(_) => "InternalServerError",
198 }
199 }
200
201 pub fn status_code(&self) -> u16 {
203 match self {
204 DynoxideError::InternalServerError(_) => 500,
205 #[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
206 DynoxideError::SqliteError(_) => 500,
207 _ => 400,
208 }
209 }
210
211 pub fn to_response(&self) -> ErrorResponse {
213 let item = if let DynoxideError::ConditionalCheckFailedException(_, item) = self {
214 item.clone()
215 } else {
216 None
217 };
218 ErrorResponse {
219 error_type: self.error_type().to_string(),
220 message: self.to_string(),
221 item,
222 }
223 }
224
225 pub fn to_json(&self) -> String {
231 let error_type = self.error_type();
232 let message = self.to_string();
233
234 match self {
235 DynoxideError::TransactionCanceledException(_, reasons) => {
236 let mut m = serde_json::Map::new();
237 m.insert(
238 "__type".to_string(),
239 serde_json::Value::String(error_type.to_string()),
240 );
241 m.insert("Message".to_string(), serde_json::Value::String(message));
242 if let Ok(reasons_val) = serde_json::to_value(reasons) {
243 m.insert("CancellationReasons".to_string(), reasons_val);
244 }
245 serde_json::to_string(&m).unwrap_or_default()
246 }
247 DynoxideError::SerializationException(_) => {
248 let mut m = serde_json::Map::new();
249 m.insert(
250 "__type".to_string(),
251 serde_json::Value::String(error_type.to_string()),
252 );
253 m.insert("Message".to_string(), serde_json::Value::String(message));
254 serde_json::to_string(&m).unwrap_or_default()
255 }
256 _ => {
257 let resp = self.to_response();
258 serde_json::to_string(&resp).unwrap_or_default()
259 }
260 }
261 }
262}
263
264#[derive(Debug, Serialize)]
266pub struct ErrorResponse {
267 #[serde(rename = "__type")]
268 pub error_type: String,
269 #[serde(rename = "message")]
270 pub message: String,
271 #[serde(rename = "Item", skip_serializing_if = "Option::is_none")]
272 pub item: Option<HashMap<String, crate::types::AttributeValue>>,
273}
274
275impl fmt::Display for ErrorResponse {
276 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277 write!(f, "{}", serde_json::to_string(self).unwrap_or_default())
278 }
279}
280
281pub type Result<T> = std::result::Result<T, DynoxideError>;
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_error_response_format() {
290 let err = DynoxideError::ResourceNotFoundException(
291 "Requested resource not found: Table: NonExistent not found".to_string(),
292 );
293 let resp = err.to_response();
294 let json = serde_json::to_string(&resp).unwrap();
295
296 assert!(json.contains("\"__type\""));
297 assert!(json.contains("ResourceNotFoundException"));
298 assert!(json.contains("NonExistent not found"));
299 }
300
301 #[test]
302 fn test_status_codes() {
303 assert_eq!(
304 DynoxideError::ResourceNotFoundException("".into()).status_code(),
305 400
306 );
307 assert_eq!(
308 DynoxideError::ResourceInUseException("".into()).status_code(),
309 400
310 );
311 assert_eq!(
312 DynoxideError::ValidationException("".into()).status_code(),
313 400
314 );
315 assert_eq!(
316 DynoxideError::ConditionalCheckFailedException("".into(), None).status_code(),
317 400
318 );
319 assert_eq!(
320 DynoxideError::TransactionCanceledException("".into(), vec![]).status_code(),
321 400
322 );
323 assert_eq!(
324 DynoxideError::InternalServerError("".into()).status_code(),
325 500
326 );
327 }
328
329 #[test]
330 fn test_error_type_strings() {
331 let err = DynoxideError::ValidationException("bad input".into());
332 assert_eq!(
333 err.error_type(),
334 "com.amazon.coral.validate#ValidationException"
335 );
336 }
337
338 #[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
339 #[test]
340 fn test_sqlite_error_maps_to_internal() {
341 let sqlite_err = rusqlite::Error::QueryReturnedNoRows;
342 let err = DynoxideError::from(sqlite_err);
343 assert_eq!(err.status_code(), 500);
344 assert!(err.error_type().contains("InternalServerError"));
345 }
346
347 #[test]
356 fn test_backend_error_envelopes_match_native() {
357 use crate::storage_backend::BackendError;
358
359 let v: DynoxideError = BackendError::Validation("too many tags".into()).into();
361 assert_eq!(v.status_code(), 400);
362 assert_eq!(
363 v.error_type(),
364 "com.amazon.coral.validate#ValidationException"
365 );
366
367 let u: DynoxideError = BackendError::Unsupported { capability: "ttl" }.into();
370 assert_eq!(u.status_code(), 500);
371 assert!(u.error_type().contains("InternalServerError"));
372 assert!(u.to_string().contains("ttl"));
373
374 for e in [
377 BackendError::NotADatabase,
378 BackendError::Locked,
379 BackendError::Constraint("constraint".into()),
380 BackendError::Io("io".into()),
381 BackendError::Other("wa-sqlite: boom".into()),
382 ] {
383 let d: DynoxideError = e.into();
384 assert_eq!(d.status_code(), 500);
385 assert!(d.error_type().contains("InternalServerError"));
386 }
387 }
388
389 #[test]
390 fn test_error_response_json_structure() {
391 let err = DynoxideError::ValidationException("1 validation error detected".to_string());
392 let resp = err.to_response();
393 let json: serde_json::Value = serde_json::to_value(&resp).unwrap();
394
395 assert!(json.get("__type").is_some());
396 assert!(json.get("message").is_some());
397 assert_eq!(
398 json["__type"],
399 "com.amazon.coral.validate#ValidationException"
400 );
401 assert_eq!(json["message"], "1 validation error detected");
402 }
403
404 #[test]
405 fn test_short_error_codes() {
406 assert_eq!(
407 DynoxideError::ResourceNotFoundException("".into()).short_error_code(),
408 "ResourceNotFound"
409 );
410 assert_eq!(
411 DynoxideError::ValidationException("".into()).short_error_code(),
412 "ValidationError"
413 );
414 assert_eq!(
415 DynoxideError::ConditionalCheckFailedException("".into(), None).short_error_code(),
416 "ConditionalCheckFailed"
417 );
418 assert_eq!(
419 DynoxideError::DuplicateItemException("".into()).short_error_code(),
420 "DuplicateItem"
421 );
422 assert_eq!(
423 DynoxideError::InternalServerError("".into()).short_error_code(),
424 "InternalServerError"
425 );
426 }
427
428 #[test]
429 fn test_transaction_cancelled_json_has_cancellation_reasons() {
430 let reasons = vec![
431 CancellationReason {
432 code: "ConditionalCheckFailed".to_string(),
433 message: Some("The conditional request failed".to_string()),
434 item: None,
435 },
436 CancellationReason {
437 code: "None".to_string(),
438 message: None,
439 item: None,
440 },
441 ];
442 let err = DynoxideError::TransactionCanceledException(
443 "Transaction cancelled, please refer cancellation reasons for specific reasons [ConditionalCheckFailed, None]".to_string(),
444 reasons,
445 );
446 let json_str = err.to_json();
447 let json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
448
449 assert!(json.get("CancellationReasons").is_some());
451 let reasons = json["CancellationReasons"].as_array().unwrap();
452 assert_eq!(reasons.len(), 2);
453 assert_eq!(reasons[0]["Code"], "ConditionalCheckFailed");
454 assert_eq!(reasons[1]["Code"], "None");
455
456 assert!(json.get("Message").is_some());
458 assert!(json.get("message").is_none());
459 }
460
461 #[test]
462 fn test_backend_error_maps_to_internal() {
463 use crate::storage_backend::BackendError;
464 let err: DynoxideError = BackendError::Locked.into();
465 assert_eq!(err.status_code(), 500);
466 assert!(err.error_type().contains("InternalServerError"));
467 assert!(err.to_string().contains("locked"));
468 }
469}