Skip to main content

rustack_ssm_model/
error.rs

1//! SSM error types.
2//!
3//! SSM errors use JSON format with a `__type` field containing the
4//! short error type name (e.g., `ParameterNotFound`), unlike DynamoDB
5//! which uses fully-qualified names.
6
7use std::fmt;
8
9/// Well-known SSM error codes.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
11#[non_exhaustive]
12pub enum SsmErrorCode {
13    /// Parameter not found.
14    ParameterNotFound,
15    /// Parameter already exists (PutParameter without Overwrite).
16    ParameterAlreadyExists,
17    /// Maximum version limit exceeded (100 versions per parameter).
18    ParameterMaxVersionLimitExceeded,
19    /// Requested parameter version not found.
20    ParameterVersionNotFound,
21    /// Too many labels on a parameter version (max 10).
22    ParameterVersionLabelLimitExceeded,
23    /// Hierarchy level limit exceeded (max 15 levels).
24    HierarchyLevelLimitExceeded,
25    /// Type mismatch for a parameter in a hierarchy.
26    HierarchyTypeMismatch,
27    /// Invalid allowed pattern regex.
28    InvalidAllowedPatternException,
29    /// Value does not match the allowed pattern.
30    ParameterPatternMismatchException,
31    /// Invalid filter key.
32    InvalidFilterKey,
33    /// Invalid filter option.
34    InvalidFilterOption,
35    /// Invalid filter value.
36    InvalidFilterValue,
37    /// Invalid next token for pagination.
38    InvalidNextToken,
39    /// Invalid resource ID.
40    InvalidResourceId,
41    /// Invalid resource type.
42    InvalidResourceType,
43    /// Unsupported parameter type.
44    UnsupportedParameterType,
45    /// Too many tags (max 50 per resource).
46    TooManyTagsError,
47    /// Internal server error.
48    InternalServerError,
49    /// Invalid security (encryption key issue).
50    InvalidSecurity,
51    /// Invalid action (unrecognized operation).
52    InvalidAction,
53    /// Missing action header.
54    MissingAction,
55    /// Validation error.
56    #[default]
57    ValidationException,
58}
59
60impl SsmErrorCode {
61    /// Returns the short error type string for the JSON `__type` field.
62    ///
63    /// SSM uses short names like `"ParameterNotFound"`, not fully-qualified
64    /// names like DynamoDB.
65    #[must_use]
66    pub fn error_type(&self) -> &'static str {
67        self.as_str()
68    }
69
70    /// Returns the short error code string.
71    #[must_use]
72    pub fn as_str(&self) -> &'static str {
73        match self {
74            Self::ParameterNotFound => "ParameterNotFound",
75            Self::ParameterAlreadyExists => "ParameterAlreadyExists",
76            Self::ParameterMaxVersionLimitExceeded => "ParameterMaxVersionLimitExceeded",
77            Self::ParameterVersionNotFound => "ParameterVersionNotFound",
78            Self::ParameterVersionLabelLimitExceeded => "ParameterVersionLabelLimitExceeded",
79            Self::HierarchyLevelLimitExceeded => "HierarchyLevelLimitExceeded",
80            Self::HierarchyTypeMismatch => "HierarchyTypeMismatch",
81            Self::InvalidAllowedPatternException => "InvalidAllowedPatternException",
82            Self::ParameterPatternMismatchException => "ParameterPatternMismatchException",
83            Self::InvalidFilterKey => "InvalidFilterKey",
84            Self::InvalidFilterOption => "InvalidFilterOption",
85            Self::InvalidFilterValue => "InvalidFilterValue",
86            Self::InvalidNextToken => "InvalidNextToken",
87            Self::InvalidResourceId => "InvalidResourceId",
88            Self::InvalidResourceType => "InvalidResourceType",
89            Self::UnsupportedParameterType => "UnsupportedParameterType",
90            Self::TooManyTagsError => "TooManyTagsError",
91            Self::InternalServerError => "InternalServerError",
92            Self::InvalidSecurity => "InvalidSecurity",
93            Self::InvalidAction => "InvalidAction",
94            Self::MissingAction => "MissingAction",
95            Self::ValidationException => "ValidationException",
96        }
97    }
98
99    /// Returns the default HTTP status code for this error.
100    #[must_use]
101    pub fn default_status_code(&self) -> http::StatusCode {
102        match self {
103            Self::InternalServerError => http::StatusCode::INTERNAL_SERVER_ERROR,
104            _ => http::StatusCode::BAD_REQUEST,
105        }
106    }
107}
108
109impl fmt::Display for SsmErrorCode {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        f.write_str(self.as_str())
112    }
113}
114
115/// An SSM error response.
116#[derive(Debug)]
117pub struct SsmError {
118    /// The error code.
119    pub code: SsmErrorCode,
120    /// A human-readable error message.
121    pub message: String,
122    /// The HTTP status code.
123    pub status_code: http::StatusCode,
124    /// The underlying source error, if any.
125    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
126}
127
128impl fmt::Display for SsmError {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(f, "SsmError({}): {}", self.code, self.message)
131    }
132}
133
134impl std::error::Error for SsmError {
135    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
136        self.source
137            .as_ref()
138            .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
139    }
140}
141
142impl SsmError {
143    /// Create a new `SsmError` from an error code.
144    #[must_use]
145    pub fn new(code: SsmErrorCode) -> Self {
146        Self {
147            status_code: code.default_status_code(),
148            message: code.as_str().to_owned(),
149            code,
150            source: None,
151        }
152    }
153
154    /// Create a new `SsmError` with a custom message.
155    #[must_use]
156    pub fn with_message(code: SsmErrorCode, message: impl Into<String>) -> Self {
157        Self {
158            status_code: code.default_status_code(),
159            message: message.into(),
160            code,
161            source: None,
162        }
163    }
164
165    /// Set the source error.
166    #[must_use]
167    pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
168        self.source = Some(Box::new(source));
169        self
170    }
171
172    /// Returns the `__type` string for the JSON error response.
173    #[must_use]
174    pub fn error_type(&self) -> &'static str {
175        self.code.error_type()
176    }
177
178    // -- Convenience constructors --
179
180    /// Parameter not found.
181    #[must_use]
182    pub fn parameter_not_found(name: &str) -> Self {
183        Self::with_message(SsmErrorCode::ParameterNotFound, name.to_owned())
184    }
185
186    /// Parameter already exists.
187    #[must_use]
188    pub fn parameter_already_exists(name: &str) -> Self {
189        Self::with_message(
190            SsmErrorCode::ParameterAlreadyExists,
191            format!(
192                "The parameter {name} already exists. To overwrite this value, set the overwrite \
193                 option in the request to true."
194            ),
195        )
196    }
197
198    /// Validation error.
199    #[must_use]
200    pub fn validation(message: impl Into<String>) -> Self {
201        Self::with_message(SsmErrorCode::ValidationException, message)
202    }
203
204    /// Internal server error.
205    #[must_use]
206    pub fn internal_error(message: impl Into<String>) -> Self {
207        Self::with_message(SsmErrorCode::InternalServerError, message)
208    }
209
210    /// Missing action header.
211    #[must_use]
212    pub fn missing_action() -> Self {
213        Self::with_message(
214            SsmErrorCode::MissingAction,
215            "Missing required header: X-Amz-Target",
216        )
217    }
218
219    /// Unknown operation.
220    #[must_use]
221    pub fn unknown_operation(target: &str) -> Self {
222        Self::with_message(
223            SsmErrorCode::InvalidAction,
224            format!(
225                "Operation {target} is not supported. Only Parameter Store operations are \
226                 implemented."
227            ),
228        )
229    }
230
231    /// Not implemented.
232    #[must_use]
233    pub fn not_implemented(operation: &str) -> Self {
234        Self::with_message(
235            SsmErrorCode::InternalServerError,
236            format!("Operation {operation} is not yet implemented"),
237        )
238    }
239}
240
241/// Create an `SsmError` from an error code.
242///
243/// # Examples
244///
245/// ```
246/// use rustack_ssm_model::ssm_error;
247/// use rustack_ssm_model::error::SsmErrorCode;
248///
249/// let err = ssm_error!(ValidationException);
250/// assert_eq!(err.code, SsmErrorCode::ValidationException);
251///
252/// let err = ssm_error!(ParameterNotFound, "Parameter /my/param not found");
253/// assert_eq!(err.message, "Parameter /my/param not found");
254/// ```
255#[macro_export]
256macro_rules! ssm_error {
257    ($code:ident) => {
258        $crate::error::SsmError::new($crate::error::SsmErrorCode::$code)
259    };
260    ($code:ident, $msg:expr) => {
261        $crate::error::SsmError::with_message($crate::error::SsmErrorCode::$code, $msg)
262    };
263}