dynamodb_facade/error.rs
1use aws_sdk_dynamodb::types::WriteRequest;
2use thiserror::Error;
3
4/// A specialized [`Result`](core::result::Result) type for this crate.
5///
6/// All fallible operations in `dynamodb-facade` return this type.
7///
8/// # Examples
9///
10/// ```
11/// use dynamodb_facade::{Error, Result};
12///
13/// fn validate_role(role: &str) -> Result<()> {
14/// if role.is_empty() {
15/// return Err(Error::custom("role must not be empty"));
16/// }
17/// Ok(())
18/// }
19///
20/// assert!(validate_role("student").is_ok());
21/// assert!(validate_role("").is_err());
22/// ```
23pub type Result<T> = core::result::Result<T, Error>;
24
25/// The error type for all `dynamodb-facade` operations.
26///
27/// Wraps the various failure modes that can occur when interacting with
28/// DynamoDB: SDK-level transport and service errors, serialization failures,
29/// and application-defined errors.
30///
31/// # Variants
32///
33/// - [`Error::DynamoDB`] — an error originating from the AWS SDK, such as a
34/// `ConditionalCheckFailedException`, a throttling error, or a network
35/// failure. Use [`Error::as_dynamodb_error`] to inspect the underlying
36/// [`aws_sdk_dynamodb::Error`].
37/// - [`Error::Serde`] — a (de)serialization failure produced by
38/// [`serde_dynamo`] when converting between Rust types and DynamoDB items.
39/// - [`Error::Other`] — any other boxed [`core::error::Error`]. Useful for
40/// wrapping domain errors via [`Error::other`].
41/// - [`Error::FailedBatchWrite`] — a batch write that could not complete
42/// after all retry attempts. Contains the unprocessed [`WriteRequest`]s.
43/// - [`Error::Custom`] — a plain string error message. Useful for quick
44/// ad-hoc errors via [`Error::custom`].
45///
46/// # Examples
47///
48/// Matching on error variants:
49///
50/// ```
51/// use dynamodb_facade::Error;
52///
53/// fn handle(err: Error) {
54/// match err {
55/// Error::DynamoDB(_) => eprintln!("AWS SDK error"),
56/// Error::Serde(_) => eprintln!("serialization error"),
57/// Error::FailedBatchWrite(r) => eprintln!("{} items unprocessed", r.len()),
58/// Error::Other(_) => eprintln!("other error"),
59/// Error::Custom(msg) => eprintln!("custom error: {msg}"),
60/// }
61/// }
62/// ```
63#[derive(Debug, Error)]
64pub enum Error {
65 /// An error returned by the AWS DynamoDB SDK.
66 ///
67 /// This variant is produced automatically via the [`From`] impls for
68 /// [`aws_sdk_dynamodb::error::SdkError`] and [`aws_sdk_dynamodb::Error`].
69 /// Use [`Error::as_dynamodb_error`] to borrow the inner error for
70 /// pattern-matching on specific service errors such as
71 /// `ConditionalCheckFailedException`.
72 #[error(transparent)]
73 DynamoDB(Box<aws_sdk_dynamodb::Error>),
74
75 /// A (de)serialization error from [`serde_dynamo`].
76 ///
77 /// Produced when converting a Rust struct to or from a DynamoDB item map
78 /// fails — for example, when a required attribute is missing or has an
79 /// unexpected type.
80 #[error(transparent)]
81 Serde(#[from] serde_dynamo::Error),
82
83 /// A batch write that did not complete after all retry attempts.
84 ///
85 /// Returned by [`dynamodb_batch_write`](crate::dynamodb_batch_write) when
86 /// some [`WriteRequest`]s remain unprocessed after the maximum number of
87 /// retries. The contained vector holds the requests that were never
88 /// acknowledged by DynamoDB, allowing the caller to inspect or retry them.
89 #[error("BatchWriteItem failure: {len} items", len = .0.len())]
90 FailedBatchWrite(Vec<WriteRequest>),
91
92 /// Any other boxed error.
93 ///
94 /// Use [`Error::other`] to wrap an arbitrary [`core::error::Error`] value
95 /// into this variant.
96 #[error(transparent)]
97 Other(#[from] Box<dyn core::error::Error + Send>),
98
99 /// A plain string error message.
100 ///
101 /// Use [`Error::custom`] to construct this variant.
102 #[error("Custom Error: {0}")]
103 Custom(String),
104}
105
106impl Error {
107 /// Creates an [`Error::Custom`] from any value that converts into a
108 /// [`String`].
109 ///
110 /// This is a convenience constructor for quick ad-hoc errors without
111 /// needing to define a dedicated error type.
112 ///
113 /// # Examples
114 ///
115 /// ```
116 /// use dynamodb_facade::Error;
117 ///
118 /// let err = Error::custom("enrollment limit reached");
119 /// assert!(matches!(err, Error::Custom(_)));
120 /// assert_eq!(err.to_string(), "Custom Error: enrollment limit reached");
121 /// ```
122 pub fn custom(message: impl Into<String>) -> Self {
123 Self::Custom(message.into())
124 }
125
126 /// Creates an [`Error::Other`] by boxing any [`core::error::Error`] value.
127 ///
128 /// Use this to wrap domain-specific or standard-library errors when
129 /// implementing fallible methods from this crate — for example, a
130 /// [`FromStr`](std::str::FromStr) parse error inside a manual
131 /// [`DynamoDBItem::try_from_item`](crate::DynamoDBItem::try_from_item)
132 /// implementation.
133 ///
134 /// # Examples
135 ///
136 /// Wrapping a [`ParseIntError`](std::num::ParseIntError) when deserializing
137 /// a DynamoDB string attribute into a numeric field:
138 ///
139 /// ```
140 /// use dynamodb_facade::Error;
141 ///
142 /// fn parse_credits(raw: &str) -> dynamodb_facade::Result<u32> {
143 /// raw.parse::<u32>().map_err(Error::other)
144 /// }
145 ///
146 /// assert!(parse_credits("42").is_ok());
147 /// assert!(matches!(parse_credits("not-a-number"), Err(Error::Other(_))));
148 /// ```
149 pub fn other(error: impl core::error::Error + Send + Sync + 'static) -> Self {
150 Self::Other(Box::new(error))
151 }
152
153 /// Returns a reference to the inner [`aws_sdk_dynamodb::Error`] if this
154 /// error is the [`Error::DynamoDB`] variant, or [`None`] otherwise.
155 ///
156 /// Use this to inspect or pattern-match on specific DynamoDB service
157 /// errors (e.g. `ConditionalCheckFailedException`, `ResourceNotFoundException`)
158 /// without unwrapping the full error chain.
159 ///
160 /// # Examples
161 ///
162 /// Distinguishing a "not found" condition failure from other errors when
163 /// deleting an enrollment that must already exist:
164 ///
165 /// ```no_run
166 /// # use dynamodb_facade::{DynamoDBItemOp, DynamoDBError, KeyId};
167 /// # use dynamodb_facade::test_fixtures::*;
168 /// # async fn example(
169 /// # client: dynamodb_facade::Client,
170 /// # user_id: &str,
171 /// # course_id: &str,
172 /// # ) -> Result<Enrollment, String> {
173 /// match Enrollment::delete_by_id(client, KeyId::pk(user_id).sk(course_id))
174 /// .exists()
175 /// .await
176 /// {
177 /// Ok(enrollment) => Ok(enrollment.expect("exists guard guarantees a return value")),
178 /// Err(e) if matches!(
179 /// e.as_dynamodb_error(),
180 /// Some(DynamoDBError::ConditionalCheckFailedException(_))
181 /// ) => Err(format!("enrollment for user {user_id} / course {course_id} not found")),
182 /// Err(e) => Err(format!("unexpected error: {e}")),
183 /// }
184 /// # }
185 /// ```
186 pub fn as_dynamodb_error(&self) -> Option<&aws_sdk_dynamodb::Error> {
187 match self {
188 Self::DynamoDB(e) => Some(e),
189 _ => None,
190 }
191 }
192}
193
194/// Converts an [`aws_sdk_dynamodb::error::SdkError`] into [`Error::DynamoDB`].
195///
196/// This impl is provided for all `SdkError<T, R>` where the SDK can convert
197/// the operation-specific error into the generic [`aws_sdk_dynamodb::Error`].
198/// It allows the `?` operator to be used directly on SDK call results.
199impl<T, R> From<aws_sdk_dynamodb::error::SdkError<T, R>> for Error
200where
201 aws_sdk_dynamodb::Error: From<aws_sdk_dynamodb::error::SdkError<T, R>>,
202{
203 fn from(value: aws_sdk_dynamodb::error::SdkError<T, R>) -> Self {
204 Self::DynamoDB(Box::new(value.into()))
205 }
206}
207
208/// Converts an [`aws_sdk_dynamodb::Error`] into [`Error::DynamoDB`].
209///
210/// Boxes the SDK error and wraps it in the [`Error::DynamoDB`] variant.
211impl From<aws_sdk_dynamodb::Error> for Error {
212 fn from(value: aws_sdk_dynamodb::Error) -> Self {
213 Self::DynamoDB(Box::new(value))
214 }
215}