use serde::Serialize;
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Default, Serialize)]
pub struct CancellationReason {
#[serde(rename = "Code")]
pub code: String,
#[serde(rename = "Message", skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(rename = "Item", skip_serializing_if = "Option::is_none")]
pub item: Option<HashMap<String, crate::types::AttributeValue>>,
}
#[derive(Debug, thiserror::Error)]
pub enum DynoxideError {
#[error("{0}")]
ResourceNotFoundException(String),
#[error("{0}")]
ResourceInUseException(String),
#[error("{0}")]
ValidationException(String),
#[error("{0}")]
ConditionalCheckFailedException(
String,
Option<HashMap<String, crate::types::AttributeValue>>,
),
#[error("{0}")]
TransactionCanceledException(String, Vec<CancellationReason>),
#[error("{0}")]
ItemCollectionSizeLimitExceededException(String),
#[error("{0}")]
DuplicateItemException(String),
#[error("{0}")]
ProvisionedThroughputExceededException(String),
#[error("{0}")]
SerializationException(String),
#[error("{0}")]
LimitExceededException(String),
#[error("{0}")]
AccessDeniedException(String),
#[error("{0}")]
IdempotentParameterMismatchException(String),
#[error("{0}")]
InternalServerError(String),
#[error("Conversion error: {0}")]
ConversionError(#[from] crate::types::ConversionError),
#[error("Internal error: {0}")]
SqliteError(#[from] rusqlite::Error),
}
impl DynoxideError {
pub fn error_type(&self) -> &'static str {
match self {
DynoxideError::ResourceNotFoundException(_) => {
"com.amazonaws.dynamodb.v20120810#ResourceNotFoundException"
}
DynoxideError::ResourceInUseException(_) => {
"com.amazonaws.dynamodb.v20120810#ResourceInUseException"
}
DynoxideError::ValidationException(_) => {
"com.amazon.coral.validate#ValidationException"
}
DynoxideError::ConditionalCheckFailedException(..) => {
"com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException"
}
DynoxideError::TransactionCanceledException(..) => {
"com.amazonaws.dynamodb.v20120810#TransactionCanceledException"
}
DynoxideError::DuplicateItemException(_) => {
"com.amazonaws.dynamodb.v20120810#DuplicateItemException"
}
DynoxideError::ItemCollectionSizeLimitExceededException(_) => {
"com.amazonaws.dynamodb.v20120810#ItemCollectionSizeLimitExceededException"
}
DynoxideError::ProvisionedThroughputExceededException(_) => {
"com.amazonaws.dynamodb.v20120810#ProvisionedThroughputExceededException"
}
DynoxideError::SerializationException(_) => {
"com.amazon.coral.service#SerializationException"
}
DynoxideError::LimitExceededException(_) => {
"com.amazonaws.dynamodb.v20120810#LimitExceededException"
}
DynoxideError::AccessDeniedException(_) => {
"com.amazonaws.dynamodb.v20120810#AccessDeniedException"
}
DynoxideError::IdempotentParameterMismatchException(_) => {
"com.amazonaws.dynamodb.v20120810#IdempotentParameterMismatchException"
}
DynoxideError::ConversionError(_) => "com.amazon.coral.validate#ValidationException",
DynoxideError::InternalServerError(_) | DynoxideError::SqliteError(_) => {
"com.amazonaws.dynamodb.v20120810#InternalServerError"
}
}
}
pub fn short_error_code(&self) -> &'static str {
match self {
DynoxideError::ResourceNotFoundException(_) => "ResourceNotFound",
DynoxideError::ResourceInUseException(_) => "ResourceInUse",
DynoxideError::ValidationException(_) | DynoxideError::ConversionError(_) => {
"ValidationError"
}
DynoxideError::ConditionalCheckFailedException(..) => "ConditionalCheckFailed",
DynoxideError::TransactionCanceledException(..) => "TransactionConflict",
DynoxideError::DuplicateItemException(_) => "DuplicateItem",
DynoxideError::ItemCollectionSizeLimitExceededException(_) => {
"ItemCollectionSizeLimitExceeded"
}
DynoxideError::ProvisionedThroughputExceededException(_) => {
"ProvisionedThroughputExceeded"
}
DynoxideError::AccessDeniedException(_) => "AccessDenied",
DynoxideError::IdempotentParameterMismatchException(_) => "IdempotentParameterMismatch",
DynoxideError::SerializationException(_) => "SerializationError",
DynoxideError::LimitExceededException(_) => "RequestLimitExceeded",
DynoxideError::InternalServerError(_) | DynoxideError::SqliteError(_) => {
"InternalServerError"
}
}
}
pub fn status_code(&self) -> u16 {
match self {
DynoxideError::InternalServerError(_) | DynoxideError::SqliteError(_) => 500,
_ => 400,
}
}
pub fn to_response(&self) -> ErrorResponse {
let item = if let DynoxideError::ConditionalCheckFailedException(_, item) = self {
item.clone()
} else {
None
};
ErrorResponse {
error_type: self.error_type().to_string(),
message: self.to_string(),
item,
}
}
pub fn to_json(&self) -> String {
let error_type = self.error_type();
let message = self.to_string();
match self {
DynoxideError::TransactionCanceledException(_, reasons) => {
let mut m = serde_json::Map::new();
m.insert(
"__type".to_string(),
serde_json::Value::String(error_type.to_string()),
);
m.insert("Message".to_string(), serde_json::Value::String(message));
if let Ok(reasons_val) = serde_json::to_value(reasons) {
m.insert("CancellationReasons".to_string(), reasons_val);
}
serde_json::to_string(&m).unwrap_or_default()
}
DynoxideError::SerializationException(_) => {
let mut m = serde_json::Map::new();
m.insert(
"__type".to_string(),
serde_json::Value::String(error_type.to_string()),
);
m.insert("Message".to_string(), serde_json::Value::String(message));
serde_json::to_string(&m).unwrap_or_default()
}
_ => {
let resp = self.to_response();
serde_json::to_string(&resp).unwrap_or_default()
}
}
}
}
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
#[serde(rename = "__type")]
pub error_type: String,
#[serde(rename = "message")]
pub message: String,
#[serde(rename = "Item", skip_serializing_if = "Option::is_none")]
pub item: Option<HashMap<String, crate::types::AttributeValue>>,
}
impl fmt::Display for ErrorResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", serde_json::to_string(self).unwrap_or_default())
}
}
pub type Result<T> = std::result::Result<T, DynoxideError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_response_format() {
let err = DynoxideError::ResourceNotFoundException(
"Requested resource not found: Table: NonExistent not found".to_string(),
);
let resp = err.to_response();
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"__type\""));
assert!(json.contains("ResourceNotFoundException"));
assert!(json.contains("NonExistent not found"));
}
#[test]
fn test_status_codes() {
assert_eq!(
DynoxideError::ResourceNotFoundException("".into()).status_code(),
400
);
assert_eq!(
DynoxideError::ResourceInUseException("".into()).status_code(),
400
);
assert_eq!(
DynoxideError::ValidationException("".into()).status_code(),
400
);
assert_eq!(
DynoxideError::ConditionalCheckFailedException("".into(), None).status_code(),
400
);
assert_eq!(
DynoxideError::TransactionCanceledException("".into(), vec![]).status_code(),
400
);
assert_eq!(
DynoxideError::InternalServerError("".into()).status_code(),
500
);
}
#[test]
fn test_error_type_strings() {
let err = DynoxideError::ValidationException("bad input".into());
assert_eq!(
err.error_type(),
"com.amazon.coral.validate#ValidationException"
);
}
#[test]
fn test_sqlite_error_maps_to_internal() {
let sqlite_err = rusqlite::Error::QueryReturnedNoRows;
let err = DynoxideError::from(sqlite_err);
assert_eq!(err.status_code(), 500);
assert!(err.error_type().contains("InternalServerError"));
}
#[test]
fn test_error_response_json_structure() {
let err = DynoxideError::ValidationException("1 validation error detected".to_string());
let resp = err.to_response();
let json: serde_json::Value = serde_json::to_value(&resp).unwrap();
assert!(json.get("__type").is_some());
assert!(json.get("message").is_some());
assert_eq!(
json["__type"],
"com.amazon.coral.validate#ValidationException"
);
assert_eq!(json["message"], "1 validation error detected");
}
#[test]
fn test_short_error_codes() {
assert_eq!(
DynoxideError::ResourceNotFoundException("".into()).short_error_code(),
"ResourceNotFound"
);
assert_eq!(
DynoxideError::ValidationException("".into()).short_error_code(),
"ValidationError"
);
assert_eq!(
DynoxideError::ConditionalCheckFailedException("".into(), None).short_error_code(),
"ConditionalCheckFailed"
);
assert_eq!(
DynoxideError::DuplicateItemException("".into()).short_error_code(),
"DuplicateItem"
);
assert_eq!(
DynoxideError::InternalServerError("".into()).short_error_code(),
"InternalServerError"
);
}
#[test]
fn test_transaction_cancelled_json_has_cancellation_reasons() {
let reasons = vec![
CancellationReason {
code: "ConditionalCheckFailed".to_string(),
message: Some("The conditional request failed".to_string()),
item: None,
},
CancellationReason {
code: "None".to_string(),
message: None,
item: None,
},
];
let err = DynoxideError::TransactionCanceledException(
"Transaction cancelled, please refer cancellation reasons for specific reasons [ConditionalCheckFailed, None]".to_string(),
reasons,
);
let json_str = err.to_json();
let json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
assert!(json.get("CancellationReasons").is_some());
let reasons = json["CancellationReasons"].as_array().unwrap();
assert_eq!(reasons.len(), 2);
assert_eq!(reasons[0]["Code"], "ConditionalCheckFailed");
assert_eq!(reasons[1]["Code"], "None");
assert!(json.get("Message").is_some());
assert!(json.get("message").is_none());
}
}