aws_lambda_events/event/sns/
mod.rs

1use chrono::{DateTime, Utc};
2use serde::{de::DeserializeOwned, Deserialize, Serialize};
3#[cfg(feature = "catch-all-fields")]
4use serde_json::Value;
5use std::collections::HashMap;
6
7use crate::custom_serde::{deserialize_lambda_map, deserialize_nullish_boolean};
8
9/// The `Event` notification event handled by Lambda
10///
11/// [https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html](https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html)
12#[non_exhaustive]
13#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
14#[serde(rename_all = "PascalCase")]
15pub struct SnsEvent {
16    pub records: Vec<SnsRecord>,
17
18    /// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
19    /// Enabled with Cargo feature `catch-all-fields`.
20    /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored.
21    #[cfg(feature = "catch-all-fields")]
22    #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))]
23    #[serde(flatten)]
24    pub other: serde_json::Map<String, Value>,
25}
26
27/// SnsRecord stores information about each record of a SNS event
28#[non_exhaustive]
29#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
30#[serde(rename_all = "PascalCase")]
31pub struct SnsRecord {
32    /// A string containing the event source.
33    pub event_source: String,
34
35    /// A string containing the event version.
36    pub event_version: String,
37
38    /// A string containing the event subscription ARN.
39    pub event_subscription_arn: String,
40
41    /// An SNS object representing the SNS message.
42    pub sns: SnsMessage,
43
44    /// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
45    /// Enabled with Cargo feature `catch-all-fields`.
46    /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored.
47    #[cfg(feature = "catch-all-fields")]
48    #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))]
49    #[serde(flatten)]
50    pub other: serde_json::Map<String, Value>,
51}
52
53/// SnsMessage stores information about each record of a SNS event
54#[non_exhaustive]
55#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
56#[serde(rename_all = "PascalCase")]
57pub struct SnsMessage {
58    /// The type of SNS message. For a lambda event, this should always be **Notification**
59    #[serde(rename = "Type")]
60    pub sns_message_type: String,
61
62    /// A Universally Unique Identifier, unique for each message published. For a notification that Amazon SNS resends during a retry, the message ID of the original message is used.
63    pub message_id: String,
64
65    /// The Amazon Resource Name (ARN) for the topic that this message was published to.
66    pub topic_arn: String,
67
68    /// The Subject parameter specified when the notification was published to the topic.
69    ///
70    /// The SNS Developer Guide states: *This is an optional parameter. If no Subject was specified, then this name-value pair does not appear in this JSON document.*
71    ///
72    /// Preliminary tests show this appears in the lambda event JSON as `Subject: null`, marking as Option with need to test additional scenarios
73    #[serde(default)]
74    pub subject: Option<String>,
75
76    /// The time (UTC) when the notification was published.
77    pub timestamp: DateTime<Utc>,
78
79    /// Version of the Amazon SNS signature used.
80    pub signature_version: String,
81
82    /// Base64-encoded SHA1withRSA signature of the Message, MessageId, Subject (if present), Type, Timestamp, and TopicArn values.
83    pub signature: String,
84
85    /// The URL to the certificate that was used to sign the message.
86    #[serde(alias = "SigningCertURL")]
87    pub signing_cert_url: String,
88
89    /// A URL that you can use to unsubscribe the endpoint from this topic. If you visit this URL, Amazon SNS unsubscribes the endpoint and stops sending notifications to this endpoint.
90    #[serde(alias = "UnsubscribeURL")]
91    pub unsubscribe_url: String,
92
93    /// The Message value specified when the notification was published to the topic.
94    pub message: String,
95
96    /// This is a HashMap of defined attributes for a message. Additional details can be found in the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html)
97    #[serde(deserialize_with = "deserialize_lambda_map")]
98    #[serde(default)]
99    pub message_attributes: HashMap<String, MessageAttribute>,
100
101    /// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
102    /// Enabled with Cargo feature `catch-all-fields`.
103    /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored.
104    #[cfg(feature = "catch-all-fields")]
105    #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))]
106    #[serde(flatten)]
107    pub other: serde_json::Map<String, Value>,
108}
109
110/// An alternate `Event` notification event to use alongside `SnsRecordObj<T>` and `SnsMessageObj<T>` if you want to deserialize an object inside your SNS messages rather than getting an `Option<String>` message
111///
112/// [https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html](https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html)
113#[non_exhaustive]
114#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
115#[serde(rename_all = "PascalCase")]
116#[serde(bound(deserialize = "T: DeserializeOwned"))]
117pub struct SnsEventObj<T: Serialize> {
118    pub records: Vec<SnsRecordObj<T>>,
119
120    /// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
121    /// Enabled with Cargo feature `catch-all-fields`.
122    /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored.
123    #[cfg(feature = "catch-all-fields")]
124    #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))]
125    #[serde(flatten)]
126    pub other: serde_json::Map<String, Value>,
127}
128
129/// Alternative to `SnsRecord`, used alongside `SnsEventObj<T>` and `SnsMessageObj<T>` when deserializing nested objects from within SNS messages)
130#[non_exhaustive]
131#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
132#[serde(rename_all = "PascalCase")]
133#[serde(bound(deserialize = "T: DeserializeOwned"))]
134pub struct SnsRecordObj<T: Serialize> {
135    /// A string containing the event source.
136    pub event_source: String,
137
138    /// A string containing the event version.
139    pub event_version: String,
140
141    /// A string containing the event subscription ARN.
142    pub event_subscription_arn: String,
143
144    /// An SNS object representing the SNS message.
145    pub sns: SnsMessageObj<T>,
146
147    /// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
148    /// Enabled with Cargo feature `catch-all-fields`.
149    /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored.
150    #[cfg(feature = "catch-all-fields")]
151    #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))]
152    #[serde(flatten)]
153    pub other: serde_json::Map<String, Value>,
154}
155
156/// Alternate version of `SnsMessage` to use in conjunction with `SnsEventObj<T>` and `SnsRecordObj<T>` for deserializing the message into a struct of type `T`
157#[non_exhaustive]
158#[serde_with::serde_as]
159#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
160#[serde(rename_all = "PascalCase")]
161#[serde(bound(deserialize = "T: DeserializeOwned"))]
162pub struct SnsMessageObj<T: Serialize> {
163    /// The type of SNS message. For a lambda event, this should always be **Notification**
164    #[serde(rename = "Type")]
165    pub sns_message_type: String,
166
167    /// A Universally Unique Identifier, unique for each message published. For a notification that Amazon SNS resends during a retry, the message ID of the original message is used.
168    pub message_id: String,
169
170    /// The Amazon Resource Name (ARN) for the topic that this message was published to.
171    pub topic_arn: String,
172
173    /// The Subject parameter specified when the notification was published to the topic.
174    ///
175    /// The SNS Developer Guide states: *This is an optional parameter. If no Subject was specified, then this name-value pair does not appear in this JSON document.*
176    ///
177    /// Preliminary tests show this appears in the lambda event JSON as `Subject: null`, marking as Option with need to test additional scenarios
178    #[serde(default)]
179    pub subject: Option<String>,
180
181    /// The time (UTC) when the notification was published.
182    pub timestamp: DateTime<Utc>,
183
184    /// Version of the Amazon SNS signature used.
185    pub signature_version: String,
186
187    /// Base64-encoded SHA1withRSA signature of the Message, MessageId, Subject (if present), Type, Timestamp, and TopicArn values.
188    pub signature: String,
189
190    /// The URL to the certificate that was used to sign the message.
191    #[serde(alias = "SigningCertURL")]
192    pub signing_cert_url: String,
193
194    /// A URL that you can use to unsubscribe the endpoint from this topic. If you visit this URL, Amazon SNS unsubscribes the endpoint and stops sending notifications to this endpoint.
195    #[serde(alias = "UnsubscribeURL")]
196    pub unsubscribe_url: String,
197
198    /// Deserialized into a `T` from nested JSON inside the SNS message string. `T` must implement the `Deserialize` or `DeserializeOwned` trait.
199    #[serde_as(as = "serde_with::json::JsonString")]
200    #[serde(bound(deserialize = "T: DeserializeOwned"))]
201    pub message: T,
202
203    /// This is a HashMap of defined attributes for a message. Additional details can be found in the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html)
204    #[serde(deserialize_with = "deserialize_lambda_map")]
205    #[serde(default)]
206    pub message_attributes: HashMap<String, MessageAttribute>,
207
208    /// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
209    /// Enabled with Cargo feature `catch-all-fields`.
210    /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored.
211    #[cfg(feature = "catch-all-fields")]
212    #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))]
213    #[serde(flatten)]
214    pub other: serde_json::Map<String, Value>,
215}
216
217/// Structured metadata items (such as timestamps, geospatial data, signatures, and identifiers) about the message.
218///
219/// Message attributes are optional and separate from—but are sent together with—the message body. The receiver can use this information to decide how to handle the message without having to process the message body first.
220///
221/// Additional details can be found in the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html)
222#[non_exhaustive]
223#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
224pub struct MessageAttribute {
225    /// The data type of the attribute. Per the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html), lambda notifications, this will only be **String** or **Binary**.
226    #[serde(rename = "Type")]
227    pub data_type: String,
228
229    /// The user-specified message attribute value.
230    #[serde(rename = "Value")]
231    pub value: String,
232
233    /// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
234    /// Enabled with Cargo feature `catch-all-fields`.
235    /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored.
236    #[cfg(feature = "catch-all-fields")]
237    #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))]
238    #[serde(flatten)]
239    pub other: serde_json::Map<String, Value>,
240}
241
242#[non_exhaustive]
243#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
244#[serde(rename_all = "PascalCase")]
245pub struct CloudWatchAlarmPayload {
246    pub alarm_name: String,
247    pub alarm_description: String,
248    #[serde(rename = "AWSAccountId")]
249    pub aws_account_id: String,
250    pub new_state_value: String,
251    pub new_state_reason: String,
252    pub state_change_time: String,
253    pub region: String,
254    pub alarm_arn: String,
255    pub old_state_value: String,
256    pub trigger: CloudWatchAlarmTrigger,
257    /// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
258    /// Enabled with Cargo feature `catch-all-fields`.
259    /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored.
260    #[cfg(feature = "catch-all-fields")]
261    #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))]
262    #[serde(flatten)]
263    pub other: serde_json::Map<String, Value>,
264}
265
266#[non_exhaustive]
267#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
268#[serde(rename_all = "PascalCase")]
269pub struct CloudWatchAlarmTrigger {
270    pub period: i64,
271    pub evaluation_periods: i64,
272    pub comparison_operator: String,
273    pub threshold: f64,
274    pub treat_missing_data: String,
275    pub evaluate_low_sample_count_percentile: String,
276    #[serde(default)]
277    pub metrics: Vec<CloudWatchMetricDataQuery>,
278    pub metric_name: Option<String>,
279    pub namespace: Option<String>,
280    pub statistic_type: Option<String>,
281    pub statistic: Option<String>,
282    pub unit: Option<String>,
283    #[serde(default)]
284    pub dimensions: Vec<CloudWatchDimension>,
285    /// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
286    /// Enabled with Cargo feature `catch-all-fields`.
287    /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored.
288    #[cfg(feature = "catch-all-fields")]
289    #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))]
290    #[serde(flatten)]
291    pub other: serde_json::Map<String, Value>,
292}
293
294#[non_exhaustive]
295#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
296#[serde(rename_all = "PascalCase")]
297pub struct CloudWatchMetricDataQuery {
298    pub id: String,
299    pub expression: Option<String>,
300    pub label: Option<String>,
301    pub metric_stat: Option<CloudWatchMetricStat>,
302    pub period: Option<i64>,
303    #[serde(default, deserialize_with = "deserialize_nullish_boolean")]
304    pub return_data: bool,
305    /// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
306    /// Enabled with Cargo feature `catch-all-fields`.
307    /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored.
308    #[cfg(feature = "catch-all-fields")]
309    #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))]
310    #[serde(flatten)]
311    pub other: serde_json::Map<String, Value>,
312}
313
314#[non_exhaustive]
315#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
316#[serde(rename_all = "PascalCase")]
317pub struct CloudWatchMetricStat {
318    pub metric: CloudWatchMetric,
319    pub period: i64,
320    pub stat: String,
321    pub unit: Option<String>,
322    /// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
323    /// Enabled with Cargo feature `catch-all-fields`.
324    /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored.
325    #[cfg(feature = "catch-all-fields")]
326    #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))]
327    #[serde(flatten)]
328    pub other: serde_json::Map<String, Value>,
329}
330
331#[non_exhaustive]
332#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
333#[serde(rename_all = "PascalCase")]
334pub struct CloudWatchMetric {
335    #[serde(default)]
336    pub dimensions: Vec<CloudWatchDimension>,
337    pub metric_name: Option<String>,
338    pub namespace: Option<String>,
339    /// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
340    /// Enabled with Cargo feature `catch-all-fields`.
341    /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored.
342    #[cfg(feature = "catch-all-fields")]
343    #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))]
344    #[serde(flatten)]
345    pub other: serde_json::Map<String, Value>,
346}
347
348#[non_exhaustive]
349#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
350pub struct CloudWatchDimension {
351    pub name: String,
352    pub value: String,
353    /// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
354    /// Enabled with Cargo feature `catch-all-fields`.
355    /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored.
356    #[cfg(feature = "catch-all-fields")]
357    #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))]
358    #[serde(flatten)]
359    pub other: serde_json::Map<String, Value>,
360}
361
362#[cfg(test)]
363mod test {
364    use super::*;
365
366    #[test]
367    #[cfg(feature = "sns")]
368    fn my_example_sns_event() {
369        let data = include_bytes!("../../fixtures/example-sns-event.json");
370        let parsed: SnsEvent = serde_json::from_slice(data).unwrap();
371        let output: String = serde_json::to_string(&parsed).unwrap();
372        let reparsed: SnsEvent = serde_json::from_slice(output.as_bytes()).unwrap();
373        assert_eq!(parsed, reparsed);
374    }
375
376    #[test]
377    #[cfg(feature = "sns")]
378    fn my_example_sns_event_pascal_case() {
379        let data = include_bytes!("../../fixtures/example-sns-event-pascal-case.json");
380        let parsed: SnsEvent = serde_json::from_slice(data).unwrap();
381        let output: String = serde_json::to_string(&parsed).unwrap();
382        let reparsed: SnsEvent = serde_json::from_slice(output.as_bytes()).unwrap();
383        assert_eq!(parsed, reparsed);
384    }
385
386    #[test]
387    #[cfg(feature = "sns")]
388    fn my_example_sns_event_cloudwatch_single_metric() {
389        let data = include_bytes!("../../fixtures/example-cloudwatch-alarm-sns-payload-single-metric.json");
390        let parsed: SnsEvent = serde_json::from_slice(data).unwrap();
391        assert_eq!(1, parsed.records.len());
392
393        let output: String = serde_json::to_string(&parsed).unwrap();
394        let reparsed: SnsEvent = serde_json::from_slice(output.as_bytes()).unwrap();
395        assert_eq!(parsed, reparsed);
396
397        let parsed: SnsEventObj<CloudWatchAlarmPayload> =
398            serde_json::from_slice(data).expect("failed to parse CloudWatch Alarm payload");
399
400        let record = parsed.records.first().unwrap();
401        assert_eq!("EXAMPLE", record.sns.message.alarm_name);
402    }
403
404    #[test]
405    #[cfg(feature = "sns")]
406    fn my_example_sns_event_cloudwatch_multiple_metrics() {
407        let data = include_bytes!("../../fixtures/example-cloudwatch-alarm-sns-payload-multiple-metrics.json");
408        let parsed: SnsEvent = serde_json::from_slice(data).unwrap();
409        assert_eq!(2, parsed.records.len());
410
411        let output: String = serde_json::to_string(&parsed).unwrap();
412        let reparsed: SnsEvent = serde_json::from_slice(output.as_bytes()).unwrap();
413        assert_eq!(parsed, reparsed);
414    }
415
416    #[test]
417    #[cfg(feature = "sns")]
418    fn my_example_sns_obj_event() {
419        let data = include_bytes!("../../fixtures/example-sns-event-obj.json");
420
421        #[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
422        struct CustStruct {
423            foo: String,
424            bar: i32,
425        }
426
427        let parsed: SnsEventObj<CustStruct> = serde_json::from_slice(data).unwrap();
428        println!("{parsed:?}");
429
430        assert_eq!(parsed.records[0].sns.message.foo, "Hello world!");
431        assert_eq!(parsed.records[0].sns.message.bar, 123);
432
433        let output: String = serde_json::to_string(&parsed).unwrap();
434        let reparsed: SnsEventObj<CustStruct> = serde_json::from_slice(output.as_bytes()).unwrap();
435        assert_eq!(parsed, reparsed);
436    }
437}