1#![deny(missing_docs)]
2
3extern 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
62pub trait PhysicalResourceIdSuffixProvider {
112 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#[derive(Debug, Clone, PartialEq, Deserialize)]
235#[serde(tag = "RequestType")]
236pub enum CfnRequest<P>
237where
238 P: Clone,
239{
240 #[serde(rename_all = "PascalCase")]
246 Create {
247 request_id: String,
249 #[serde(rename = "ResponseURL")]
252 response_url: String,
253 resource_type: String,
257 logical_resource_id: String,
260 stack_id: String,
263 resource_properties: P,
266 },
267 #[serde(rename_all = "PascalCase")]
274 Delete {
275 request_id: String,
277 #[serde(rename = "ResponseURL")]
280 response_url: String,
281 resource_type: String,
285 logical_resource_id: String,
288 stack_id: String,
291 physical_resource_id: String,
294 resource_properties: P,
297 },
298 #[serde(rename_all = "PascalCase")]
305 Update {
306 request_id: String,
308 #[serde(rename = "ResponseURL")]
311 response_url: String,
312 resource_type: String,
316 logical_resource_id: String,
319 stack_id: String,
322 physical_resource_id: String,
325 resource_properties: P,
328 old_resource_properties: P,
331 },
332}
333
334impl<P> CfnRequest<P>
335where
336 P: PhysicalResourceIdSuffixProvider + Clone,
337{
338 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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#[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#[derive(Debug, Clone, PartialEq, Serialize)]
543#[serde(tag = "Status", rename_all = "SCREAMING_SNAKE_CASE")]
544pub enum CfnResponse {
545 #[serde(rename_all = "PascalCase")]
550 Success {
551 request_id: String,
554 logical_resource_id: String,
558 stack_id: String,
561 physical_resource_id: String,
565 #[serde(skip_serializing_if = "Option::is_none")]
569 no_echo: Option<bool>,
570 #[serde(skip_serializing_if = "Option::is_none")]
574 data: Option<serde_json::Value>,
575 },
576 #[serde(rename_all = "PascalCase")]
580 Failed {
581 reason: String,
583 request_id: String,
586 logical_resource_id: String,
590 stack_id: String,
593 physical_resource_id: String,
597 },
598}
599
600pub 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 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 #[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 #[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 #[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 #[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 #[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}