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(
16 rename = "Item",
17 skip_serializing_if = "Option::is_none",
18 serialize_with = "crate::set_order::item_in_hash_order"
19 )]
20 pub item: Option<HashMap<String, crate::types::AttributeValue>>,
23}
24
25#[derive(Debug, thiserror::Error)]
34#[non_exhaustive]
35pub enum DynoxideError {
36 #[error("{0}")]
38 ResourceNotFoundException(String),
39
40 #[error("{0}")]
42 ResourceInUseException(String),
43
44 #[error("{0}")]
46 ValidationException(String),
47
48 #[error("{0}")]
53 KeyEmptyValueValidation(String),
54
55 #[error("{0}")]
60 EnvelopedValidation(String),
61
62 #[error("{0}")]
65 ConditionalCheckFailedException(
66 String,
67 Option<HashMap<String, crate::types::AttributeValue>>,
68 ),
69
70 #[error("{0}")]
79 StatementConditionalCheckFailed(String, HashMap<String, crate::types::AttributeValue>),
80
81 #[error("{0}")]
84 TransactionCanceledException(String, Vec<CancellationReason>),
85
86 #[error("{0}")]
88 ItemCollectionSizeLimitExceededException(String),
89
90 #[error("{0}")]
92 DuplicateItemException(String),
93
94 #[error("{0}")]
96 ProvisionedThroughputExceededException(String),
97
98 #[error("{0}")]
100 SerializationException(String),
101
102 #[error("{0}")]
104 LimitExceededException(String),
105
106 #[error("{0}")]
108 AccessDeniedException(String),
109
110 #[error("{0}")]
112 IdempotentParameterMismatchException(String),
113
114 #[error("{0}")]
116 InternalServerError(String),
117
118 #[error("'{0}' is not supported by this build of the engine")]
125 UnsupportedCapability(String),
126
127 #[error("Conversion error: {0}")]
129 ConversionError(#[from] crate::types::ConversionError),
130
131 #[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
133 #[error("Internal error: {0}")]
134 SqliteError(#[from] rusqlite::Error),
135
136 #[cfg(feature = "wasm-sqlite")]
140 #[error("{0}")]
141 OpfsUnavailable(String),
142}
143
144impl From<crate::storage_backend::BackendError> for DynoxideError {
164 fn from(err: crate::storage_backend::BackendError) -> Self {
165 use crate::storage_backend::BackendError;
166 match err {
167 BackendError::Validation(msg) => DynoxideError::ValidationException(msg),
168 BackendError::Unsupported { capability } => {
169 DynoxideError::UnsupportedCapability(capability.to_string())
170 }
171 #[cfg(feature = "wasm-sqlite")]
172 BackendError::OpfsUnavailable(msg) => DynoxideError::OpfsUnavailable(msg),
173 other => DynoxideError::InternalServerError(other.to_string()),
174 }
175 }
176}
177
178pub(crate) const UNSUPPORTED_TYPE: &str = "com.dynoxide.wasm#UnsupportedOperation";
184
185impl DynoxideError {
186 pub fn error_type(&self) -> &'static str {
188 match self {
189 DynoxideError::ResourceNotFoundException(_) => {
190 "com.amazonaws.dynamodb.v20120810#ResourceNotFoundException"
191 }
192 DynoxideError::ResourceInUseException(_) => {
193 "com.amazonaws.dynamodb.v20120810#ResourceInUseException"
194 }
195 DynoxideError::ValidationException(_)
196 | DynoxideError::KeyEmptyValueValidation(_)
197 | DynoxideError::EnvelopedValidation(_) => {
198 "com.amazon.coral.validate#ValidationException"
199 }
200 DynoxideError::ConditionalCheckFailedException(..)
201 | DynoxideError::StatementConditionalCheckFailed(..) => {
202 "com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException"
203 }
204 DynoxideError::TransactionCanceledException(..) => {
205 "com.amazonaws.dynamodb.v20120810#TransactionCanceledException"
206 }
207 DynoxideError::DuplicateItemException(_) => {
208 "com.amazonaws.dynamodb.v20120810#DuplicateItemException"
209 }
210 DynoxideError::ItemCollectionSizeLimitExceededException(_) => {
211 "com.amazonaws.dynamodb.v20120810#ItemCollectionSizeLimitExceededException"
212 }
213 DynoxideError::ProvisionedThroughputExceededException(_) => {
214 "com.amazonaws.dynamodb.v20120810#ProvisionedThroughputExceededException"
215 }
216 DynoxideError::SerializationException(_) => {
217 "com.amazon.coral.service#SerializationException"
218 }
219 DynoxideError::LimitExceededException(_) => {
220 "com.amazonaws.dynamodb.v20120810#LimitExceededException"
221 }
222 DynoxideError::AccessDeniedException(_) => {
223 "com.amazonaws.dynamodb.v20120810#AccessDeniedException"
224 }
225 DynoxideError::IdempotentParameterMismatchException(_) => {
226 "com.amazonaws.dynamodb.v20120810#IdempotentParameterMismatchException"
227 }
228 DynoxideError::ConversionError(_) => "com.amazon.coral.validate#ValidationException",
229 DynoxideError::InternalServerError(_) => {
230 "com.amazonaws.dynamodb.v20120810#InternalServerError"
231 }
232 DynoxideError::UnsupportedCapability(_) => UNSUPPORTED_TYPE,
235 #[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
236 DynoxideError::SqliteError(_) => "com.amazonaws.dynamodb.v20120810#InternalServerError",
237 #[cfg(feature = "wasm-sqlite")]
238 DynoxideError::OpfsUnavailable(_) => "com.dynoxide.wasm#OpfsUnavailable",
239 }
240 }
241
242 pub fn short_error_code(&self) -> &'static str {
247 match self {
248 DynoxideError::ResourceNotFoundException(_) => "ResourceNotFound",
249 DynoxideError::ResourceInUseException(_) => "ResourceInUse",
250 DynoxideError::ValidationException(_)
251 | DynoxideError::KeyEmptyValueValidation(_)
252 | DynoxideError::EnvelopedValidation(_)
253 | DynoxideError::ConversionError(_) => "ValidationError",
254 DynoxideError::ConditionalCheckFailedException(..)
255 | DynoxideError::StatementConditionalCheckFailed(..) => "ConditionalCheckFailed",
256 DynoxideError::TransactionCanceledException(..) => "TransactionConflict",
257 DynoxideError::DuplicateItemException(_) => "DuplicateItem",
258 DynoxideError::ItemCollectionSizeLimitExceededException(_) => {
259 "ItemCollectionSizeLimitExceeded"
260 }
261 DynoxideError::ProvisionedThroughputExceededException(_) => {
262 "ProvisionedThroughputExceeded"
263 }
264 DynoxideError::AccessDeniedException(_) => "AccessDenied",
265 DynoxideError::IdempotentParameterMismatchException(_) => "IdempotentParameterMismatch",
266 DynoxideError::SerializationException(_) => "SerializationError",
267 DynoxideError::LimitExceededException(_) => "RequestLimitExceeded",
268 DynoxideError::InternalServerError(_) => "InternalServerError",
269 DynoxideError::UnsupportedCapability(_) => "UnsupportedOperation",
270 #[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
271 DynoxideError::SqliteError(_) => "InternalServerError",
272 #[cfg(feature = "wasm-sqlite")]
273 DynoxideError::OpfsUnavailable(_) => "OpfsUnavailable",
274 }
275 }
276
277 pub fn status_code(&self) -> u16 {
279 match self {
280 DynoxideError::InternalServerError(_) => 500,
281 #[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
282 DynoxideError::SqliteError(_) => 500,
283 DynoxideError::UnsupportedCapability(_) => 501,
287 _ => 400,
288 }
289 }
290
291 pub fn to_response(&self) -> ErrorResponse {
293 let item = match self {
294 DynoxideError::ConditionalCheckFailedException(_, item) => item.clone(),
295 DynoxideError::StatementConditionalCheckFailed(_, item) => Some(item.clone()),
296 _ => None,
297 };
298 ErrorResponse {
299 error_type: self.error_type().to_string(),
300 message: self.to_string(),
301 item,
302 }
303 }
304
305 pub fn to_json(&self) -> String {
311 let error_type = self.error_type();
312 let message = self.to_string();
313
314 match self {
315 DynoxideError::TransactionCanceledException(_, reasons) => {
316 let mut m = serde_json::Map::new();
317 m.insert(
318 "__type".to_string(),
319 serde_json::Value::String(error_type.to_string()),
320 );
321 m.insert("Message".to_string(), serde_json::Value::String(message));
322 if let Ok(reasons_val) = serde_json::to_value(reasons) {
323 m.insert("CancellationReasons".to_string(), reasons_val);
324 }
325 serde_json::to_string(&m).unwrap_or_default()
326 }
327 DynoxideError::StatementConditionalCheckFailed(_, item) => {
328 let mut m = serde_json::Map::new();
329 m.insert(
330 "__type".to_string(),
331 serde_json::Value::String(error_type.to_string()),
332 );
333 m.insert("Message".to_string(), serde_json::Value::String(message));
334 if let Ok(item) = serde_json::to_value(crate::set_order::HashOrderedItem(item)) {
335 m.insert("Item".to_string(), item);
336 }
337 serde_json::to_string(&m).unwrap_or_default()
338 }
339 DynoxideError::SerializationException(_) => {
340 let mut m = serde_json::Map::new();
341 m.insert(
342 "__type".to_string(),
343 serde_json::Value::String(error_type.to_string()),
344 );
345 if !message.is_empty() {
348 m.insert("Message".to_string(), serde_json::Value::String(message));
349 }
350 serde_json::to_string(&m).unwrap_or_default()
351 }
352 _ => {
353 let resp = self.to_response();
354 serde_json::to_string(&resp).unwrap_or_default()
355 }
356 }
357 }
358}
359
360#[derive(Debug, Serialize)]
362pub struct ErrorResponse {
363 #[serde(rename = "__type")]
364 pub error_type: String,
365 #[serde(rename = "message")]
366 pub message: String,
367 #[serde(rename = "Item", skip_serializing_if = "Option::is_none")]
368 pub item: Option<HashMap<String, crate::types::AttributeValue>>,
371}
372
373impl fmt::Display for ErrorResponse {
374 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
375 write!(f, "{}", serde_json::to_string(self).unwrap_or_default())
376 }
377}
378
379pub type Result<T> = std::result::Result<T, DynoxideError>;
381
382#[cfg(any(feature = "http-server", feature = "wasm-sqlite", test))]
388pub(crate) fn for_operation(operation: &str, err: DynoxideError) -> DynoxideError {
389 match (operation, err) {
390 (
391 "ExecuteStatement",
392 DynoxideError::ConditionalCheckFailedException(message, Some(item)),
393 ) => DynoxideError::StatementConditionalCheckFailed(message, item),
394 (_, err) => err,
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn test_error_response_format() {
404 let err = DynoxideError::ResourceNotFoundException(
405 "Requested resource not found: Table: NonExistent not found".to_string(),
406 );
407 let resp = err.to_response();
408 let json = serde_json::to_string(&resp).unwrap();
409
410 assert!(json.contains("\"__type\""));
411 assert!(json.contains("ResourceNotFoundException"));
412 assert!(json.contains("NonExistent not found"));
413 }
414
415 #[test]
416 fn test_status_codes() {
417 assert_eq!(
418 DynoxideError::ResourceNotFoundException("".into()).status_code(),
419 400
420 );
421 assert_eq!(
422 DynoxideError::ResourceInUseException("".into()).status_code(),
423 400
424 );
425 assert_eq!(
426 DynoxideError::ValidationException("".into()).status_code(),
427 400
428 );
429 assert_eq!(
430 DynoxideError::ConditionalCheckFailedException("".into(), None).status_code(),
431 400
432 );
433 assert_eq!(
434 DynoxideError::TransactionCanceledException("".into(), vec![]).status_code(),
435 400
436 );
437 assert_eq!(
438 DynoxideError::InternalServerError("".into()).status_code(),
439 500
440 );
441 }
442
443 #[test]
444 fn test_key_empty_value_validation_is_wire_identical_to_validation_exception() {
445 let messages = [
448 "One or more parameter values are not valid. The AttributeValue for a key \
449 attribute cannot contain an empty string value. Key: pk",
450 "One or more parameter values are not valid. The AttributeValue for a key \
451 attribute cannot contain an empty binary value. Key: pk",
452 ];
453 for msg in messages {
454 let empty = DynoxideError::KeyEmptyValueValidation(msg.to_string());
455 let plain = DynoxideError::ValidationException(msg.to_string());
456 assert_eq!(empty.status_code(), plain.status_code());
457 assert_eq!(empty.error_type(), plain.error_type());
458 assert_eq!(empty.short_error_code(), plain.short_error_code());
459 assert_eq!(empty.to_json(), plain.to_json());
460 assert_eq!(empty.to_string(), plain.to_string());
461 }
462 }
463
464 #[test]
465 fn test_enveloped_validation_is_wire_identical_to_validation_exception() {
466 let msg = "One or more parameter values were invalid: \
469 Type mismatch for key pk expected: S actual: N";
470 let enveloped = DynoxideError::EnvelopedValidation(msg.to_string());
471 let plain = DynoxideError::ValidationException(msg.to_string());
472 assert_eq!(enveloped.status_code(), plain.status_code());
473 assert_eq!(enveloped.error_type(), plain.error_type());
474 assert_eq!(enveloped.short_error_code(), plain.short_error_code());
475 assert_eq!(enveloped.to_json(), plain.to_json());
476 assert_eq!(enveloped.to_string(), plain.to_string());
477 }
478
479 #[test]
480 fn test_error_type_strings() {
481 let err = DynoxideError::ValidationException("bad input".into());
482 assert_eq!(
483 err.error_type(),
484 "com.amazon.coral.validate#ValidationException"
485 );
486 }
487
488 #[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
489 #[test]
490 fn test_sqlite_error_maps_to_internal() {
491 let sqlite_err = rusqlite::Error::QueryReturnedNoRows;
492 let err = DynoxideError::from(sqlite_err);
493 assert_eq!(err.status_code(), 500);
494 assert!(err.error_type().contains("InternalServerError"));
495 }
496
497 #[test]
506 fn test_backend_error_envelopes_match_native() {
507 use crate::storage_backend::BackendError;
508
509 let v: DynoxideError = BackendError::Validation("too many tags".into()).into();
511 assert_eq!(v.status_code(), 400);
512 assert_eq!(
513 v.error_type(),
514 "com.amazon.coral.validate#ValidationException"
515 );
516
517 let u: DynoxideError = BackendError::Unsupported { capability: "ttl" }.into();
523 assert_eq!(u.status_code(), 501);
524 assert_eq!(u.error_type(), "com.dynoxide.wasm#UnsupportedOperation");
525 assert!(u.to_string().contains("'ttl' is not supported"));
526
527 for e in [
530 BackendError::NotADatabase,
531 BackendError::Locked,
532 BackendError::Constraint("constraint".into()),
533 BackendError::Io("io".into()),
534 BackendError::Other("sqlite-wasm: boom".into()),
535 ] {
536 let d: DynoxideError = e.into();
537 assert_eq!(d.status_code(), 500);
538 assert!(d.error_type().contains("InternalServerError"));
539 }
540 }
541
542 #[test]
543 fn test_error_response_json_structure() {
544 let err = DynoxideError::ValidationException("1 validation error detected".to_string());
545 let resp = err.to_response();
546 let json: serde_json::Value = serde_json::to_value(&resp).unwrap();
547
548 assert!(json.get("__type").is_some());
549 assert!(json.get("message").is_some());
550 assert_eq!(
551 json["__type"],
552 "com.amazon.coral.validate#ValidationException"
553 );
554 assert_eq!(json["message"], "1 validation error detected");
555 }
556
557 #[test]
558 fn test_short_error_codes() {
559 assert_eq!(
560 DynoxideError::ResourceNotFoundException("".into()).short_error_code(),
561 "ResourceNotFound"
562 );
563 assert_eq!(
564 DynoxideError::ValidationException("".into()).short_error_code(),
565 "ValidationError"
566 );
567 assert_eq!(
568 DynoxideError::ConditionalCheckFailedException("".into(), None).short_error_code(),
569 "ConditionalCheckFailed"
570 );
571 assert_eq!(
572 DynoxideError::DuplicateItemException("".into()).short_error_code(),
573 "DuplicateItem"
574 );
575 assert_eq!(
576 DynoxideError::InternalServerError("".into()).short_error_code(),
577 "InternalServerError"
578 );
579 }
580
581 #[test]
582 fn test_transaction_cancelled_json_has_cancellation_reasons() {
583 let reasons = vec![
584 CancellationReason {
585 code: "ConditionalCheckFailed".to_string(),
586 message: Some("The conditional request failed".to_string()),
587 item: None,
588 },
589 CancellationReason {
590 code: "None".to_string(),
591 message: None,
592 item: None,
593 },
594 ];
595 let err = DynoxideError::TransactionCanceledException(
596 "Transaction cancelled, please refer cancellation reasons for specific reasons [ConditionalCheckFailed, None]".to_string(),
597 reasons,
598 );
599 let json_str = err.to_json();
600 let json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
601
602 assert!(json.get("CancellationReasons").is_some());
604 let reasons = json["CancellationReasons"].as_array().unwrap();
605 assert_eq!(reasons.len(), 2);
606 assert_eq!(reasons[0]["Code"], "ConditionalCheckFailed");
607 assert_eq!(reasons[1]["Code"], "None");
608
609 assert!(json.get("Message").is_some());
611 assert!(json.get("message").is_none());
612 }
613
614 #[test]
615 fn test_backend_error_maps_to_internal() {
616 use crate::storage_backend::BackendError;
617 let err: DynoxideError = BackendError::Locked.into();
618 assert_eq!(err.status_code(), 500);
619 assert!(err.error_type().contains("InternalServerError"));
620 assert!(err.to_string().contains("locked"));
621 }
622}