cfn_resource_provider/
lib.rs

1#![deny(missing_docs)]
2
3//! # cfn-resource-provider
4//!
5//! This library is a relatively thin wrapper enabling the use of Rust in AWS Lambda to provide an
6//! AWS CloudFormation [custom resource]. It is intended to be used in conjunction with
7//! [`rust-aws-lambda`][rust-aws-lambda], a library that enables to run Rust applications serverless
8//! on AWS Lambda using the Go 1.x runtime.
9//!
10//! [custom resource]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html
11//! [rust-aws-lambda]: https://github.com/srijs/rust-aws-lambda
12//!
13//! ## Quick start example
14//!
15//! ```norun
16//! extern crate aws_lambda as lambda;
17//! extern crate cfn_resource_provider as cfn;
18//!
19//! use cfn::*;
20//!
21//! fn main() {
22//!     lambda::start(cfn::process(|event: CfnRequest<MyResourceProperties>| {
23//!         // Perform the necessary steps to create the custom resource. Afterwards you can return
24//!         // some data that should be serialized into the response. If you don't want to serialize
25//!         // any data, you can return `None` (were you unfortunately have to specify the unknown
26//!         // serializable type using the turbofish).
27//!         Ok(None::<()>)
28//!     }));
29//! }
30//! ```
31//!
32//! ## License
33//!
34//! This library is licensed under either of
35//!
36//! * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
37//!   http://www.apache.org/licenses/LICENSE-2.0)
38//! * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
39//!
40//! at your option.
41//!
42//! ### Contribution
43//!
44//! Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in
45//! cfn-resource-provider by you, as defined in the Apache-2.0 license, shall be dual licensed as
46//! above, without any additional terms or conditions.
47
48extern crate failure;
49extern crate futures;
50extern crate reqwest;
51extern crate serde;
52#[macro_use]
53extern crate serde_derive;
54#[cfg_attr(test, macro_use)]
55extern crate serde_json;
56
57use failure::Error;
58use futures::{Future, IntoFuture};
59use serde::de::{Deserialize, Deserializer};
60use serde::ser::Serialize;
61
62/// Every AWS CloudFormation resource, including custom resources, needs a unique physical resource
63/// ID. To aid in supplying this resource ID, your resource property type has to implement this
64/// trait with its single member, `physical_resource_id_suffix`.
65///
66/// When this library creates the response which will be sent to AWS CloudFormation, a physical
67/// resource ID will be created according to the following format (where `suffix` will be the suffix
68/// provided by the implementor):
69///
70/// ```norun
71/// arn:custom:cfn-resource-provider:::{stack_id}-{logical_resource_id}/{suffix}
72/// ```
73///
74/// The fields of a property-type used for suffix creation should be chosen as such that it changes
75/// when ever the custom resource implementation has to create an actual new physical resource. The
76/// suffix should also include the type of resource, maybe including a version number.
77///
78/// ## Example
79///
80/// Let's assume you have the following type and trait implementation:
81///
82/// ```
83/// # use cfn_resource_provider::*;
84/// struct MyResourcePropertiesType {
85///     my_unique_parameter: String,
86///     some_other_parameter: String,
87/// }
88/// impl PhysicalResourceIdSuffixProvider for MyResourcePropertiesType {
89///     fn physical_resource_id_suffix(&self) -> String {
90///         format!(
91///             "{resource_type}@{version}/{unique_reference}",
92///             resource_type=env!("CARGO_PKG_NAME"),
93///             version=env!("CARGO_PKG_VERSION"),
94///             unique_reference=self.my_unique_parameter,
95///         )
96///     }
97/// }
98/// ```
99///
100/// When [`CfnResponse`] creates or updates the physical ID for the resource, it might look like the
101/// following:
102///
103/// ```norun
104/// arn:custom:cfn-resource-provider:::12345678-1234-1234-1234-1234567890ab-logical-id/myresource@1.0.0/uniquereference
105/// ```
106///
107/// In this case `my_unique_parameter` is assumed to be the parameter that requires the custom
108/// resource implementation to create a new physical resource, thus the ID changes with it.
109///
110/// [`CfnResponse`]: enum.CfnResponse.html
111pub trait PhysicalResourceIdSuffixProvider {
112    /// Creates a suffix that uniquely identifies the physical resource represented by the type
113    /// holding the AWS CloudFormation resource properties.
114    fn physical_resource_id_suffix(&self) -> String;
115}
116
117impl<T> PhysicalResourceIdSuffixProvider for Option<T>
118where
119    T: PhysicalResourceIdSuffixProvider,
120{
121    fn physical_resource_id_suffix(&self) -> String {
122        match self {
123            Some(value) => value.physical_resource_id_suffix(),
124            None => String::new(),
125        }
126    }
127}
128
129impl PhysicalResourceIdSuffixProvider for () {
130    fn physical_resource_id_suffix(&self) -> String {
131        String::new()
132    }
133}
134
135/// On stack modification, AWS CloudFormation sends out a request for custom resources. This enum
136/// can represent such a request, encapsulating the three request variants:
137///
138/// 1. Creation of a custom resource.
139/// 2. Update of a custom resource.
140/// 3. Deletion of a custom resource.
141///
142/// (For more information on AWS CloudFormation custom resource requests, see
143/// [docs.aws.amazon.com].)
144///
145/// When creating/updating a custom resource, AWS CloudFormation forwards any additional key-value
146/// pairs the template designer provided with the request. To enable strict typing on this data,
147/// `CfnRequest` has the generic type parameter `P` which the caller provides. This has to be a
148/// deserializable type.
149///
150/// [docs.aws.amazon.com]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requests.html
151///
152/// ## Example
153///
154/// The following is an example on how one can create a type that is deserializable through [Serde],
155/// such that the untyped JSON map object provided by AWS CloudFormation can be converted into a
156/// strongly typed struct. (If the JSON is not compatible with the struct, deserialization and thus
157/// modification of the custom resource fails.)
158///
159/// ```
160/// # extern crate cfn_resource_provider;
161/// # #[macro_use]
162/// # extern crate serde_derive;
163/// # #[macro_use]
164/// # extern crate serde_json;
165/// # use cfn_resource_provider::*;
166/// #[derive(Debug, PartialEq, Clone, Deserialize)]
167/// struct MyResourceProperties {
168///     parameter1: String,
169///     parameter2: Vec<String>,
170/// }
171/// # fn main() {
172/// let actual = serde_json::from_value(json!(
173///     {
174///         "parameter1": "example for the first parameter",
175///         "parameter2": ["list", "of", "values"]
176///     }
177/// )).unwrap();
178///
179/// let expected = MyResourceProperties {
180///     parameter1: "example for the first parameter".to_owned(),
181///     parameter2: vec!["list".to_owned(), "of".to_owned(), "values".to_owned()],
182/// };
183///
184/// assert_eq!(expected, actual);
185/// # }
186/// ```
187///
188/// [Serde]: https://serde.rs/
189///
190/// ## Required presence of resource properties
191///
192/// If you have read the AWS CloudFormation documentation on [custom resource requests], you might
193/// have seen that the `ResourceProperties` field on a request sent by AWS CloudFormation can be
194/// optional, whereas all variants in this enum seem to require the field to be present.
195///
196/// The reason for the field being optional is (presumably) that AWS CloudFormation wants to support
197/// custom resources that do not require additional parameters besides the ones automatically sent
198/// by AWS CloudFormation, i.e. a custom resource might be just fine with only the stack ID.
199///
200/// Where this reasoning falls short, and where the documentation contradicts itself, is when it
201/// comes to updating resources. For update requests it is documented that the
202/// `OldResourceProperties` field is mandatory. Now, what happens if you update a resource that
203/// previously didn't have any properties? Will the `OldResourceProperties` field be present as the
204/// documentation requires it to be, although it cannot have any (reasonable) content?
205///
206/// For this reason, and for the sake of simplicity in usage and implementation, the user of this
207/// library can decide whether they want all property fields to be required or optional. You have at
208/// least four options:
209///
210/// 1. If your custom resource requires additional properties to function correctly, simply provide
211///    your type `T` as-is.
212///
213/// 2. If you want your resource to support custom resource properties, but not to depend on them,
214///    you can provide an `Option<T>` instead.
215///
216/// 3. Should you not need custom resource properties at all, but want the deserialization of the
217///    request to fail if any are provided, you can specify `Option<()>`.
218///
219/// 4. If you don't need custom resource properties _and_ don't want to fail should they have been
220///    provided, you can specify `Ignored` as the type. This is a [custom struct included] in this
221///    library that deserializes any and no data into itself. This means that any data provided by
222///    AWS CloudFormation will be discarded, but it will also not fail if no data was present.
223///
224/// [custom resource requests]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requests.html
225/// [custom struct included]: (struct.Ignored.html)
226///
227/// ## License attribution
228///
229/// The documentation for the `CfnRequest` enum-variants and their fields has been taken unmodified
230/// from the AWS CloudFormation [Custom Resource Reference], which is licensed under [CC BY-SA 4.0].
231///
232/// [Custom Resource Reference]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref.html
233/// [CC BY-SA 4.0]: https://creativecommons.org/licenses/by-sa/4.0/
234#[derive(Debug, Clone, PartialEq, Deserialize)]
235#[serde(tag = "RequestType")]
236pub enum CfnRequest<P>
237where
238    P: Clone,
239{
240    /// Custom resource provider requests with `RequestType` set to "`Create`" are sent when the
241    /// template developer creates a stack that contains a custom resource. _See
242    /// [docs.aws.amazon.com] for more information._
243    ///
244    /// [docs.aws.amazon.com]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requesttypes-create.html
245    #[serde(rename_all = "PascalCase")]
246    Create {
247        /// A unique ID for the request.
248        request_id: String,
249        /// The response URL identifies a presigned S3 bucket that receives responses from the
250        /// custom resource provider to AWS CloudFormation.
251        #[serde(rename = "ResponseURL")]
252        response_url: String,
253        /// The template developer-chosen resource type of the custom resource in the AWS
254        /// CloudFormation template. Custom resource type names can be up to 60 characters long and
255        /// can include alphanumeric and the following characters: `_@-`.
256        resource_type: String,
257        /// The template developer-chosen name (logical ID) of the custom resource in the AWS
258        /// CloudFormation template.
259        logical_resource_id: String,
260        /// The Amazon Resource Name (ARN) that identifies the stack that contains the custom
261        /// resource.
262        stack_id: String,
263        /// This field contains the contents of the `Properties` object sent by the template
264        /// developer. Its contents are defined by the custom resource provider.
265        resource_properties: P,
266    },
267    /// Custom resource provider requests with `RequestType` set to "`Delete`" are sent when the
268    /// template developer deletes a stack that contains a custom resource. To successfully delete a
269    /// stack with a custom resource, the custom resource provider must respond successfully to a
270    /// delete request. _See [docs.aws.amazon.com] for more information._
271    ///
272    /// [docs.aws.amazon.com]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requesttypes-delete.html
273    #[serde(rename_all = "PascalCase")]
274    Delete {
275        /// A unique ID for the request.
276        request_id: String,
277        /// The response URL identifies a presigned S3 bucket that receives responses from the
278        /// custom resource provider to AWS CloudFormation.
279        #[serde(rename = "ResponseURL")]
280        response_url: String,
281        /// The template developer-chosen resource type of the custom resource in the AWS
282        /// CloudFormation template. Custom resource type names can be up to 60 characters long and
283        /// can include alphanumeric and the following characters: `_@-`.
284        resource_type: String,
285        /// The template developer-chosen name (logical ID) of the custom resource in the AWS
286        /// CloudFormation template.
287        logical_resource_id: String,
288        /// The Amazon Resource Name (ARN) that identifies the stack that contains the custom
289        /// resource.
290        stack_id: String,
291        /// A required custom resource provider-defined physical ID that is unique for that
292        /// provider.
293        physical_resource_id: String,
294        /// This field contains the contents of the `Properties` object sent by the template
295        /// developer. Its contents are defined by the custom resource provider.
296        resource_properties: P,
297    },
298    /// Custom resource provider requests with `RequestType` set to "`Update`" are sent when there's
299    /// any change to the properties of the custom resource within the template. Therefore, custom
300    /// resource code doesn't have to detect changes because it knows that its properties have
301    /// changed when Update is being called. _See [docs.aws.amazon.com] for more information._
302    ///
303    /// [docs.aws.amazon.com]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requesttypes-update.html
304    #[serde(rename_all = "PascalCase")]
305    Update {
306        /// A unique ID for the request.
307        request_id: String,
308        /// The response URL identifies a presigned S3 bucket that receives responses from the
309        /// custom resource provider to AWS CloudFormation.
310        #[serde(rename = "ResponseURL")]
311        response_url: String,
312        /// The template developer-chosen resource type of the custom resource in the AWS
313        /// CloudFormation template. Custom resource type names can be up to 60 characters long and
314        /// can include alphanumeric and the following characters: `_@-`.
315        resource_type: String,
316        /// The template developer-chosen name (logical ID) of the custom resource in the AWS
317        /// CloudFormation template.
318        logical_resource_id: String,
319        /// The Amazon Resource Name (ARN) that identifies the stack that contains the custom
320        /// resource.
321        stack_id: String,
322        /// A required custom resource provider-defined physical ID that is unique for that
323        /// provider.
324        physical_resource_id: String,
325        /// This field contains the contents of the `Properties` object sent by the template
326        /// developer. Its contents are defined by the custom resource provider.
327        resource_properties: P,
328        /// The resource property values that were previously declared by the template developer in
329        /// the AWS CloudFormation template.
330        old_resource_properties: P,
331    },
332}
333
334impl<P> CfnRequest<P>
335where
336    P: PhysicalResourceIdSuffixProvider + Clone,
337{
338    /// The request ID field exists for all variants of the [`CfnRequest` enum]. This is a helper
339    /// method to access this field without requiring you to match for the variant yourself.
340    ///
341    /// [`CfnRequest` enum]: enum.CfnRequest.html
342    #[inline(always)]
343    pub fn request_id(&self) -> String {
344        match self {
345            CfnRequest::Create { request_id, .. } => request_id.to_owned(),
346            CfnRequest::Delete { request_id, .. } => request_id.to_owned(),
347            CfnRequest::Update { request_id, .. } => request_id.to_owned(),
348        }
349    }
350
351    /// The response URL field exists for all variants of the [`CfnRequest` enum]. This is a helper
352    /// method to access this field without requiring you to match for the variant yourself.
353    ///
354    /// [`CfnRequest` enum]: enum.CfnRequest.html
355    #[inline(always)]
356    pub fn response_url(&self) -> String {
357        match self {
358            CfnRequest::Create { response_url, .. } => response_url.to_owned(),
359            CfnRequest::Delete { response_url, .. } => response_url.to_owned(),
360            CfnRequest::Update { response_url, .. } => response_url.to_owned(),
361        }
362    }
363
364    /// The resource type field exists for all variants of the [`CfnRequest` enum]. This is a helper
365    /// method to access this field without requiring you to match for the variant yourself.
366    ///
367    /// [`CfnRequest` enum]: enum.CfnRequest.html
368    #[inline(always)]
369    pub fn resource_type(&self) -> String {
370        match self {
371            CfnRequest::Create { resource_type, .. } => resource_type.to_owned(),
372            CfnRequest::Delete { resource_type, .. } => resource_type.to_owned(),
373            CfnRequest::Update { resource_type, .. } => resource_type.to_owned(),
374        }
375    }
376
377    /// The logical resource ID field exists for all variants of the [`CfnRequest` enum]. This is a
378    /// helper method to access this field without requiring you to match for the variant yourself.
379    ///
380    /// [`CfnRequest` enum]: enum.CfnRequest.html
381    #[inline(always)]
382    pub fn logical_resource_id(&self) -> String {
383        match self {
384            CfnRequest::Create {
385                logical_resource_id,
386                ..
387            } => logical_resource_id.to_owned(),
388            CfnRequest::Delete {
389                logical_resource_id,
390                ..
391            } => logical_resource_id.to_owned(),
392            CfnRequest::Update {
393                logical_resource_id,
394                ..
395            } => logical_resource_id.to_owned(),
396        }
397    }
398
399    /// The stack ID field exists for all variants of the [`CfnRequest` enum]. This is a helper
400    /// method to access this field without requiring you to match for the variant yourself.
401    ///
402    /// [`CfnRequest` enum]: enum.CfnRequest.html
403    #[inline(always)]
404    pub fn stack_id(&self) -> String {
405        match self {
406            CfnRequest::Create { stack_id, .. } => stack_id.to_owned(),
407            CfnRequest::Delete { stack_id, .. } => stack_id.to_owned(),
408            CfnRequest::Update { stack_id, .. } => stack_id.to_owned(),
409        }
410    }
411
412    /// The physical resource ID field either exists or has to be (re)generated for all variants of
413    /// the [`CfnRequest` enum]. This is a helper method to access this field without requiring you
414    /// to match for the variant yourself, while always getting the correct and up-to-date physical
415    /// resource ID.
416    ///
417    /// [`CfnRequest` enum]: enum.CfnRequest.html
418    #[inline(always)]
419    pub fn physical_resource_id(&self) -> String {
420        match self {
421            CfnRequest::Create {
422                logical_resource_id,
423                stack_id,
424                resource_properties,
425                ..
426            }
427            | CfnRequest::Update {
428                logical_resource_id,
429                stack_id,
430                resource_properties,
431                ..
432            } => {
433                let suffix = resource_properties.physical_resource_id_suffix();
434                format!(
435                    "arn:custom:cfn-resource-provider:::{stack_id}-{logical_resource_id}{suffix_separator}{suffix}",
436                    stack_id = stack_id.rsplit('/').next().expect("failed to get GUID from stack ID"),
437                    logical_resource_id = logical_resource_id,
438                    suffix_separator = if suffix.is_empty() { "" } else { "/" },
439                    suffix = suffix,
440                )
441            }
442            CfnRequest::Delete {
443                physical_resource_id,
444                ..
445            } => physical_resource_id.to_owned(),
446        }
447    }
448
449    /// The resource properties field exists for all variants of the [`CfnRequest` enum]. This is a
450    /// helper method to access this field without requiring you to match for the variant yourself.
451    ///
452    /// [`CfnRequest` enum]: enum.CfnRequest.html
453    #[inline(always)]
454    pub fn resource_properties(&self) -> &P {
455        match self {
456            CfnRequest::Create { resource_properties, .. } => resource_properties,
457            CfnRequest::Delete { resource_properties, .. } => resource_properties,
458            CfnRequest::Update { resource_properties, .. } => resource_properties,
459        }
460    }
461
462    /// This method turns a [`CfnRequest`] into a [`CfnResponse`], choosing one of the `Success` or
463    /// `Failed` variants based on a `Result`. A [`CfnResponse`] should always be created through
464    /// this method to ensure that all the relevant response-fields that AWS CloudFormation requires
465    /// are populated correctly.
466    ///
467    /// [`CfnRequest`]: enum.CfnRequest.html
468    /// [`CfnResponse`]: enum.CfnResponse.html
469    pub fn into_response<S>(self, result: &Result<Option<S>, Error>) -> CfnResponse
470    where
471        S: Serialize,
472    {
473        match result {
474            Ok(data) => CfnResponse::Success {
475                request_id: self.request_id(),
476                logical_resource_id: self.logical_resource_id(),
477                stack_id: self.stack_id(),
478                physical_resource_id: self.physical_resource_id(),
479                no_echo: None,
480                data: data
481                    .as_ref()
482                    .and_then(|value| serde_json::to_value(value).ok()),
483            },
484            Err(e) => CfnResponse::Failed {
485                reason: format!("{}", e),
486                request_id: self.request_id(),
487                logical_resource_id: self.logical_resource_id(),
488                stack_id: self.stack_id(),
489                physical_resource_id: self.physical_resource_id(),
490            },
491        }
492    }
493}
494
495/// This is a special struct that can be used in conjunction with [Serde] to represent a field whose
496/// contents should be discarded during deserialization if it is present, and doesn't fail if the
497/// field doesn't exist.
498///
499/// This type is meant to be used as the generic type parameter for [`CfnRequest`] if your AWS
500/// CloudFormation custom resource doesn't take any custom resource properties, but you don't want
501/// deserialization to fail should any properties be specified.
502///
503/// [Serde]: https://serde.rs/
504/// [`CfnRequest`]: enum.CfnRequest.html
505#[derive(Debug, Clone, Default, Copy, PartialEq)]
506pub struct Ignored;
507
508impl<'de> Deserialize<'de> for Ignored {
509    fn deserialize<D>(_deserializer: D) -> Result<Ignored, D::Error>
510    where
511        D: Deserializer<'de>,
512    {
513        Ok(Ignored)
514    }
515}
516
517impl PhysicalResourceIdSuffixProvider for Ignored {
518    fn physical_resource_id_suffix(&self) -> String {
519        String::new()
520    }
521}
522
523/// This enum represents the response expected by AWS CloudFormation to a custom resource
524/// modification request (see [`CfnRequest`]). It is serializable into the
525/// required JSON form, such that it can be sent to the pre-signed S3 response-URL provided by AWS
526/// CloudFormation without further modification.
527///
528/// This type should always be constructed from a [`CfnRequest`] using
529/// [`CfnRequest::into_response`][into_response] such that the response-fields are pre-filled with
530/// the expected values.
531///
532/// [`CfnRequest`]: enum.CfnRequest.html
533/// [into_response]: enum.CfnRequest.html#method.into_response
534///
535/// ## License attribution
536///
537/// The documentation for the fields of the `CfnResponse` enum-variants has been taken unmodified
538/// from the AWS CloudFormation [Custom Resource Reference], which is licensed under [CC BY-SA 4.0].
539///
540/// [Custom Resource Reference]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref.html
541/// [CC BY-SA 4.0]: https://creativecommons.org/licenses/by-sa/4.0/
542#[derive(Debug, Clone, PartialEq, Serialize)]
543#[serde(tag = "Status", rename_all = "SCREAMING_SNAKE_CASE")]
544pub enum CfnResponse {
545    /// Indicates that the modification of the custom resource finished successfully.
546    ///
547    /// This can return data which the AWS CloudFormation template can interact with through the use
548    /// of `Fn::GetAtt`.
549    #[serde(rename_all = "PascalCase")]
550    Success {
551        /// A unique ID for the request. This response value should be copied _verbatim_ from the
552        /// request.
553        request_id: String,
554        /// The template developer-chosen name (logical ID) of the custom resource in the AWS
555        /// CloudFormation template. This response value should be copied _verbatim_ from the
556        /// request.
557        logical_resource_id: String,
558        /// The Amazon Resource Name (ARN) that identifies the stack that contains the custom
559        /// resource. This response value should be copied _verbatim_ from the request.
560        stack_id: String,
561        /// This value should be an identifier unique to the custom resource vendor, and can be up
562        /// to 1 Kb in size. The value must be a non-empty string and must be identical for all
563        /// responses for the same resource.
564        physical_resource_id: String,
565        /// Optional. Indicates whether to mask the output of the custom resource when retrieved by
566        /// using the `Fn::GetAtt` function. If set to `true`, all returned values are masked with
567        /// asterisks (\*\*\*\*\*). The default value is `false`.
568        #[serde(skip_serializing_if = "Option::is_none")]
569        no_echo: Option<bool>,
570        /// Optional. The custom resource provider-defined name-value pairs to send with the
571        /// response. You can access the values provided here by name in the template with
572        /// `Fn::GetAtt`.
573        #[serde(skip_serializing_if = "Option::is_none")]
574        data: Option<serde_json::Value>,
575    },
576    /// Indicates that the modification of the custom resource failed.
577    ///
578    /// A reason for this failure will be provided.
579    #[serde(rename_all = "PascalCase")]
580    Failed {
581        /// Describes the reason for a failure response.
582        reason: String,
583        /// A unique ID for the request. This response value should be copied _verbatim_ from the
584        /// request.
585        request_id: String,
586        /// The template developer-chosen name (logical ID) of the custom resource in the AWS
587        /// CloudFormation template. This response value should be copied _verbatim_ from the
588        /// request.
589        logical_resource_id: String,
590        /// The Amazon Resource Name (ARN) that identifies the stack that contains the custom
591        /// resource. This response value should be copied _verbatim_ from the request.
592        stack_id: String,
593        /// This value should be an identifier unique to the custom resource vendor, and can be up
594        /// to 1 Kb in size. The value must be a non-empty string and must be identical for all
595        /// responses for the same resource.
596        physical_resource_id: String,
597    },
598}
599
600/// Process an AWS CloudFormation custom resource request.
601///
602/// This function will, in conjunction with [`rust-aws-lambda`][rust-aws-lambda], deserialize the
603/// JSON message sent by AWS CloudFormation into a strongly typed struct. Any custom resource
604/// properties you might have can be specified to have them deserialized, too.
605///
606/// `process` expects a single parameter, which should be a closure that receives a
607/// [`CfnRequest<P>`][CfnRequest] as its only parameter, and is expected to return a type that can
608/// succeed or fail (this can be a future or simply a [`Result`]; anything that implements
609/// [`IntoFuture`]). The type returned for success has to be an `Option<S>`, where `S` needs to be
610/// serializable. The failure type is expected to be [`failure::Error`]. The computation required to
611/// create your custom resource should happen in this closure.
612///
613/// The result of your closure will then be used to construct the response that will be sent to AWS
614/// CloudFormation. This response informs AWS CloudFormation whether creating the custom resource
615/// was successful or if it failed (including a reason for the failure). This is done by converting
616/// the initial [`CfnRequest`][CfnRequest] into a [`CfnResponse`][CfnResponse], pre-filling the
617/// required fields based on the result your closure returned.
618///
619/// If your closure has errored, the failure reason will be extracted from the error you returned.
620/// If your closure succeeded, the positive return value will be serialized into the
621/// [`data` field][CfnResponse.Success.data] (unless the returned `Option` is `None`). (Specifying
622/// the [`no_echo` option] is currently not possible.)
623///
624/// ## Example
625///
626/// ```norun
627/// extern crate aws_lambda as lambda;
628/// extern crate cfn_resource_provider as cfn;
629///
630/// use cfn::*;
631///
632/// fn main() {
633///     lambda::start(cfn::process(|event: CfnRequest<MyResourceProperties>| {
634///         // Perform the necessary steps to create the custom resource. Afterwards you can return
635///         // some data that should be serialized into the response. If you don't want to serialize
636///         // any data, you can return `None` (were you unfortunately have to specify the unknown
637///         // serializable type using the turbofish).
638///         Ok(None::<()>)
639///     });
640/// }
641/// ```
642///
643/// [rust-aws-lambda]: https://github.com/srijs/rust-aws-lambda
644/// [CfnRequest]: enum.CfnRequest.html
645/// [`Result`]: https://doc.rust-lang.org/std/result/enum.Result.html
646/// [`IntoFuture`]: https://docs.rs/futures/0.1/futures/future/trait.IntoFuture.html
647/// [`failure::Error`]: https://docs.rs/failure/0.1/failure/struct.Error.html
648/// [CfnResponse]: enum.CfnRequest.html
649/// [CfnResponse.Success.data]: enum.CfnResponse.html#variant.Success.field.data
650/// [CfnResponse.Success.no_echo]: enum.CfnResponse.html#variant.Success.field.no_echo
651pub fn process<F, R, P, S>(
652    f: F,
653) -> impl Fn(CfnRequest<P>) -> Box<Future<Item = Option<S>, Error = Error> + Send>
654where
655    F: Fn(CfnRequest<P>) -> R + Send + Sync + 'static,
656    R: IntoFuture<Item = Option<S>, Error = Error> + Send + 'static,
657    R::Future: Send,
658    S: Serialize + Send + 'static,
659    P: PhysicalResourceIdSuffixProvider + Clone + Send + 'static,
660{
661    // The process below is a bit convoluted to read, the main reason for this is the following: we
662    // want to forward the response given by the closure `f` to our caller, while using that same
663    // response to inform AWS CloudFormation of the status of the custom resource.
664    //
665    // To accomplish this, we use a nested chain of futures that works as follows.
666    //
667    // 1. Call closure `f`.
668    // 2. Transform the initial request into a AWS CloudFormation response, deciding on success or
669    //    failure through the result returned by `f`.
670    // 3. Try to serialize and send the response to AWS CloudFormation (if this fails at any step,
671    //    propagate the error through to our caller).
672    // 4. If informing AWS CloudFormation succeeded, return the initial result of `f` to our caller.
673    move |request: CfnRequest<P>| {
674        let response_url = request.response_url();
675        Box::new(f(request.clone()).into_future().then(|request_result| {
676            let cfn_response = request.into_response(&request_result);
677            serde_json::to_string(&cfn_response)
678                .map_err(Into::into)
679                .into_future()
680                .and_then(|cfn_response| {
681                    reqwest::async::Client::builder()
682                        .build()
683                        .into_future()
684                        .and_then(move |client| {
685                            client
686                                .put(&response_url)
687                                .header("Content-Type", "")
688                                .body(cfn_response)
689                                .send()
690                        }).and_then(reqwest::async::Response::error_for_status)
691                        .map_err(Into::into)
692                }).and_then(move |_| request_result)
693        }))
694    }
695}
696
697#[cfg(test)]
698mod test {
699    use super::*;
700
701    #[derive(Debug, Clone)]
702    struct Empty;
703    impl PhysicalResourceIdSuffixProvider for Empty {
704        fn physical_resource_id_suffix(&self) -> String {
705            String::new()
706        }
707    }
708
709    #[derive(Debug, Clone)]
710    struct StaticSuffix;
711    impl PhysicalResourceIdSuffixProvider for StaticSuffix {
712        fn physical_resource_id_suffix(&self) -> String {
713            "STATIC-SUFFIX".to_owned()
714        }
715    }
716
717    #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
718    #[serde(rename_all = "PascalCase")]
719    struct ExampleProperties {
720        example_property_1: String,
721        example_property_2: Option<bool>,
722    }
723    impl PhysicalResourceIdSuffixProvider for ExampleProperties {
724        fn physical_resource_id_suffix(&self) -> String {
725            self.example_property_1.to_owned()
726        }
727    }
728
729    /// This test verifies that if the suffix returned by the generic type parameter given to
730    /// `CfnRequest` is empty, the physical resource ID does not end on a separating slash.
731    #[test]
732    fn empty_suffix_has_no_trailing_slash() {
733        let request: CfnRequest<Empty> = CfnRequest::Create {
734            request_id: String::new(),
735            response_url: String::new(),
736            resource_type: String::new(),
737            logical_resource_id: String::new(),
738            stack_id: String::new(),
739            resource_properties: Empty,
740        };
741        assert!(!request.physical_resource_id().ends_with("/"));
742    }
743
744    /// This test verifies that the suffix provided by the generic type given to `CfnRequest` is
745    /// separated from the resource ID by a slash.
746    #[test]
747    fn static_suffix_is_correctly_appended() {
748        let request: CfnRequest<StaticSuffix> = CfnRequest::Create {
749            request_id: String::new(),
750            response_url: String::new(),
751            resource_type: String::new(),
752            logical_resource_id: String::new(),
753            stack_id: String::new(),
754            resource_properties: StaticSuffix,
755        };
756        assert!(request.physical_resource_id().ends_with("/STATIC-SUFFIX"));
757    }
758
759    /// This is meant as a type-checking test: we want to ensure that we can provide a required type
760    /// to `CfnRequest`.
761    #[test]
762    fn cfnrequest_generic_type_required() {
763        let request: CfnRequest<Empty> = CfnRequest::Create {
764            request_id: String::new(),
765            response_url: String::new(),
766            resource_type: String::new(),
767            logical_resource_id: String::new(),
768            stack_id: String::new(),
769            resource_properties: Empty,
770        };
771        assert!(!request.physical_resource_id().is_empty());
772    }
773
774    /// This is meant as a type-checking test: we want to ensure that we can provide an optional
775    /// type to `CfnRequest`.
776    #[test]
777    fn cfnrequest_generic_type_optional() {
778        let mut request: CfnRequest<Option<Empty>> = CfnRequest::Create {
779            request_id: String::new(),
780            response_url: String::new(),
781            resource_type: String::new(),
782            logical_resource_id: String::new(),
783            stack_id: String::new(),
784            resource_properties: None,
785        };
786        assert!(!request.physical_resource_id().is_empty());
787        assert!(!request.physical_resource_id().ends_with("/"));
788        request = CfnRequest::Create {
789            request_id: String::new(),
790            response_url: String::new(),
791            resource_type: String::new(),
792            logical_resource_id: String::new(),
793            stack_id: String::new(),
794            resource_properties: Some(Empty),
795        };
796        assert!(!request.physical_resource_id().is_empty());
797        assert!(!request.physical_resource_id().ends_with("/"));
798    }
799
800    /// This is meant as a type-checking test: we want to ensure that we can provide the optional
801    /// unit-type to `CfnRequest`.
802    #[test]
803    fn cfnrequest_generic_type_optional_unit() {
804        let mut request: CfnRequest<Option<()>> = CfnRequest::Create {
805            request_id: String::new(),
806            response_url: String::new(),
807            resource_type: String::new(),
808            logical_resource_id: String::new(),
809            stack_id: String::new(),
810            resource_properties: None,
811        };
812        assert!(!request.physical_resource_id().is_empty());
813        assert!(!request.physical_resource_id().ends_with("/"));
814        request = CfnRequest::Create {
815            request_id: String::new(),
816            response_url: String::new(),
817            resource_type: String::new(),
818            logical_resource_id: String::new(),
819            stack_id: String::new(),
820            resource_properties: Some(()),
821        };
822        assert!(!request.physical_resource_id().is_empty());
823        assert!(!request.physical_resource_id().ends_with("/"));
824    }
825
826    #[test]
827    fn cfnrequest_type_present() {
828        let expected_request: CfnRequest<ExampleProperties> = CfnRequest::Create {
829            request_id: "unique id for this create request".to_owned(),
830            response_url: "pre-signed-url-for-create-response".to_owned(),
831            resource_type: "Custom::MyCustomResourceType".to_owned(),
832            logical_resource_id: "name of resource in template".to_owned(),
833            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
834            resource_properties: ExampleProperties {
835                example_property_1: "example property 1".to_owned(),
836                example_property_2: None,
837            },
838        };
839        let actual_request: CfnRequest<ExampleProperties> = serde_json::from_value(json!({
840            "RequestType" : "Create",
841            "RequestId" : "unique id for this create request",
842            "ResponseURL" : "pre-signed-url-for-create-response",
843            "ResourceType" : "Custom::MyCustomResourceType",
844            "LogicalResourceId" : "name of resource in template",
845            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
846            "ResourceProperties": {
847                "ExampleProperty1": "example property 1"
848            }
849        })).unwrap();
850        assert_eq!(expected_request, actual_request);
851    }
852
853    #[test]
854    #[should_panic]
855    fn cfnrequest_type_absent() {
856        serde_json::from_value::<CfnRequest<ExampleProperties>>(json!({
857            "RequestType" : "Create",
858            "RequestId" : "unique id for this create request",
859            "ResponseURL" : "pre-signed-url-for-create-response",
860            "ResourceType" : "Custom::MyCustomResourceType",
861            "LogicalResourceId" : "name of resource in template",
862            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid"
863        })).unwrap();
864    }
865
866    #[test]
867    #[should_panic]
868    fn cfnrequest_type_malformed() {
869        serde_json::from_value::<CfnRequest<ExampleProperties>>(json!({
870            "RequestType" : "Create",
871            "RequestId" : "unique id for this create request",
872            "ResponseURL" : "pre-signed-url-for-create-response",
873            "ResourceType" : "Custom::MyCustomResourceType",
874            "LogicalResourceId" : "name of resource in template",
875            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
876            "ResourceProperties": {
877                "UnknownProperty": null
878            }
879        })).unwrap();
880    }
881
882    #[test]
883    fn cfnrequest_type_option_present() {
884        let expected_request: CfnRequest<Option<ExampleProperties>> = CfnRequest::Create {
885            request_id: "unique id for this create request".to_owned(),
886            response_url: "pre-signed-url-for-create-response".to_owned(),
887            resource_type: "Custom::MyCustomResourceType".to_owned(),
888            logical_resource_id: "name of resource in template".to_owned(),
889            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
890            resource_properties: Some(ExampleProperties {
891                example_property_1: "example property 1".to_owned(),
892                example_property_2: None,
893            }),
894        };
895        let actual_request: CfnRequest<Option<ExampleProperties>> = serde_json::from_value(json!({
896            "RequestType" : "Create",
897            "RequestId" : "unique id for this create request",
898            "ResponseURL" : "pre-signed-url-for-create-response",
899            "ResourceType" : "Custom::MyCustomResourceType",
900            "LogicalResourceId" : "name of resource in template",
901            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
902            "ResourceProperties": {
903                "ExampleProperty1": "example property 1"
904            }
905        })).unwrap();
906        assert_eq!(expected_request, actual_request);
907    }
908
909    #[test]
910    fn cfnrequest_type_option_absent() {
911        let expected_request: CfnRequest<Option<ExampleProperties>> = CfnRequest::Create {
912            request_id: "unique id for this create request".to_owned(),
913            response_url: "pre-signed-url-for-create-response".to_owned(),
914            resource_type: "Custom::MyCustomResourceType".to_owned(),
915            logical_resource_id: "name of resource in template".to_owned(),
916            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
917            resource_properties: None,
918        };
919        let actual_request: CfnRequest<Option<ExampleProperties>> = serde_json::from_value(json!({
920            "RequestType" : "Create",
921            "RequestId" : "unique id for this create request",
922            "ResponseURL" : "pre-signed-url-for-create-response",
923            "ResourceType" : "Custom::MyCustomResourceType",
924            "LogicalResourceId" : "name of resource in template",
925            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid"
926        })).unwrap();
927        assert_eq!(expected_request, actual_request);
928    }
929
930    #[test]
931    #[should_panic]
932    fn cfnrequest_type_option_malformed() {
933        serde_json::from_value::<CfnRequest<Option<ExampleProperties>>>(json!({
934            "RequestType" : "Create",
935            "RequestId" : "unique id for this create request",
936            "ResponseURL" : "pre-signed-url-for-create-response",
937            "ResourceType" : "Custom::MyCustomResourceType",
938            "LogicalResourceId" : "name of resource in template",
939            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
940            "ResourceProperties": {
941                "UnknownProperty": null
942            }
943        })).unwrap();
944    }
945
946    #[test]
947    fn cfnrequest_type_option_unit() {
948        let expected_request: CfnRequest<Option<()>> = CfnRequest::Create {
949            request_id: "unique id for this create request".to_owned(),
950            response_url: "pre-signed-url-for-create-response".to_owned(),
951            resource_type: "Custom::MyCustomResourceType".to_owned(),
952            logical_resource_id: "name of resource in template".to_owned(),
953            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
954            resource_properties: None,
955        };
956        let mut actual_request: CfnRequest<Option<()>> = serde_json::from_value(json!({
957            "RequestType" : "Create",
958            "RequestId" : "unique id for this create request",
959            "ResponseURL" : "pre-signed-url-for-create-response",
960            "ResourceType" : "Custom::MyCustomResourceType",
961            "LogicalResourceId" : "name of resource in template",
962            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
963            "ResourceProperties" : null
964        })).unwrap();
965        assert_eq!(expected_request, actual_request);
966        actual_request = serde_json::from_value(json!({
967            "RequestType" : "Create",
968            "RequestId" : "unique id for this create request",
969            "ResponseURL" : "pre-signed-url-for-create-response",
970            "ResourceType" : "Custom::MyCustomResourceType",
971            "LogicalResourceId" : "name of resource in template",
972            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid"
973        })).unwrap();
974        assert_eq!(expected_request, actual_request);
975    }
976
977    #[test]
978    #[should_panic]
979    fn cfnrequest_type_option_unit_data_provided() {
980        serde_json::from_value::<CfnRequest<Option<()>>>(json!({
981            "RequestType" : "Create",
982            "RequestId" : "unique id for this create request",
983            "ResponseURL" : "pre-signed-url-for-create-response",
984            "ResourceType" : "Custom::MyCustomResourceType",
985            "LogicalResourceId" : "name of resource in template",
986            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
987            "ResourceProperties" : {
988                "key1" : "string",
989                "key2" : [ "list" ],
990                "key3" : { "key4" : "map" }
991            }
992        })).unwrap();
993    }
994
995    #[test]
996    fn cfnrequest_type_ignored() {
997        let expected_request: CfnRequest<Ignored> = CfnRequest::Create {
998            request_id: "unique id for this create request".to_owned(),
999            response_url: "pre-signed-url-for-create-response".to_owned(),
1000            resource_type: "Custom::MyCustomResourceType".to_owned(),
1001            logical_resource_id: "name of resource in template".to_owned(),
1002            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
1003            resource_properties: Ignored,
1004        };
1005        let mut actual_request: CfnRequest<Ignored> = serde_json::from_value(json!({
1006            "RequestType" : "Create",
1007            "RequestId" : "unique id for this create request",
1008            "ResponseURL" : "pre-signed-url-for-create-response",
1009            "ResourceType" : "Custom::MyCustomResourceType",
1010            "LogicalResourceId" : "name of resource in template",
1011            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
1012            "ResourceProperties" : {
1013                "key1" : "string",
1014                "key2" : [ "list" ],
1015                "key3" : { "key4" : "map" }
1016            }
1017        })).unwrap();
1018        assert_eq!(expected_request, actual_request);
1019        actual_request = serde_json::from_value(json!({
1020            "RequestType" : "Create",
1021            "RequestId" : "unique id for this create request",
1022            "ResponseURL" : "pre-signed-url-for-create-response",
1023            "ResourceType" : "Custom::MyCustomResourceType",
1024            "LogicalResourceId" : "name of resource in template",
1025            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid"
1026        })).unwrap();
1027        assert_eq!(expected_request, actual_request);
1028    }
1029
1030    #[test]
1031    fn cfnrequest_create_example() {
1032        #[derive(Debug, Clone, PartialEq, Deserialize)]
1033        struct ExampleProperties {
1034            key1: String,
1035            key2: Vec<String>,
1036            key3: serde_json::Value,
1037        }
1038
1039        let expected_request = CfnRequest::Create {
1040            request_id: "unique id for this create request".to_owned(),
1041            response_url: "pre-signed-url-for-create-response".to_owned(),
1042            resource_type: "Custom::MyCustomResourceType".to_owned(),
1043            logical_resource_id: "name of resource in template".to_owned(),
1044            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
1045            resource_properties: ExampleProperties {
1046                key1: "string".to_owned(),
1047                key2: vec!["list".to_owned()],
1048                key3: json!({ "key4": "map" }),
1049            },
1050        };
1051
1052        let actual_request = serde_json::from_value(json!({
1053            "RequestType" : "Create",
1054            "RequestId" : "unique id for this create request",
1055            "ResponseURL" : "pre-signed-url-for-create-response",
1056            "ResourceType" : "Custom::MyCustomResourceType",
1057            "LogicalResourceId" : "name of resource in template",
1058            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
1059            "ResourceProperties" : {
1060                "key1" : "string",
1061                "key2" : [ "list" ],
1062                "key3" : { "key4" : "map" }
1063            }
1064        })).unwrap();
1065
1066        assert_eq!(expected_request, actual_request);
1067    }
1068
1069    #[test]
1070    fn cfnresponse_create_success_example() {
1071        let expected_response = json!({
1072            "Status" : "SUCCESS",
1073            "RequestId" : "unique id for this create request (copied from request)",
1074            "LogicalResourceId" : "name of resource in template (copied from request)",
1075            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
1076            "PhysicalResourceId" : "required vendor-defined physical id that is unique for that vendor",
1077            "Data" : {
1078                "keyThatCanBeUsedInGetAtt1" : "data for key 1",
1079                "keyThatCanBeUsedInGetAtt2" : "data for key 2"
1080            }
1081        });
1082
1083        let actual_response = serde_json::to_value(CfnResponse::Success {
1084            request_id: "unique id for this create request (copied from request)".to_owned(),
1085            logical_resource_id: "name of resource in template (copied from request)".to_owned(),
1086            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)".to_owned(),
1087            physical_resource_id: "required vendor-defined physical id that is unique for that vendor".to_owned(),
1088            no_echo: None,
1089            data: Some(json!({
1090                "keyThatCanBeUsedInGetAtt1" : "data for key 1",
1091                "keyThatCanBeUsedInGetAtt2" : "data for key 2"
1092            })),
1093        }).unwrap();
1094
1095        assert_eq!(expected_response, actual_response);
1096    }
1097
1098    #[test]
1099    fn cfnresponse_create_failed_example() {
1100        let expected_response = json!({
1101            "Status" : "FAILED",
1102            "Reason" : "Required failure reason string",
1103            "RequestId" : "unique id for this create request (copied from request)",
1104            "LogicalResourceId" : "name of resource in template (copied from request)",
1105            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
1106            "PhysicalResourceId" : "required vendor-defined physical id that is unique for that vendor"
1107        });
1108
1109        let actual_response = serde_json::to_value(CfnResponse::Failed {
1110            reason: "Required failure reason string".to_owned(),
1111            request_id: "unique id for this create request (copied from request)".to_owned(),
1112            logical_resource_id: "name of resource in template (copied from request)".to_owned(),
1113            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)".to_owned(),
1114            physical_resource_id: "required vendor-defined physical id that is unique for that vendor".to_owned(),
1115        }).unwrap();
1116
1117        assert_eq!(expected_response, actual_response);
1118    }
1119
1120    #[test]
1121    fn cfnrequest_delete_example() {
1122        #[derive(Debug, PartialEq, Clone, Deserialize)]
1123        struct ExampleProperties {
1124            key1: String,
1125            key2: Vec<String>,
1126            key3: serde_json::Value,
1127        }
1128
1129        let expected_request = CfnRequest::Delete {
1130            request_id: "unique id for this delete request".to_owned(),
1131            response_url: "pre-signed-url-for-delete-response".to_owned(),
1132            resource_type: "Custom::MyCustomResourceType".to_owned(),
1133            logical_resource_id: "name of resource in template".to_owned(),
1134            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
1135            physical_resource_id: "custom resource provider-defined physical id".to_owned(),
1136            resource_properties: ExampleProperties {
1137                key1: "string".to_owned(),
1138                key2: vec!["list".to_owned()],
1139                key3: json!({ "key4": "map" }),
1140            },
1141        };
1142
1143        let actual_request = serde_json::from_value(json!({
1144            "RequestType" : "Delete",
1145            "RequestId" : "unique id for this delete request",
1146            "ResponseURL" : "pre-signed-url-for-delete-response",
1147            "ResourceType" : "Custom::MyCustomResourceType",
1148            "LogicalResourceId" : "name of resource in template",
1149            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
1150            "PhysicalResourceId" : "custom resource provider-defined physical id",
1151            "ResourceProperties" : {
1152                "key1" : "string",
1153                "key2" : [ "list" ],
1154                "key3" : { "key4" : "map" }
1155            }
1156        })).unwrap();
1157
1158        assert_eq!(expected_request, actual_request);
1159    }
1160
1161    #[test]
1162    fn cfnresponse_delete_success_example() {
1163        let expected_response = json!({
1164            "Status" : "SUCCESS",
1165            "RequestId" : "unique id for this delete request (copied from request)",
1166            "LogicalResourceId" : "name of resource in template (copied from request)",
1167            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
1168            "PhysicalResourceId" : "custom resource provider-defined physical id"
1169        });
1170
1171        let actual_response = serde_json::to_value(CfnResponse::Success {
1172            request_id: "unique id for this delete request (copied from request)".to_owned(),
1173            logical_resource_id: "name of resource in template (copied from request)".to_owned(),
1174            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)".to_owned(),
1175            physical_resource_id: "custom resource provider-defined physical id".to_owned(),
1176            no_echo: None,
1177            data: None,
1178        }).unwrap();
1179
1180        assert_eq!(expected_response, actual_response);
1181    }
1182
1183    #[test]
1184    fn cfnresponse_delete_failed_example() {
1185        let expected_response = json!({
1186            "Status" : "FAILED",
1187            "Reason" : "Required failure reason string",
1188            "RequestId" : "unique id for this delete request (copied from request)",
1189            "LogicalResourceId" : "name of resource in template (copied from request)",
1190            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
1191            "PhysicalResourceId" : "custom resource provider-defined physical id"
1192        });
1193
1194        let actual_response = serde_json::to_value(CfnResponse::Failed {
1195            reason: "Required failure reason string".to_owned(),
1196            request_id: "unique id for this delete request (copied from request)".to_owned(),
1197            logical_resource_id: "name of resource in template (copied from request)".to_owned(),
1198            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)".to_owned(),
1199            physical_resource_id: "custom resource provider-defined physical id".to_owned(),
1200        }).unwrap();
1201
1202        assert_eq!(expected_response, actual_response);
1203    }
1204
1205    #[test]
1206    fn cfnrequest_update_example() {
1207        #[derive(Debug, PartialEq, Clone, Deserialize)]
1208        struct ExampleProperties {
1209            key1: String,
1210            key2: Vec<String>,
1211            key3: serde_json::Value,
1212        }
1213
1214        let expected_request = CfnRequest::Update {
1215            request_id: "unique id for this update request".to_owned(),
1216            response_url: "pre-signed-url-for-update-response".to_owned(),
1217            resource_type: "Custom::MyCustomResourceType".to_owned(),
1218            logical_resource_id: "name of resource in template".to_owned(),
1219            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
1220            physical_resource_id: "custom resource provider-defined physical id".to_owned(),
1221            resource_properties: ExampleProperties {
1222                key1: "new-string".to_owned(),
1223                key2: vec!["new-list".to_owned()],
1224                key3: json!({ "key4": "new-map" }),
1225            },
1226            old_resource_properties: ExampleProperties {
1227                key1: "string".to_owned(),
1228                key2: vec!["list".to_owned()],
1229                key3: json!({ "key4": "map" }),
1230            },
1231        };
1232
1233        let actual_request: CfnRequest<ExampleProperties> = serde_json::from_value(json!({
1234            "RequestType" : "Update",
1235            "RequestId" : "unique id for this update request",
1236            "ResponseURL" : "pre-signed-url-for-update-response",
1237            "ResourceType" : "Custom::MyCustomResourceType",
1238            "LogicalResourceId" : "name of resource in template",
1239            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
1240            "PhysicalResourceId" : "custom resource provider-defined physical id",
1241            "ResourceProperties" : {
1242                "key1" : "new-string",
1243                "key2" : [ "new-list" ],
1244                "key3" : { "key4" : "new-map" }
1245            },
1246            "OldResourceProperties" : {
1247                "key1" : "string",
1248                "key2" : [ "list" ],
1249                "key3" : { "key4" : "map" }
1250            }
1251        })).unwrap();
1252
1253        assert_eq!(expected_request, actual_request);
1254    }
1255
1256    #[test]
1257    fn cfnresponse_update_success_example() {
1258        let expected_response = json!({
1259            "Status" : "SUCCESS",
1260            "RequestId" : "unique id for this update request (copied from request)",
1261            "LogicalResourceId" : "name of resource in template (copied from request)",
1262            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
1263            "PhysicalResourceId" : "custom resource provider-defined physical id",
1264            "Data" : {
1265                "keyThatCanBeUsedInGetAtt1" : "data for key 1",
1266                "keyThatCanBeUsedInGetAtt2" : "data for key 2"
1267            }
1268        });
1269
1270        let actual_response = serde_json::to_value(CfnResponse::Success {
1271            request_id: "unique id for this update request (copied from request)".to_owned(),
1272            logical_resource_id: "name of resource in template (copied from request)".to_owned(),
1273            physical_resource_id: "custom resource provider-defined physical id".to_owned(),
1274            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)".to_owned(),
1275            no_echo: None,
1276            data: Some(json!({
1277               "keyThatCanBeUsedInGetAtt1" : "data for key 1",
1278               "keyThatCanBeUsedInGetAtt2" : "data for key 2"
1279            })),
1280        }).unwrap();
1281
1282        assert_eq!(expected_response, actual_response);
1283    }
1284
1285    #[test]
1286    fn cfnresponse_update_failed_example() {
1287        let expected_response = json!({
1288            "Status" : "FAILED",
1289            "Reason" : "Required failure reason string",
1290            "RequestId" : "unique id for this update request (copied from request)",
1291            "LogicalResourceId" : "name of resource in template (copied from request)",
1292            "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
1293            "PhysicalResourceId" : "custom resource provider-defined physical id"
1294        });
1295
1296        let actual_response = serde_json::to_value(CfnResponse::Failed {
1297            reason: "Required failure reason string".to_owned(),
1298            request_id: "unique id for this update request (copied from request)".to_owned(),
1299            logical_resource_id: "name of resource in template (copied from request)".to_owned(),
1300            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)".to_owned(),
1301            physical_resource_id: "custom resource provider-defined physical id".to_owned(),
1302        }).unwrap();
1303
1304        assert_eq!(expected_response, actual_response);
1305    }
1306
1307    #[test]
1308    fn cfnresponse_from_cfnrequest_unit() {
1309        let actual_request: CfnRequest<Ignored> = CfnRequest::Create {
1310            request_id: "unique id for this create request".to_owned(),
1311            response_url: "pre-signed-url-for-create-response".to_owned(),
1312            resource_type: "Custom::MyCustomResourceType".to_owned(),
1313            logical_resource_id: "name of resource in template".to_owned(),
1314            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
1315            resource_properties: Ignored,
1316        };
1317        let actual_response =
1318            serde_json::to_value(actual_request.into_response(&Ok(None::<()>))).unwrap();
1319        let expected_response = json!({
1320            "Status": "SUCCESS",
1321            "RequestId": "unique id for this create request",
1322            "LogicalResourceId": "name of resource in template",
1323            "StackId": "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
1324            "PhysicalResourceId": "arn:custom:cfn-resource-provider:::guid-name of resource in template"
1325        });
1326
1327        assert_eq!(actual_response, expected_response)
1328    }
1329
1330    #[test]
1331    fn cfnresponse_from_cfnrequest_serializable() {
1332        let actual_request: CfnRequest<Ignored> = CfnRequest::Create {
1333            request_id: "unique id for this create request".to_owned(),
1334            response_url: "pre-signed-url-for-create-response".to_owned(),
1335            resource_type: "Custom::MyCustomResourceType".to_owned(),
1336            logical_resource_id: "name of resource in template".to_owned(),
1337            stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
1338            resource_properties: Ignored,
1339        };
1340        let actual_response =
1341            serde_json::to_value(actual_request.into_response(&Ok(Some(ExampleProperties {
1342                example_property_1: "example return property 1".to_owned(),
1343                example_property_2: None,
1344            })))).unwrap();
1345        let expected_response = json!({
1346            "Status": "SUCCESS",
1347            "RequestId": "unique id for this create request",
1348            "LogicalResourceId": "name of resource in template",
1349            "StackId": "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
1350            "PhysicalResourceId": "arn:custom:cfn-resource-provider:::guid-name of resource in template",
1351            "Data": {
1352                "ExampleProperty1": "example return property 1",
1353                "ExampleProperty2": null,
1354            }
1355        });
1356
1357        assert_eq!(actual_response, expected_response)
1358    }
1359}