1use base64::Engine;
2use serde_json::{json, Value};
3use std::collections::{BTreeMap, BTreeSet};
4
5const NO_VALUE_SENTINEL_KEY: &str = "__fakecloud_aws_no_value__";
12
13#[derive(Debug, Clone, Default)]
15pub struct ParsedTemplate {
16 pub description: Option<String>,
17 pub resources: Vec<ResourceDefinition>,
18 pub outputs: Vec<TemplateOutput>,
19}
20
21#[derive(Debug, Clone)]
25pub struct TemplateOutput {
26 pub logical_id: String,
27 pub value: String,
28 pub description: Option<String>,
29 pub export_name: Option<String>,
30}
31
32#[derive(Debug, Clone)]
34pub struct ResourceDefinition {
35 pub logical_id: String,
36 pub resource_type: String,
37 pub properties: Value,
38}
39
40const PSEUDO_REFS: &[&str] = &[
42 "AWS::AccountId",
43 "AWS::NotificationARNs",
44 "AWS::NoValue",
45 "AWS::Partition",
46 "AWS::Region",
47 "AWS::StackId",
48 "AWS::StackName",
49 "AWS::URLSuffix",
50];
51
52type Mappings = BTreeMap<String, BTreeMap<String, BTreeMap<String, Value>>>;
53
54pub(crate) fn partition_for_region(region: &str) -> &'static str {
59 if region.starts_with("cn-") {
60 "aws-cn"
61 } else if region.starts_with("us-gov-") {
62 "aws-us-gov"
63 } else {
64 "aws"
65 }
66}
67
68pub(crate) fn url_suffix_for_region(region: &str) -> &'static str {
72 if region.starts_with("cn-") {
73 "amazonaws.com.cn"
74 } else {
75 "amazonaws.com"
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn parse_json_template() {
85 let template = r#"{
86 "Resources": {
87 "MyQueue": {
88 "Type": "AWS::SQS::Queue",
89 "Properties": {
90 "QueueName": "test-queue"
91 }
92 }
93 }
94 }"#;
95
96 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
97 assert_eq!(parsed.resources.len(), 1);
98 assert_eq!(parsed.resources[0].logical_id, "MyQueue");
99 assert_eq!(parsed.resources[0].resource_type, "AWS::SQS::Queue");
100 }
101
102 #[test]
103 fn parse_yaml_template() {
104 let template = r#"
105Resources:
106 MyTopic:
107 Type: AWS::SNS::Topic
108 Properties:
109 TopicName: test-topic
110"#;
111
112 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
113 assert_eq!(parsed.resources.len(), 1);
114 assert_eq!(parsed.resources[0].logical_id, "MyTopic");
115 assert_eq!(parsed.resources[0].resource_type, "AWS::SNS::Topic");
116 }
117
118 #[test]
119 fn resolve_ref_parameters() {
120 let template = r#"{
121 "Resources": {
122 "MyQueue": {
123 "Type": "AWS::SQS::Queue",
124 "Properties": {
125 "QueueName": { "Ref": "QueueNameParam" }
126 }
127 }
128 }
129 }"#;
130
131 let mut params = BTreeMap::new();
132 params.insert("QueueNameParam".to_string(), "resolved-queue".to_string());
133 let parsed = parse_template(template, ¶ms).unwrap();
134 assert_eq!(
135 parsed.resources[0].properties["QueueName"],
136 Value::String("resolved-queue".to_string())
137 );
138 }
139
140 #[test]
141 fn ref_resolves_physical_id_over_logical_id() {
142 let template = r#"{
143 "Resources": {
144 "MyTopic": {
145 "Type": "AWS::SNS::Topic",
146 "Properties": {
147 "TopicName": "my-topic"
148 }
149 },
150 "MySub": {
151 "Type": "AWS::SNS::Subscription",
152 "Properties": {
153 "TopicArn": { "Ref": "MyTopic" },
154 "Protocol": "sqs",
155 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:q"
156 }
157 }
158 }
159 }"#;
160
161 let mut physical_ids = BTreeMap::new();
162 physical_ids.insert(
163 "MyTopic".to_string(),
164 "arn:aws:sns:us-east-1:123456789012:my-topic".to_string(),
165 );
166
167 let parsed =
168 parse_template_with_physical_ids(template, &BTreeMap::new(), &physical_ids).unwrap();
169 let sub = parsed
170 .resources
171 .iter()
172 .find(|r| r.logical_id == "MySub")
173 .unwrap();
174 assert_eq!(
175 sub.properties["TopicArn"],
176 Value::String("arn:aws:sns:us-east-1:123456789012:my-topic".to_string())
177 );
178 }
179
180 #[test]
181 fn ref_without_physical_id_returns_logical_id_for_known_resource() {
182 let template = r#"{
183 "Resources": {
184 "MyTopic": {
185 "Type": "AWS::SNS::Topic",
186 "Properties": {
187 "TopicName": "my-topic"
188 }
189 },
190 "MySub": {
191 "Type": "AWS::SNS::Subscription",
192 "Properties": {
193 "TopicArn": { "Ref": "MyTopic" },
194 "Protocol": "sqs",
195 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:q"
196 }
197 }
198 }
199 }"#;
200
201 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
203 let sub = parsed
204 .resources
205 .iter()
206 .find(|r| r.logical_id == "MySub")
207 .unwrap();
208 assert_eq!(
209 sub.properties["TopicArn"],
210 Value::String("MyTopic".to_string())
211 );
212 }
213
214 #[test]
215 fn pseudo_ref_substitutes_when_param_provided() {
216 let template = r#"{
217 "Resources": {
218 "MyQueue": {
219 "Type": "AWS::SQS::Queue",
220 "Properties": {
221 "QueueArn": {
222 "Fn::Join": ["", [
223 "arn:", {"Ref": "AWS::Partition"}, ":sqs:",
224 {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"},
225 ":", {"Ref": "AWS::StackName"}, "-q"
226 ]]
227 }
228 }
229 }
230 }
231 }"#;
232 let mut params = BTreeMap::new();
233 params.insert("AWS::Region".to_string(), "us-west-2".to_string());
234 params.insert("AWS::AccountId".to_string(), "111122223333".to_string());
235 params.insert("AWS::Partition".to_string(), "aws".to_string());
236 params.insert("AWS::StackName".to_string(), "demo".to_string());
237
238 let parsed = parse_template(template, ¶ms).unwrap();
239 assert_eq!(
240 parsed.resources[0].properties["QueueArn"],
241 Value::String("arn:aws:sqs:us-west-2:111122223333:demo-q".to_string())
242 );
243 }
244
245 #[test]
246 fn pseudo_ref_partition_default_when_unset() {
247 let template = r#"{
248 "Resources": {
249 "MyQueue": {
250 "Type": "AWS::SQS::Queue",
251 "Properties": {
252 "Partition": {"Ref": "AWS::Partition"},
253 "Suffix": {"Ref": "AWS::URLSuffix"}
254 }
255 }
256 }
257 }"#;
258 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
259 assert_eq!(
260 parsed.resources[0].properties["Partition"],
261 Value::String("aws".to_string())
262 );
263 assert_eq!(
264 parsed.resources[0].properties["Suffix"],
265 Value::String("amazonaws.com".to_string())
266 );
267 }
268
269 #[test]
270 fn pseudo_ref_passes_through() {
271 let template = r#"{
272 "Resources": {
273 "MyQueue": {
274 "Type": "AWS::SQS::Queue",
275 "Properties": {
276 "QueueName": { "Ref": "AWS::StackName" }
277 }
278 }
279 }
280 }"#;
281
282 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
283 assert_eq!(
284 parsed.resources[0].properties["QueueName"],
285 Value::String("AWS::StackName".to_string())
286 );
287 }
288
289 #[test]
292 fn bb6_ref_aws_region_returns_seeded_region() {
293 let template = r#"{
294 "Resources": {
295 "Q": {
296 "Type": "AWS::SQS::Queue",
297 "Properties": {"Region": {"Ref": "AWS::Region"}}
298 }
299 }
300 }"#;
301 let mut params = BTreeMap::new();
302 params.insert("AWS::Region".to_string(), "us-east-1".to_string());
303 let parsed = parse_template(template, ¶ms).unwrap();
304 assert_eq!(
305 parsed.resources[0].properties["Region"],
306 Value::String("us-east-1".to_string())
307 );
308 }
309
310 #[test]
311 fn bb6_fn_sub_substitutes_aws_account_id() {
312 let template = r#"{
313 "Resources": {
314 "Q": {
315 "Type": "AWS::SQS::Queue",
316 "Properties": {
317 "Owner": {"Fn::Sub": "owner-${AWS::AccountId}"}
318 }
319 }
320 }
321 }"#;
322 let mut params = BTreeMap::new();
323 params.insert("AWS::AccountId".to_string(), "123456789012".to_string());
324 let parsed = parse_template(template, ¶ms).unwrap();
325 assert_eq!(
326 parsed.resources[0].properties["Owner"],
327 Value::String("owner-123456789012".to_string())
328 );
329 }
330
331 #[test]
332 fn bb6_partition_for_china_region_is_aws_cn() {
333 let template = r#"{
336 "Resources": {
337 "Q": {
338 "Type": "AWS::SQS::Queue",
339 "Properties": {"P": {"Ref": "AWS::Partition"}}
340 }
341 }
342 }"#;
343 let mut params = BTreeMap::new();
344 params.insert("AWS::Region".to_string(), "cn-north-1".to_string());
345 let parsed = parse_template(template, ¶ms).unwrap();
346 assert_eq!(
347 parsed.resources[0].properties["P"],
348 Value::String("aws-cn".to_string())
349 );
350 }
351
352 #[test]
353 fn bb6_partition_for_govcloud_region_is_aws_us_gov() {
354 let template = r#"{
355 "Resources": {
356 "Q": {
357 "Type": "AWS::SQS::Queue",
358 "Properties": {"P": {"Ref": "AWS::Partition"}}
359 }
360 }
361 }"#;
362 let mut params = BTreeMap::new();
363 params.insert("AWS::Region".to_string(), "us-gov-west-1".to_string());
364 let parsed = parse_template(template, ¶ms).unwrap();
365 assert_eq!(
366 parsed.resources[0].properties["P"],
367 Value::String("aws-us-gov".to_string())
368 );
369 }
370
371 #[test]
372 fn bb6_url_suffix_for_china_is_amazonaws_com_cn() {
373 let template = r#"{
374 "Resources": {
375 "Q": {
376 "Type": "AWS::SQS::Queue",
377 "Properties": {"S": {"Ref": "AWS::URLSuffix"}}
378 }
379 }
380 }"#;
381 let mut params = BTreeMap::new();
382 params.insert("AWS::Region".to_string(), "cn-north-1".to_string());
383 let parsed = parse_template(template, ¶ms).unwrap();
384 assert_eq!(
385 parsed.resources[0].properties["S"],
386 Value::String("amazonaws.com.cn".to_string())
387 );
388 }
389
390 #[test]
391 fn bb6_url_suffix_for_govcloud_stays_amazonaws_com() {
392 let template = r#"{
394 "Resources": {
395 "Q": {
396 "Type": "AWS::SQS::Queue",
397 "Properties": {"S": {"Ref": "AWS::URLSuffix"}}
398 }
399 }
400 }"#;
401 let mut params = BTreeMap::new();
402 params.insert("AWS::Region".to_string(), "us-gov-east-1".to_string());
403 let parsed = parse_template(template, ¶ms).unwrap();
404 assert_eq!(
405 parsed.resources[0].properties["S"],
406 Value::String("amazonaws.com".to_string())
407 );
408 }
409
410 #[test]
411 fn bb6_no_value_omits_property_from_resource_input() {
412 let template = r#"{
415 "Resources": {
416 "Q": {
417 "Type": "AWS::SQS::Queue",
418 "Properties": {
419 "QueueName": "q",
420 "OptionalProp": {"Ref": "AWS::NoValue"}
421 }
422 }
423 }
424 }"#;
425 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
426 let props = parsed.resources[0].properties.as_object().unwrap();
427 assert!(
428 !props.contains_key("OptionalProp"),
429 "OptionalProp should be omitted, got: {props:?}"
430 );
431 assert_eq!(
432 props.get("QueueName"),
433 Some(&Value::String("q".to_string()))
434 );
435 }
436
437 #[test]
438 fn bb6_notification_arns_returns_seeded_array() {
439 let template = r#"{
443 "Resources": {
444 "Q": {
445 "Type": "AWS::SQS::Queue",
446 "Properties": {"Targets": {"Ref": "AWS::NotificationARNs"}}
447 }
448 }
449 }"#;
450 let mut params = BTreeMap::new();
451 params.insert(
452 "AWS::NotificationARNs".to_string(),
453 r#"["arn:aws:sns:us-east-1:111122223333:topic"]"#.to_string(),
454 );
455 let parsed = parse_template(template, ¶ms).unwrap();
456 assert_eq!(
457 parsed.resources[0].properties["Targets"],
458 serde_json::json!(["arn:aws:sns:us-east-1:111122223333:topic"])
459 );
460 }
461
462 #[test]
463 fn bb6_notification_arns_defaults_to_empty_array() {
464 let template = r#"{
465 "Resources": {
466 "Q": {
467 "Type": "AWS::SQS::Queue",
468 "Properties": {"Targets": {"Ref": "AWS::NotificationARNs"}}
469 }
470 }
471 }"#;
472 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
473 assert_eq!(
474 parsed.resources[0].properties["Targets"],
475 serde_json::json!([])
476 );
477 }
478
479 #[test]
480 fn bb6_fn_sub_array_form_substitutes_extra_vars() {
481 let template = r#"{
484 "Resources": {
485 "Q": {
486 "Type": "AWS::SQS::Queue",
487 "Properties": {
488 "Path": {"Fn::Sub": ["${AWS::Region}/${Suffix}", {"Suffix": "tail"}]}
489 }
490 }
491 }
492 }"#;
493 let mut params = BTreeMap::new();
494 params.insert("AWS::Region".to_string(), "eu-west-1".to_string());
495 let parsed = parse_template(template, ¶ms).unwrap();
496 assert_eq!(
497 parsed.resources[0].properties["Path"],
498 Value::String("eu-west-1/tail".to_string())
499 );
500 }
501
502 #[test]
503 fn bb6_partition_helper_classifies_regions() {
504 assert_eq!(partition_for_region("us-east-1"), "aws");
505 assert_eq!(partition_for_region("eu-central-1"), "aws");
506 assert_eq!(partition_for_region("cn-north-1"), "aws-cn");
507 assert_eq!(partition_for_region("cn-northwest-1"), "aws-cn");
508 assert_eq!(partition_for_region("us-gov-west-1"), "aws-us-gov");
509 assert_eq!(partition_for_region("us-gov-east-1"), "aws-us-gov");
510 }
511
512 #[test]
513 fn bb6_url_suffix_helper_classifies_regions() {
514 assert_eq!(url_suffix_for_region("us-east-1"), "amazonaws.com");
515 assert_eq!(url_suffix_for_region("us-gov-west-1"), "amazonaws.com");
516 assert_eq!(url_suffix_for_region("cn-north-1"), "amazonaws.com.cn");
517 }
518
519 #[test]
520 fn fn_sub_resolves_physical_ids() {
521 let template = r#"{
522 "Resources": {
523 "MyTopic": {
524 "Type": "AWS::SNS::Topic",
525 "Properties": {
526 "TopicName": "my-topic"
527 }
528 },
529 "MyParam": {
530 "Type": "AWS::SSM::Parameter",
531 "Properties": {
532 "Name": "/app/topic",
533 "Type": "String",
534 "Value": { "Fn::Sub": "Topic is ${MyTopic}" }
535 }
536 }
537 }
538 }"#;
539
540 let mut physical_ids = BTreeMap::new();
541 physical_ids.insert(
542 "MyTopic".to_string(),
543 "arn:aws:sns:us-east-1:123456789012:my-topic".to_string(),
544 );
545
546 let parsed =
547 parse_template_with_physical_ids(template, &BTreeMap::new(), &physical_ids).unwrap();
548 let param = parsed
549 .resources
550 .iter()
551 .find(|r| r.logical_id == "MyParam")
552 .unwrap();
553 assert_eq!(
554 param.properties["Value"],
555 Value::String("Topic is arn:aws:sns:us-east-1:123456789012:my-topic".to_string())
556 );
557 }
558
559 #[test]
562 fn parse_template_invalid_json_errors() {
563 let params = BTreeMap::new();
564 let result = parse_template("{not-json}", ¶ms);
565 assert!(result.is_err());
566 }
567
568 #[test]
569 fn parse_template_missing_resources_errors() {
570 let params = BTreeMap::new();
571 let result = parse_template(r#"{"Description":"no resources"}"#, ¶ms);
572 assert!(result.is_err());
573 }
574
575 #[test]
576 fn parse_template_resources_not_object_errors() {
577 let params = BTreeMap::new();
578 let result = parse_template(r#"{"Resources": []}"#, ¶ms);
579 assert!(result.is_err());
580 }
581
582 #[test]
583 fn parse_template_missing_type_errors() {
584 let params = BTreeMap::new();
585 let result = parse_template(r#"{"Resources":{"R":{"Properties":{}}}}"#, ¶ms);
586 assert!(result.is_err());
587 }
588
589 #[test]
592 fn fn_getatt_resolves_attribute_in_array_form() {
593 let template = r#"{
594 "Resources": {
595 "MyQueue": {
596 "Type": "AWS::SQS::Queue",
597 "Properties": { "QueueName": "q1" }
598 },
599 "MyTopic": {
600 "Type": "AWS::SNS::Topic",
601 "Properties": {
602 "TopicName": "t1",
603 "DataProtectionPolicy": {
604 "Fn::GetAtt": ["MyQueue", "Arn"]
605 }
606 }
607 }
608 }
609 }"#;
610
611 let mut attrs = BTreeMap::new();
612 let mut q_attrs = BTreeMap::new();
613 q_attrs.insert(
614 "Arn".to_string(),
615 "arn:aws:sqs:us-east-1:123456789012:q1".to_string(),
616 );
617 attrs.insert("MyQueue".to_string(), q_attrs);
618
619 let parsed =
620 parse_template_with_resolution(template, &BTreeMap::new(), &BTreeMap::new(), &attrs)
621 .unwrap();
622 let topic = parsed
623 .resources
624 .iter()
625 .find(|r| r.logical_id == "MyTopic")
626 .unwrap();
627 assert_eq!(
628 topic.properties["DataProtectionPolicy"],
629 Value::String("arn:aws:sqs:us-east-1:123456789012:q1".to_string())
630 );
631 }
632
633 #[test]
634 fn fn_getatt_resolves_attribute_in_short_string_form() {
635 let template = r#"{
636 "Resources": {
637 "MyTopic": {
638 "Type": "AWS::SNS::Topic",
639 "Properties": {
640 "TopicName": "t1",
641 "PolicyArn": { "Fn::GetAtt": "MyQueue.Arn" }
642 }
643 }
644 }
645 }"#;
646
647 let mut attrs = BTreeMap::new();
648 let mut q_attrs = BTreeMap::new();
649 q_attrs.insert(
650 "Arn".to_string(),
651 "arn:aws:sqs:us-east-1:123456789012:q1".to_string(),
652 );
653 attrs.insert("MyQueue".to_string(), q_attrs);
654
655 let parsed =
656 parse_template_with_resolution(template, &BTreeMap::new(), &BTreeMap::new(), &attrs)
657 .unwrap();
658 assert_eq!(
659 parsed.resources[0].properties["PolicyArn"],
660 Value::String("arn:aws:sqs:us-east-1:123456789012:q1".to_string())
661 );
662 }
663
664 #[test]
665 fn fn_getatt_unknown_resource_returns_placeholder() {
666 let template = r#"{
667 "Resources": {
668 "MyTopic": {
669 "Type": "AWS::SNS::Topic",
670 "Properties": {
671 "TopicName": { "Fn::GetAtt": ["MyQueue", "Arn"] }
672 }
673 }
674 }
675 }"#;
676
677 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
678 assert_eq!(
681 parsed.resources[0].properties["TopicName"],
682 Value::String("MyQueue.Arn".to_string())
683 );
684 }
685
686 #[test]
687 fn fn_getatt_inside_fn_join_resolves() {
688 let template = r#"{
689 "Resources": {
690 "MyParam": {
691 "Type": "AWS::SSM::Parameter",
692 "Properties": {
693 "Name": "/app/q",
694 "Type": "String",
695 "Value": {
696 "Fn::Join": [":", ["queue", { "Fn::GetAtt": ["MyQueue", "Arn"] }]]
697 }
698 }
699 }
700 }
701 }"#;
702
703 let mut attrs = BTreeMap::new();
704 let mut q_attrs = BTreeMap::new();
705 q_attrs.insert(
706 "Arn".to_string(),
707 "arn:aws:sqs:us-east-1:123456789012:q1".to_string(),
708 );
709 attrs.insert("MyQueue".to_string(), q_attrs);
710
711 let parsed =
712 parse_template_with_resolution(template, &BTreeMap::new(), &BTreeMap::new(), &attrs)
713 .unwrap();
714 assert_eq!(
715 parsed.resources[0].properties["Value"],
716 Value::String("queue:arn:aws:sqs:us-east-1:123456789012:q1".to_string())
717 );
718 }
719
720 #[test]
721 fn fn_sub_resolves_getatt_style_substitution() {
722 let template = r#"{
723 "Resources": {
724 "MyParam": {
725 "Type": "AWS::SSM::Parameter",
726 "Properties": {
727 "Name": "/app/q",
728 "Type": "String",
729 "Value": { "Fn::Sub": "Queue arn is ${MyQueue.Arn}" }
730 }
731 }
732 }
733 }"#;
734
735 let mut attrs = BTreeMap::new();
736 let mut q_attrs = BTreeMap::new();
737 q_attrs.insert(
738 "Arn".to_string(),
739 "arn:aws:sqs:us-east-1:123456789012:q1".to_string(),
740 );
741 attrs.insert("MyQueue".to_string(), q_attrs);
742
743 let parsed =
744 parse_template_with_resolution(template, &BTreeMap::new(), &BTreeMap::new(), &attrs)
745 .unwrap();
746 assert_eq!(
747 parsed.resources[0].properties["Value"],
748 Value::String("Queue arn is arn:aws:sqs:us-east-1:123456789012:q1".to_string())
749 );
750 }
751
752 #[test]
753 fn parse_template_with_description() {
754 let params = BTreeMap::new();
755 let parsed = parse_template(
756 r#"{"Description":"My template","Resources":{"R":{"Type":"AWS::SQS::Queue"}}}"#,
757 ¶ms,
758 )
759 .unwrap();
760 assert_eq!(parsed.description.as_deref(), Some("My template"));
761 assert_eq!(parsed.resources.len(), 1);
762 }
763
764 type EmptyCtx = (
765 BTreeMap<String, String>,
766 serde_json::Map<String, Value>,
767 BTreeMap<String, String>,
768 BTreeMap<String, BTreeMap<String, String>>,
769 );
770
771 fn empty() -> EmptyCtx {
772 (
773 BTreeMap::new(),
774 serde_json::Map::new(),
775 BTreeMap::new(),
776 BTreeMap::new(),
777 )
778 }
779
780 #[test]
781 fn fn_base64_encodes_string() {
782 let (p, r, ids, attrs) = empty();
783 let v: Value = serde_json::from_str(r#"{"Fn::Base64": "hello"}"#).unwrap();
784 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
785 assert_eq!(resolved, Value::String("aGVsbG8=".to_string()));
786 }
787
788 #[test]
789 fn ref_on_lambda_alias_resolves_to_alias_arn() {
790 let (p, r, mut ids, attrs) = empty();
795 let alias_arn = "arn:aws:lambda:us-east-1:123456789012:function:my-func:live";
796 ids.insert("Alias".to_string(), alias_arn.to_string());
797 let v: Value = serde_json::from_str(r#"{"Ref": "Alias"}"#).unwrap();
798 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
799 assert_eq!(resolved, Value::String(alias_arn.to_string()));
800 }
801
802 #[test]
803 fn fn_split_emits_array() {
804 let (p, r, ids, attrs) = empty();
805 let v: Value = serde_json::from_str(r#"{"Fn::Split": [",", "a,b,c"]}"#).unwrap();
806 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
807 assert_eq!(resolved, serde_json::json!(["a", "b", "c"]));
808 }
809
810 #[test]
811 fn fn_select_picks_index() {
812 let (p, r, ids, attrs) = empty();
813 let v: Value =
814 serde_json::from_str(r#"{"Fn::Select": [1, {"Fn::Split": [",", "a,b,c"]}]}"#).unwrap();
815 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
816 assert_eq!(resolved, Value::String("b".to_string()));
817 }
818
819 #[test]
820 fn fn_length_counts_array() {
821 let (p, r, ids, attrs) = empty();
822 let v: Value = serde_json::from_str(r#"{"Fn::Length": [1,2,3,4]}"#).unwrap();
823 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
824 assert_eq!(resolved, Value::Number(4.into()));
825 }
826
827 #[test]
828 fn fn_to_json_string_serializes() {
829 let (p, r, ids, attrs) = empty();
830 let v: Value =
831 serde_json::from_str(r#"{"Fn::ToJsonString": {"a": 1, "b": [2, 3]}}"#).unwrap();
832 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
833 let s = resolved.as_str().unwrap();
834 let parsed: Value = serde_json::from_str(s).unwrap();
836 assert_eq!(parsed["a"], serde_json::json!(1));
837 assert_eq!(parsed["b"], serde_json::json!([2, 3]));
838 }
839
840 #[test]
841 fn fn_cidr_carves_subnets() {
842 let (p, r, ids, attrs) = empty();
843 let v: Value = serde_json::from_str(r#"{"Fn::Cidr": ["10.0.0.0/16", 4, 8]}"#).unwrap();
845 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
846 assert_eq!(
847 resolved,
848 serde_json::json!(["10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24",])
849 );
850 }
851
852 #[test]
853 fn condition_skips_resource_when_false() {
854 let template = r#"{
855 "Parameters": {"Env": {"Type": "String"}},
856 "Conditions": {
857 "IsProd": {"Fn::Equals": [{"Ref": "Env"}, "prod"]}
858 },
859 "Resources": {
860 "ProdQueue": {
861 "Type": "AWS::SQS::Queue",
862 "Condition": "IsProd",
863 "Properties": {"QueueName": "prod-q"}
864 },
865 "AlwaysQueue": {
866 "Type": "AWS::SQS::Queue",
867 "Properties": {"QueueName": "always-q"}
868 }
869 }
870 }"#;
871 let mut params = BTreeMap::new();
872 params.insert("Env".to_string(), "dev".to_string());
873 let parsed = parse_template(template, ¶ms).unwrap();
874 let names: Vec<&str> = parsed
875 .resources
876 .iter()
877 .map(|r| r.logical_id.as_str())
878 .collect();
879 assert!(names.contains(&"AlwaysQueue"));
880 assert!(!names.contains(&"ProdQueue"));
881 }
882
883 #[test]
884 fn condition_includes_resource_when_true() {
885 let template = r#"{
886 "Parameters": {"Env": {"Type": "String"}},
887 "Conditions": {
888 "IsProd": {"Fn::Equals": [{"Ref": "Env"}, "prod"]}
889 },
890 "Resources": {
891 "ProdQueue": {
892 "Type": "AWS::SQS::Queue",
893 "Condition": "IsProd",
894 "Properties": {"QueueName": "prod-q"}
895 }
896 }
897 }"#;
898 let mut params = BTreeMap::new();
899 params.insert("Env".to_string(), "prod".to_string());
900 let parsed = parse_template(template, ¶ms).unwrap();
901 assert_eq!(parsed.resources.len(), 1);
902 }
903
904 #[test]
905 fn fn_if_picks_branch_based_on_condition() {
906 let template = r#"{
907 "Parameters": {"Env": {"Type": "String"}},
908 "Conditions": {
909 "IsProd": {"Fn::Equals": [{"Ref": "Env"}, "prod"]}
910 },
911 "Resources": {
912 "Q": {
913 "Type": "AWS::SQS::Queue",
914 "Properties": {
915 "QueueName": {"Fn::If": ["IsProd", "prod-q", "dev-q"]}
916 }
917 }
918 }
919 }"#;
920 let mut params = BTreeMap::new();
921 params.insert("Env".to_string(), "dev".to_string());
922 let parsed = parse_template(template, ¶ms).unwrap();
923 assert_eq!(
924 parsed.resources[0].properties["QueueName"],
925 Value::String("dev-q".to_string())
926 );
927 }
928
929 #[test]
930 fn fn_and_or_not_combine_conditions() {
931 let template = r#"{
932 "Parameters": {"Env": {"Type": "String"}, "Region": {"Type": "String"}},
933 "Conditions": {
934 "IsProd": {"Fn::Equals": [{"Ref": "Env"}, "prod"]},
935 "IsUsEast": {"Fn::Equals": [{"Ref": "Region"}, "us-east-1"]},
936 "IsProdInUsEast": {"Fn::And": [{"Condition": "IsProd"}, {"Condition": "IsUsEast"}]},
937 "IsNotProd": {"Fn::Not": [{"Condition": "IsProd"}]},
938 "IsAny": {"Fn::Or": [{"Condition": "IsProd"}, {"Condition": "IsNotProd"}]}
939 },
940 "Resources": {
941 "Q": {
942 "Type": "AWS::SQS::Queue",
943 "Properties": {
944 "P1": {"Fn::If": ["IsProdInUsEast", "yes", "no"]},
945 "P2": {"Fn::If": ["IsNotProd", "yes", "no"]},
946 "P3": {"Fn::If": ["IsAny", "yes", "no"]}
947 }
948 }
949 }
950 }"#;
951 let mut params = BTreeMap::new();
952 params.insert("Env".to_string(), "prod".to_string());
953 params.insert("Region".to_string(), "us-east-1".to_string());
954 let parsed = parse_template(template, ¶ms).unwrap();
955 let p = &parsed.resources[0].properties;
956 assert_eq!(p["P1"], Value::String("yes".to_string()));
957 assert_eq!(p["P2"], Value::String("no".to_string()));
958 assert_eq!(p["P3"], Value::String("yes".to_string()));
959 }
960
961 #[test]
962 fn fn_find_in_map_resolves_leaf_value() {
963 let template = r#"{
964 "Mappings": {
965 "RegionMap": {
966 "us-east-1": {"AMI": "ami-east"},
967 "us-west-2": {"AMI": "ami-west"}
968 }
969 },
970 "Resources": {
971 "Inst": {
972 "Type": "AWS::EC2::Instance",
973 "Properties": {
974 "ImageId": {"Fn::FindInMap": ["RegionMap", "us-east-1", "AMI"]}
975 }
976 }
977 }
978 }"#;
979 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
980 assert_eq!(
981 parsed.resources[0].properties["ImageId"],
982 Value::String("ami-east".to_string())
983 );
984 }
985
986 #[test]
987 fn fn_find_in_map_resolves_keys_via_ref() {
988 let template = r#"{
989 "Parameters": {"Region": {"Type": "String"}},
990 "Mappings": {
991 "RegionMap": {
992 "us-east-1": {"AMI": "ami-east"},
993 "us-west-2": {"AMI": "ami-west"}
994 }
995 },
996 "Resources": {
997 "Inst": {
998 "Type": "AWS::EC2::Instance",
999 "Properties": {
1000 "ImageId": {"Fn::FindInMap": ["RegionMap", {"Ref": "Region"}, "AMI"]}
1001 }
1002 }
1003 }
1004 }"#;
1005 let mut params = BTreeMap::new();
1006 params.insert("Region".to_string(), "us-west-2".to_string());
1007 let parsed = parse_template(template, ¶ms).unwrap();
1008 assert_eq!(
1009 parsed.resources[0].properties["ImageId"],
1010 Value::String("ami-west".to_string())
1011 );
1012 }
1013
1014 #[test]
1015 fn fn_find_in_map_unknown_keys_returns_error() {
1016 let template = r#"{
1017 "Mappings": {
1018 "RegionMap": {
1019 "us-east-1": {"AMI": "ami-east"}
1020 }
1021 },
1022 "Resources": {
1023 "Inst": {
1024 "Type": "AWS::EC2::Instance",
1025 "Properties": {
1026 "ImageId": {"Fn::FindInMap": ["RegionMap", "ap-south-1", "AMI"]}
1027 }
1028 }
1029 }
1030 }"#;
1031 let err = parse_template(template, &BTreeMap::new()).unwrap_err();
1032 assert!(
1033 err.contains("Unable to get mapping for RegionMap::ap-south-1::AMI"),
1034 "got: {err}"
1035 );
1036 }
1037
1038 #[test]
1039 fn fn_find_in_map_four_arg_returns_default_when_missing() {
1040 let template = r#"{
1041 "Mappings": {
1042 "RegionMap": {
1043 "us-east-1": {"AMI": "ami-east"}
1044 }
1045 },
1046 "Resources": {
1047 "Inst": {
1048 "Type": "AWS::EC2::Instance",
1049 "Properties": {
1050 "ImageId": {"Fn::FindInMap": [
1051 "RegionMap",
1052 "ap-south-1",
1053 "AMI",
1054 {"DefaultValue": "ami-fallback"}
1055 ]}
1056 }
1057 }
1058 }
1059 }"#;
1060 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
1061 assert_eq!(
1062 parsed.resources[0].properties["ImageId"],
1063 Value::String("ami-fallback".to_string())
1064 );
1065 }
1066
1067 #[test]
1068 fn fn_find_in_map_four_arg_prefers_match_over_default() {
1069 let template = r#"{
1070 "Mappings": {
1071 "RegionMap": {
1072 "us-east-1": {"AMI": "ami-east"}
1073 }
1074 },
1075 "Resources": {
1076 "Inst": {
1077 "Type": "AWS::EC2::Instance",
1078 "Properties": {
1079 "ImageId": {"Fn::FindInMap": [
1080 "RegionMap",
1081 "us-east-1",
1082 "AMI",
1083 {"DefaultValue": "ami-fallback"}
1084 ]}
1085 }
1086 }
1087 }
1088 }"#;
1089 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
1090 assert_eq!(
1091 parsed.resources[0].properties["ImageId"],
1092 Value::String("ami-east".to_string())
1093 );
1094 }
1095
1096 #[test]
1097 fn fn_find_in_map_default_value_is_resolved_intrinsic() {
1098 let template = r#"{
1099 "Parameters": {"Fallback": {"Type": "String"}},
1100 "Mappings": {
1101 "RegionMap": {
1102 "us-east-1": {"AMI": "ami-east"}
1103 }
1104 },
1105 "Resources": {
1106 "Inst": {
1107 "Type": "AWS::EC2::Instance",
1108 "Properties": {
1109 "ImageId": {"Fn::FindInMap": [
1110 "RegionMap",
1111 "ap-south-1",
1112 "AMI",
1113 {"DefaultValue": {"Ref": "Fallback"}}
1114 ]}
1115 }
1116 }
1117 }
1118 }"#;
1119 let mut params = BTreeMap::new();
1120 params.insert("Fallback".to_string(), "ami-default".to_string());
1121 let parsed = parse_template(template, ¶ms).unwrap();
1122 assert_eq!(
1123 parsed.resources[0].properties["ImageId"],
1124 Value::String("ami-default".to_string())
1125 );
1126 }
1127
1128 #[test]
1129 fn fn_find_in_map_unknown_map_name_errors() {
1130 let template = r#"{
1131 "Mappings": {
1132 "RegionMap": {
1133 "us-east-1": {"AMI": "ami-east"}
1134 }
1135 },
1136 "Resources": {
1137 "Inst": {
1138 "Type": "AWS::EC2::Instance",
1139 "Properties": {
1140 "ImageId": {"Fn::FindInMap": ["DoesNotExist", "us-east-1", "AMI"]}
1141 }
1142 }
1143 }
1144 }"#;
1145 let err = parse_template(template, &BTreeMap::new()).unwrap_err();
1146 assert!(
1147 err.contains("Unable to get mapping for DoesNotExist::us-east-1::AMI"),
1148 "got: {err}"
1149 );
1150 }
1151
1152 #[test]
1153 fn fn_find_in_map_wrong_arg_count_errors() {
1154 let template = r#"{
1155 "Mappings": {"M": {"a": {"b": "c"}}},
1156 "Resources": {
1157 "Q": {
1158 "Type": "AWS::SQS::Queue",
1159 "Properties": {
1160 "QueueName": {"Fn::FindInMap": ["M", "a"]}
1161 }
1162 }
1163 }
1164 }"#;
1165 let err = parse_template(template, &BTreeMap::new()).unwrap_err();
1166 assert!(
1167 err.contains("Fn::FindInMap requires 3 or 4 arguments"),
1168 "got: {err}"
1169 );
1170 }
1171
1172 #[test]
1173 fn fn_find_in_map_resolves_via_pseudo_region() {
1174 let template = r#"{
1175 "Mappings": {
1176 "RegionMap": {
1177 "us-east-1": {"AMI": "ami-east"},
1178 "us-west-2": {"AMI": "ami-west"}
1179 }
1180 },
1181 "Resources": {
1182 "Inst": {
1183 "Type": "AWS::EC2::Instance",
1184 "Properties": {
1185 "ImageId": {"Fn::FindInMap": [
1186 "RegionMap",
1187 {"Ref": "AWS::Region"},
1188 "AMI"
1189 ]}
1190 }
1191 }
1192 }
1193 }"#;
1194 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
1197 assert_eq!(
1198 parsed.resources[0].properties["ImageId"],
1199 Value::String("ami-east".to_string())
1200 );
1201 }
1202
1203 #[test]
1204 fn fn_find_in_map_in_unused_if_branch_does_not_error() {
1205 let template = r#"{
1211 "Parameters": {"WantAlt": {"Type": "String"}},
1212 "Conditions": {
1213 "UseAlt": {"Fn::Equals": [{"Ref": "WantAlt"}, "yes"]}
1214 },
1215 "Mappings": {
1216 "RegionMap": {
1217 "us-east-1": {"AMI": "ami-east"}
1218 }
1219 },
1220 "Resources": {
1221 "Inst": {
1222 "Type": "AWS::EC2::Instance",
1223 "Properties": {
1224 "ImageId": {"Fn::If": [
1225 "UseAlt",
1226 {"Fn::FindInMap": ["RegionMap", "ap-south-1", "AMI"]},
1227 {"Fn::FindInMap": ["RegionMap", "us-east-1", "AMI"]}
1228 ]}
1229 }
1230 }
1231 }
1232 }"#;
1233 let mut params = BTreeMap::new();
1234 params.insert("WantAlt".to_string(), "no".to_string());
1235 let parsed = parse_template(template, ¶ms).unwrap();
1236 assert_eq!(
1237 parsed.resources[0].properties["ImageId"],
1238 Value::String("ami-east".to_string())
1239 );
1240 }
1241
1242 #[test]
1243 fn fn_find_in_map_in_active_if_branch_still_errors_on_miss() {
1244 let template = r#"{
1247 "Parameters": {"WantAlt": {"Type": "String"}},
1248 "Conditions": {
1249 "UseAlt": {"Fn::Equals": [{"Ref": "WantAlt"}, "yes"]}
1250 },
1251 "Mappings": {
1252 "RegionMap": {
1253 "us-east-1": {"AMI": "ami-east"}
1254 }
1255 },
1256 "Resources": {
1257 "Inst": {
1258 "Type": "AWS::EC2::Instance",
1259 "Properties": {
1260 "ImageId": {"Fn::If": [
1261 "UseAlt",
1262 {"Fn::FindInMap": ["RegionMap", "ap-south-1", "AMI"]},
1263 {"Fn::FindInMap": ["RegionMap", "us-east-1", "AMI"]}
1264 ]}
1265 }
1266 }
1267 }
1268 }"#;
1269 let mut params = BTreeMap::new();
1270 params.insert("WantAlt".to_string(), "yes".to_string());
1271 let err = parse_template(template, ¶ms).unwrap_err();
1272 assert!(
1273 err.contains("Unable to get mapping for RegionMap::ap-south-1::AMI"),
1274 "got: {err}"
1275 );
1276 }
1277
1278 #[test]
1279 fn fn_find_in_map_alongside_ref_and_sub_still_resolve() {
1280 let template = r#"{
1281 "Parameters": {"Env": {"Type": "String"}},
1282 "Mappings": {
1283 "EnvMap": {
1284 "prod": {"Suffix": "live"},
1285 "dev": {"Suffix": "test"}
1286 }
1287 },
1288 "Resources": {
1289 "Q": {
1290 "Type": "AWS::SQS::Queue",
1291 "Properties": {
1292 "QueueName": {"Fn::FindInMap": ["EnvMap", {"Ref": "Env"}, "Suffix"]},
1293 "Tags": [
1294 {"Key": "EnvRef", "Value": {"Ref": "Env"}},
1295 {"Key": "Subbed", "Value": {"Fn::Sub": "env-${Env}"}}
1296 ]
1297 }
1298 }
1299 }
1300 }"#;
1301 let mut params = BTreeMap::new();
1302 params.insert("Env".to_string(), "prod".to_string());
1303 let parsed = parse_template(template, ¶ms).unwrap();
1304 let p = &parsed.resources[0].properties;
1305 assert_eq!(p["QueueName"], Value::String("live".to_string()));
1306 assert_eq!(p["Tags"][0]["Value"], Value::String("prod".to_string()));
1307 assert_eq!(p["Tags"][1]["Value"], Value::String("env-prod".to_string()));
1308 }
1309
1310 #[test]
1313 fn cyclic_conditions_self_reference_errors() {
1314 let template = r#"{
1315 "Conditions": {
1316 "A": {"Condition": "A"}
1317 },
1318 "Resources": {
1319 "Q": {
1320 "Type": "AWS::SQS::Queue",
1321 "Condition": "A",
1322 "Properties": {"QueueName": "q"}
1323 }
1324 }
1325 }"#;
1326 let err = parse_template(template, &BTreeMap::new()).unwrap_err();
1327 assert!(err.contains("Circular reference"), "got: {err}");
1328 assert!(err.contains("'A'"), "got: {err}");
1329 }
1330
1331 #[test]
1332 fn cyclic_conditions_two_step_errors() {
1333 let template = r#"{
1334 "Conditions": {
1335 "A": {"Condition": "B"},
1336 "B": {"Condition": "A"}
1337 },
1338 "Resources": {
1339 "Q": {
1340 "Type": "AWS::SQS::Queue",
1341 "Condition": "A",
1342 "Properties": {"QueueName": "q"}
1343 }
1344 }
1345 }"#;
1346 let err = parse_template(template, &BTreeMap::new()).unwrap_err();
1347 assert!(err.contains("Circular reference"), "got: {err}");
1348 }
1349
1350 #[test]
1351 fn condition_referencing_undefined_name_errors() {
1352 let template = r#"{
1353 "Conditions": {
1354 "A": {"Condition": "DoesNotExist"}
1355 },
1356 "Resources": {
1357 "Q": {
1358 "Type": "AWS::SQS::Queue",
1359 "Condition": "A",
1360 "Properties": {"QueueName": "q"}
1361 }
1362 }
1363 }"#;
1364 let err = parse_template(template, &BTreeMap::new()).unwrap_err();
1365 assert!(err.contains("DoesNotExist"), "got: {err}");
1366 }
1367
1368 #[test]
1369 fn fn_if_no_value_removes_property_from_parent_map() {
1370 let template = r#"{
1371 "Parameters": {"WantTags": {"Type": "String"}},
1372 "Conditions": {
1373 "HasTags": {"Fn::Equals": [{"Ref": "WantTags"}, "yes"]}
1374 },
1375 "Resources": {
1376 "Q": {
1377 "Type": "AWS::SQS::Queue",
1378 "Properties": {
1379 "QueueName": "q",
1380 "Tags": {"Fn::If": [
1381 "HasTags",
1382 [{"Key": "a", "Value": "b"}],
1383 {"Ref": "AWS::NoValue"}
1384 ]}
1385 }
1386 }
1387 }
1388 }"#;
1389 let mut params = BTreeMap::new();
1390 params.insert("WantTags".to_string(), "no".to_string());
1391 let parsed = parse_template(template, ¶ms).unwrap();
1392 let props = parsed.resources[0].properties.as_object().unwrap();
1393 assert!(
1394 !props.contains_key("Tags"),
1395 "Tags should be omitted when AWS::NoValue picked, got: {props:?}"
1396 );
1397 assert_eq!(
1398 props.get("QueueName"),
1399 Some(&Value::String("q".to_string()))
1400 );
1401 }
1402
1403 #[test]
1404 fn fn_if_no_value_keeps_property_when_branch_concrete() {
1405 let template = r#"{
1406 "Parameters": {"WantTags": {"Type": "String"}},
1407 "Conditions": {
1408 "HasTags": {"Fn::Equals": [{"Ref": "WantTags"}, "yes"]}
1409 },
1410 "Resources": {
1411 "Q": {
1412 "Type": "AWS::SQS::Queue",
1413 "Properties": {
1414 "QueueName": "q",
1415 "Tags": {"Fn::If": [
1416 "HasTags",
1417 [{"Key": "a", "Value": "b"}],
1418 {"Ref": "AWS::NoValue"}
1419 ]}
1420 }
1421 }
1422 }
1423 }"#;
1424 let mut params = BTreeMap::new();
1425 params.insert("WantTags".to_string(), "yes".to_string());
1426 let parsed = parse_template(template, ¶ms).unwrap();
1427 let tags = &parsed.resources[0].properties["Tags"];
1428 assert_eq!(
1429 tags,
1430 &serde_json::json!([{"Key": "a", "Value": "b"}]),
1431 "tags should be the true branch's array"
1432 );
1433 }
1434
1435 #[test]
1436 fn fn_if_no_value_in_array_drops_element() {
1437 let template = r#"{
1438 "Parameters": {"Extra": {"Type": "String"}},
1439 "Conditions": {
1440 "HasExtra": {"Fn::Equals": [{"Ref": "Extra"}, "yes"]}
1441 },
1442 "Resources": {
1443 "Q": {
1444 "Type": "AWS::SQS::Queue",
1445 "Properties": {
1446 "Items": [
1447 "first",
1448 {"Fn::If": ["HasExtra", "second", {"Ref": "AWS::NoValue"}]},
1449 "third"
1450 ]
1451 }
1452 }
1453 }
1454 }"#;
1455 let mut params = BTreeMap::new();
1456 params.insert("Extra".to_string(), "no".to_string());
1457 let parsed = parse_template(template, ¶ms).unwrap();
1458 assert_eq!(
1459 parsed.resources[0].properties["Items"],
1460 serde_json::json!(["first", "third"])
1461 );
1462 }
1463
1464 #[test]
1465 fn condition_skips_output_when_false() {
1466 let template = r#"{
1467 "Parameters": {"Env": {"Type": "String"}},
1468 "Conditions": {
1469 "IsProd": {"Fn::Equals": [{"Ref": "Env"}, "prod"]}
1470 },
1471 "Resources": {
1472 "Q": {
1473 "Type": "AWS::SQS::Queue",
1474 "Properties": {"QueueName": "q"}
1475 }
1476 },
1477 "Outputs": {
1478 "ProdName": {
1479 "Condition": "IsProd",
1480 "Value": "prod-only"
1481 },
1482 "Always": {
1483 "Value": "shown"
1484 }
1485 }
1486 }"#;
1487 let mut params = BTreeMap::new();
1488 params.insert("Env".to_string(), "dev".to_string());
1489 let parsed = parse_template(template, ¶ms).unwrap();
1490 let names: Vec<&str> = parsed
1491 .outputs
1492 .iter()
1493 .map(|o| o.logical_id.as_str())
1494 .collect();
1495 assert!(names.contains(&"Always"));
1496 assert!(!names.contains(&"ProdName"));
1497 }
1498
1499 #[test]
1500 fn fn_and_short_circuits_on_false() {
1501 let template = r#"{
1502 "Parameters": {"Env": {"Type": "String"}},
1503 "Conditions": {
1504 "IsProd": {"Fn::Equals": [{"Ref": "Env"}, "prod"]},
1505 "Combined": {"Fn::And": [
1506 {"Condition": "IsProd"},
1507 {"Fn::Equals": [{"Ref": "Env"}, "prod"]}
1508 ]}
1509 },
1510 "Resources": {
1511 "Q": {
1512 "Type": "AWS::SQS::Queue",
1513 "Condition": "Combined",
1514 "Properties": {"QueueName": "q"}
1515 }
1516 }
1517 }"#;
1518 let mut params = BTreeMap::new();
1519 params.insert("Env".to_string(), "dev".to_string());
1520 let parsed = parse_template(template, ¶ms).unwrap();
1521 assert_eq!(parsed.resources.len(), 0);
1522 }
1523
1524 #[test]
1525 fn fn_or_short_circuits_on_true() {
1526 let template = r#"{
1527 "Parameters": {"Env": {"Type": "String"}},
1528 "Conditions": {
1529 "IsProd": {"Fn::Equals": [{"Ref": "Env"}, "prod"]},
1530 "AnyEnv": {"Fn::Or": [
1531 {"Condition": "IsProd"},
1532 {"Fn::Equals": [{"Ref": "Env"}, "dev"]},
1533 {"Fn::Equals": [{"Ref": "Env"}, "stage"]}
1534 ]}
1535 },
1536 "Resources": {
1537 "Q": {
1538 "Type": "AWS::SQS::Queue",
1539 "Condition": "AnyEnv",
1540 "Properties": {"QueueName": "q"}
1541 }
1542 }
1543 }"#;
1544 let mut params = BTreeMap::new();
1545 params.insert("Env".to_string(), "stage".to_string());
1546 let parsed = parse_template(template, ¶ms).unwrap();
1547 assert_eq!(parsed.resources.len(), 1);
1548 }
1549
1550 #[test]
1551 fn fn_and_rejects_arity_outside_1_to_10() {
1552 let template = r#"{
1553 "Conditions": {
1554 "Empty": {"Fn::And": []}
1555 },
1556 "Resources": {
1557 "Q": {
1558 "Type": "AWS::SQS::Queue",
1559 "Condition": "Empty",
1560 "Properties": {"QueueName": "q"}
1561 }
1562 }
1563 }"#;
1564 let err = parse_template(template, &BTreeMap::new()).unwrap_err();
1565 assert!(err.contains("Fn::And"), "got: {err}");
1566 }
1567
1568 #[test]
1569 fn condition_evaluation_memoizes_complex_expression() {
1570 let template = r#"{
1575 "Parameters": {"Env": {"Type": "String"}},
1576 "Conditions": {
1577 "Inner": {"Fn::Equals": [{"Ref": "Env"}, "prod"]},
1578 "OuterA": {"Fn::And": [{"Condition": "Inner"}, {"Condition": "Inner"}]},
1579 "OuterB": {"Fn::Or": [{"Condition": "Inner"}, {"Condition": "OuterA"}]}
1580 },
1581 "Resources": {
1582 "Q": {
1583 "Type": "AWS::SQS::Queue",
1584 "Condition": "OuterB",
1585 "Properties": {"QueueName": "q"}
1586 }
1587 }
1588 }"#;
1589 let mut params = BTreeMap::new();
1590 params.insert("Env".to_string(), "prod".to_string());
1591 let parsed = parse_template(template, ¶ms).unwrap();
1592 assert_eq!(parsed.resources.len(), 1);
1593 }
1594
1595 #[test]
1596 fn fn_not_rejects_multiple_arguments() {
1597 let template = r#"{
1598 "Parameters": {"Env": {"Type": "String"}},
1599 "Conditions": {
1600 "IsProd": {"Fn::Equals": [{"Ref": "Env"}, "prod"]},
1601 "Bad": {"Fn::Not": [
1602 {"Condition": "IsProd"},
1603 {"Condition": "IsProd"}
1604 ]}
1605 },
1606 "Resources": {
1607 "Q": {
1608 "Type": "AWS::SQS::Queue",
1609 "Condition": "Bad",
1610 "Properties": {"QueueName": "q"}
1611 }
1612 }
1613 }"#;
1614 let mut params = BTreeMap::new();
1615 params.insert("Env".to_string(), "prod".to_string());
1616 let err = parse_template(template, ¶ms).unwrap_err();
1617 assert!(err.contains("Fn::Not"), "got: {err}");
1618 }
1619
1620 #[test]
1621 fn fn_not_rejects_zero_arguments() {
1622 let template = r#"{
1623 "Conditions": {
1624 "Bad": {"Fn::Not": []}
1625 },
1626 "Resources": {
1627 "Q": {
1628 "Type": "AWS::SQS::Queue",
1629 "Condition": "Bad",
1630 "Properties": {"QueueName": "q"}
1631 }
1632 }
1633 }"#;
1634 let err = parse_template(template, &BTreeMap::new()).unwrap_err();
1635 assert!(err.contains("Fn::Not"), "got: {err}");
1636 }
1637
1638 #[test]
1639 fn resolve_resource_properties_strips_no_value_at_provision_time() {
1640 let template = r#"{
1645 "Parameters": {"WantTags": {"Type": "String"}},
1646 "Conditions": {
1647 "HasTags": {"Fn::Equals": [{"Ref": "WantTags"}, "yes"]}
1648 },
1649 "Resources": {
1650 "Q": {
1651 "Type": "AWS::SQS::Queue",
1652 "Properties": {
1653 "QueueName": "q",
1654 "Tags": {"Fn::If": [
1655 "HasTags",
1656 [{"Key": "a", "Value": "b"}],
1657 {"Ref": "AWS::NoValue"}
1658 ]}
1659 }
1660 }
1661 }
1662 }"#;
1663 let mut params = BTreeMap::new();
1664 params.insert("WantTags".to_string(), "no".to_string());
1665 let parsed = parse_template(template, ¶ms).unwrap();
1666 let resource = parsed
1667 .resources
1668 .iter()
1669 .find(|r| r.logical_id == "Q")
1670 .unwrap();
1671 assert!(!resource
1673 .properties
1674 .as_object()
1675 .unwrap()
1676 .contains_key("Tags"));
1677
1678 let reresolved = resolve_resource_properties_with_attrs(
1682 resource,
1683 template,
1684 ¶ms,
1685 &BTreeMap::new(),
1686 &BTreeMap::new(),
1687 &BTreeMap::new(),
1688 )
1689 .unwrap();
1690 let props = reresolved.properties.as_object().unwrap();
1691 assert!(
1692 !props.contains_key("Tags"),
1693 "Tags should be stripped on re-resolve, got: {props:?}"
1694 );
1695 let serialized = serde_json::to_string(&reresolved.properties).unwrap();
1697 assert!(
1698 !serialized.contains(NO_VALUE_SENTINEL_KEY),
1699 "sentinel leaked: {serialized}"
1700 );
1701 }
1702
1703 #[test]
1706 fn fn_select_string_index_resolves() {
1707 let (p, r, ids, attrs) = empty();
1710 let v: Value = serde_json::from_str(r#"{"Fn::Select": ["2", ["a", "b", "c", "d"]]}"#)
1711 .expect("static fixture parses");
1712 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
1713 assert_eq!(resolved, Value::String("c".to_string()));
1714 }
1715
1716 #[test]
1717 fn fn_select_out_of_range_returns_null() {
1718 let (p, r, ids, attrs) = empty();
1719 let v: Value = serde_json::from_str(r#"{"Fn::Select": [10, ["a", "b"]]}"#)
1720 .expect("static fixture parses");
1721 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
1722 assert_eq!(resolved, Value::Null);
1723 }
1724
1725 #[test]
1726 fn fn_select_resolves_ref_inside_list() {
1727 let template = r#"{
1728 "Parameters": {"AZs": {"Type": "CommaDelimitedList"}},
1729 "Resources": {
1730 "Q": {
1731 "Type": "AWS::SQS::Queue",
1732 "Properties": {
1733 "QueueName": {"Fn::Select": [0, {"Fn::Split": [",", {"Ref": "AZs"}]}]}
1734 }
1735 }
1736 }
1737 }"#;
1738 let mut params = BTreeMap::new();
1739 params.insert(
1740 "AZs".to_string(),
1741 "us-east-1a,us-east-1b,us-east-1c".to_string(),
1742 );
1743 let parsed = parse_template(template, ¶ms).unwrap();
1744 assert_eq!(
1745 parsed.resources[0].properties["QueueName"],
1746 Value::String("us-east-1a".to_string())
1747 );
1748 }
1749
1750 #[test]
1751 fn fn_split_empty_delimiter_returns_full_string_split_per_char() {
1752 let (p, r, ids, attrs) = empty();
1753 let v: Value =
1754 serde_json::from_str(r#"{"Fn::Split": ["", "abc"]}"#).expect("static fixture parses");
1755 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
1756 assert!(resolved.is_array());
1760 }
1761
1762 #[test]
1763 fn fn_split_no_match_returns_single_element_array() {
1764 let (p, r, ids, attrs) = empty();
1765 let v: Value = serde_json::from_str(r#"{"Fn::Split": [",", "no-commas-here"]}"#)
1766 .expect("static fixture parses");
1767 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
1768 assert_eq!(resolved, serde_json::json!(["no-commas-here"]));
1769 }
1770
1771 #[test]
1772 fn fn_base64_encodes_unicode() {
1773 let (p, r, ids, attrs) = empty();
1774 let v: Value =
1775 serde_json::from_str(r#"{"Fn::Base64": "héllo"}"#).expect("static fixture parses");
1776 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
1777 assert_eq!(resolved, Value::String("aMOpbGxv".to_string()));
1779 }
1780
1781 #[test]
1782 fn fn_base64_resolves_nested_intrinsic() {
1783 let template = r#"{
1784 "Parameters": {"Greeting": {"Type": "String"}},
1785 "Resources": {
1786 "Q": {
1787 "Type": "AWS::SQS::Queue",
1788 "Properties": {
1789 "QueueName": {"Fn::Base64": {"Ref": "Greeting"}}
1790 }
1791 }
1792 }
1793 }"#;
1794 let mut params = BTreeMap::new();
1795 params.insert("Greeting".to_string(), "hello".to_string());
1796 let parsed = parse_template(template, ¶ms).unwrap();
1797 assert_eq!(
1798 parsed.resources[0].properties["QueueName"],
1799 Value::String("aGVsbG8=".to_string())
1800 );
1801 }
1802
1803 #[test]
1804 fn fn_length_counts_string_chars() {
1805 let (p, r, ids, attrs) = empty();
1806 let v: Value =
1807 serde_json::from_str(r#"{"Fn::Length": "héllo"}"#).expect("static fixture parses");
1808 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
1809 assert_eq!(resolved, Value::Number(5.into()));
1811 }
1812
1813 #[test]
1814 fn fn_length_resolves_nested_split() {
1815 let (p, r, ids, attrs) = empty();
1816 let v: Value = serde_json::from_str(r#"{"Fn::Length": {"Fn::Split": [",", "a,b,c,d,e"]}}"#)
1817 .expect("static fixture parses");
1818 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
1819 assert_eq!(resolved, Value::Number(5.into()));
1820 }
1821
1822 #[test]
1823 fn fn_to_json_string_serializes_array() {
1824 let (p, r, ids, attrs) = empty();
1825 let v: Value = serde_json::from_str(r#"{"Fn::ToJsonString": ["a", "b", "c"]}"#)
1826 .expect("static fixture parses");
1827 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
1828 assert_eq!(resolved, Value::String(r#"["a","b","c"]"#.to_string()));
1829 }
1830
1831 #[test]
1832 fn fn_to_json_string_resolves_inner_ref() {
1833 let template = r#"{
1834 "Parameters": {"Name": {"Type": "String"}},
1835 "Resources": {
1836 "Q": {
1837 "Type": "AWS::SQS::Queue",
1838 "Properties": {
1839 "QueueName": {
1840 "Fn::ToJsonString": {"k": {"Ref": "Name"}}
1841 }
1842 }
1843 }
1844 }
1845 }"#;
1846 let mut params = BTreeMap::new();
1847 params.insert("Name".to_string(), "abc".to_string());
1848 let parsed = parse_template(template, ¶ms).unwrap();
1849 assert_eq!(
1850 parsed.resources[0].properties["QueueName"],
1851 Value::String(r#"{"k":"abc"}"#.to_string())
1852 );
1853 }
1854
1855 #[test]
1856 fn fn_cidr_count_matches_request() {
1857 let (p, r, ids, attrs) = empty();
1860 let v: Value = serde_json::from_str(r#"{"Fn::Cidr": ["10.0.0.0/16", 2, 8]}"#)
1861 .expect("static fixture parses");
1862 let resolved = resolve_refs(&v, &p, &r, &ids, &attrs);
1863 assert_eq!(resolved, serde_json::json!(["10.0.0.0/24", "10.0.1.0/24"]));
1864 }
1865
1866 #[test]
1867 fn fn_cidr_resolves_via_ref() {
1868 let template = r#"{
1869 "Parameters": {"Vpc": {"Type": "String"}},
1870 "Resources": {
1871 "Q": {
1872 "Type": "AWS::SQS::Queue",
1873 "Properties": {
1874 "QueueName": {"Fn::Select": [
1875 0,
1876 {"Fn::Cidr": [{"Ref": "Vpc"}, 4, 8]}
1877 ]}
1878 }
1879 }
1880 }
1881 }"#;
1882 let mut params = BTreeMap::new();
1883 params.insert("Vpc".to_string(), "172.16.0.0/16".to_string());
1884 let parsed = parse_template(template, ¶ms).unwrap();
1885 assert_eq!(
1886 parsed.resources[0].properties["QueueName"],
1887 Value::String("172.16.0.0/24".to_string())
1888 );
1889 }
1890
1891 #[test]
1892 fn fn_for_each_expands_resources() {
1893 let template = r#"{
1894 "Resources": {
1895 "Fn::ForEach::TopicLoop": [
1896 "TopicName",
1897 ["alpha", "beta", "gamma"],
1898 {
1899 "${TopicName}Topic": {
1900 "Type": "AWS::SNS::Topic",
1901 "Properties": {"TopicName": "${TopicName}-topic"}
1902 }
1903 }
1904 ]
1905 }
1906 }"#;
1907 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
1908 let names: Vec<&str> = parsed
1909 .resources
1910 .iter()
1911 .map(|r| r.logical_id.as_str())
1912 .collect();
1913 assert!(names.contains(&"alphaTopic"), "got: {names:?}");
1914 assert!(names.contains(&"betaTopic"), "got: {names:?}");
1915 assert!(names.contains(&"gammaTopic"), "got: {names:?}");
1916 let alpha = parsed
1917 .resources
1918 .iter()
1919 .find(|r| r.logical_id == "alphaTopic")
1920 .unwrap();
1921 assert_eq!(
1922 alpha.properties["TopicName"],
1923 Value::String("alpha-topic".to_string())
1924 );
1925 }
1926
1927 #[test]
1928 fn fn_for_each_substitutes_in_nested_values() {
1929 let template = r#"{
1930 "Resources": {
1931 "Fn::ForEach::Q": [
1932 "QName",
1933 ["one", "two"],
1934 {
1935 "${QName}Queue": {
1936 "Type": "AWS::SQS::Queue",
1937 "Properties": {
1938 "QueueName": "${QName}",
1939 "Tags": [
1940 {"Key": "name", "Value": "${QName}"}
1941 ]
1942 }
1943 }
1944 }
1945 ]
1946 }
1947 }"#;
1948 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
1949 let one = parsed
1950 .resources
1951 .iter()
1952 .find(|r| r.logical_id == "oneQueue")
1953 .unwrap();
1954 assert_eq!(
1955 one.properties["QueueName"],
1956 Value::String("one".to_string())
1957 );
1958 assert_eq!(
1959 one.properties["Tags"][0]["Value"],
1960 Value::String("one".to_string())
1961 );
1962 }
1963
1964 #[test]
1965 fn fn_for_each_nested_loops_expand_cartesian() {
1966 let template = r#"{
1967 "Resources": {
1968 "Fn::ForEach::Outer": [
1969 "Env",
1970 ["dev", "prod"],
1971 {
1972 "Fn::ForEach::Inner": [
1973 "Region",
1974 ["us-east-1", "eu-west-1"],
1975 {
1976 "${Env}${Region}Q": {
1977 "Type": "AWS::SQS::Queue",
1978 "Properties": {"QueueName": "${Env}-${Region}"}
1979 }
1980 }
1981 ]
1982 }
1983 ]
1984 }
1985 }"#;
1986 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
1987 let names: Vec<&str> = parsed
1988 .resources
1989 .iter()
1990 .map(|r| r.logical_id.as_str())
1991 .collect();
1992 for env in ["dev", "prod"] {
1993 for region in ["us-east-1", "eu-west-1"] {
1994 let expected = format!("{env}{region}Q");
1995 assert!(
1996 names.contains(&expected.as_str()),
1997 "missing {expected} in {names:?}"
1998 );
1999 }
2000 }
2001 let dev_us = parsed
2002 .resources
2003 .iter()
2004 .find(|r| r.logical_id == "devus-east-1Q")
2005 .unwrap();
2006 assert_eq!(
2007 dev_us.properties["QueueName"],
2008 Value::String("dev-us-east-1".to_string())
2009 );
2010 }
2011
2012 #[test]
2013 fn fn_for_each_keeps_other_resources_untouched() {
2014 let template = r#"{
2015 "Resources": {
2016 "Static": {
2017 "Type": "AWS::SQS::Queue",
2018 "Properties": {"QueueName": "static-q"}
2019 },
2020 "Fn::ForEach::Loop": [
2021 "I",
2022 ["a", "b"],
2023 {
2024 "${I}Topic": {
2025 "Type": "AWS::SNS::Topic",
2026 "Properties": {"TopicName": "${I}"}
2027 }
2028 }
2029 ]
2030 }
2031 }"#;
2032 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
2033 let names: Vec<&str> = parsed
2034 .resources
2035 .iter()
2036 .map(|r| r.logical_id.as_str())
2037 .collect();
2038 assert!(names.contains(&"Static"));
2039 assert!(names.contains(&"aTopic"));
2040 assert!(names.contains(&"bTopic"));
2041 assert_eq!(parsed.resources.len(), 3);
2042 }
2043
2044 #[test]
2045 fn fn_for_each_invalid_arity_errors() {
2046 let template = r#"{
2047 "Resources": {
2048 "Fn::ForEach::Bad": [
2049 "Var",
2050 ["a"]
2051 ]
2052 }
2053 }"#;
2054 let err = parse_template(template, &BTreeMap::new()).unwrap_err();
2055 assert!(err.contains("Fn::ForEach"), "got: {err}");
2056 }
2057
2058 #[test]
2059 fn fn_for_each_resolves_intrinsics_in_emitted_resources() {
2060 let template = r#"{
2064 "Parameters": {"Env": {"Type": "String"}},
2065 "Resources": {
2066 "Fn::ForEach::Q": [
2067 "Name",
2068 ["alpha", "beta"],
2069 {
2070 "${Name}Queue": {
2071 "Type": "AWS::SQS::Queue",
2072 "Properties": {
2073 "QueueName": {"Fn::Sub": "${Env}-${Name}"}
2074 }
2075 }
2076 }
2077 ]
2078 }
2079 }"#;
2080 let mut params = BTreeMap::new();
2081 params.insert("Env".to_string(), "prod".to_string());
2082 let parsed = parse_template(template, ¶ms).unwrap();
2083 let alpha = parsed
2086 .resources
2087 .iter()
2088 .find(|r| r.logical_id == "alphaQueue")
2089 .unwrap();
2090 assert_eq!(
2091 alpha.properties["QueueName"],
2092 Value::String("prod-alpha".to_string())
2093 );
2094 }
2095
2096 #[test]
2097 fn fn_for_each_re_resolves_at_provision_time() {
2098 let template = r#"{
2102 "Resources": {
2103 "Fn::ForEach::Q": [
2104 "Name",
2105 ["alpha"],
2106 {
2107 "${Name}Queue": {
2108 "Type": "AWS::SQS::Queue",
2109 "Properties": {"QueueName": "${Name}-q"}
2110 }
2111 }
2112 ]
2113 }
2114 }"#;
2115 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
2116 let resource = parsed
2117 .resources
2118 .iter()
2119 .find(|r| r.logical_id == "alphaQueue")
2120 .unwrap();
2121 let reresolved = resolve_resource_properties_with_attrs(
2122 resource,
2123 template,
2124 &BTreeMap::new(),
2125 &BTreeMap::new(),
2126 &BTreeMap::new(),
2127 &BTreeMap::new(),
2128 )
2129 .unwrap();
2130 assert_eq!(
2131 reresolved.properties["QueueName"],
2132 Value::String("alpha-q".to_string())
2133 );
2134 }
2135
2136 #[test]
2137 fn fn_for_each_resolves_ref_to_comma_delimited_list_param() {
2138 let template = r#"{
2142 "Parameters": {"Names": {"Type": "CommaDelimitedList"}},
2143 "Resources": {
2144 "Fn::ForEach::Q": [
2145 "N",
2146 {"Ref": "Names"},
2147 {
2148 "${N}Queue": {
2149 "Type": "AWS::SQS::Queue",
2150 "Properties": {"QueueName": "${N}-q"}
2151 }
2152 }
2153 ]
2154 }
2155 }"#;
2156 let mut params = BTreeMap::new();
2157 params.insert("Names".to_string(), "alpha,beta,gamma".to_string());
2158 let parsed = parse_template(template, ¶ms).unwrap();
2159 let names: Vec<&str> = parsed
2160 .resources
2161 .iter()
2162 .map(|r| r.logical_id.as_str())
2163 .collect();
2164 for v in ["alphaQueue", "betaQueue", "gammaQueue"] {
2165 assert!(names.contains(&v), "missing {v} in {names:?}");
2166 }
2167 }
2168
2169 #[test]
2170 fn fn_for_each_ampersand_substitution_form() {
2171 let template = r#"{
2175 "Resources": {
2176 "Fn::ForEach::Q": [
2177 "Name",
2178 ["alpha", "beta"],
2179 {
2180 "&{Name}Queue": {
2181 "Type": "AWS::SQS::Queue",
2182 "Properties": {"QueueName": "&{Name}"}
2183 }
2184 }
2185 ]
2186 }
2187 }"#;
2188 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
2189 let names: Vec<&str> = parsed
2190 .resources
2191 .iter()
2192 .map(|r| r.logical_id.as_str())
2193 .collect();
2194 assert!(names.contains(&"alphaQueue"), "got: {names:?}");
2195 assert!(names.contains(&"betaQueue"), "got: {names:?}");
2196 let alpha = parsed
2197 .resources
2198 .iter()
2199 .find(|r| r.logical_id == "alphaQueue")
2200 .unwrap();
2201 assert_eq!(
2202 alpha.properties["QueueName"],
2203 Value::String("alpha".to_string())
2204 );
2205 }
2206
2207 #[test]
2208 fn fn_for_each_in_outputs_expands() {
2209 let template = r#"{
2210 "Resources": {
2211 "Q": {"Type": "AWS::SQS::Queue", "Properties": {"QueueName": "q"}}
2212 },
2213 "Outputs": {
2214 "Fn::ForEach::OutputLoop": [
2215 "I",
2216 ["one", "two"],
2217 {
2218 "${I}Out": {"Value": "${I}-value"}
2219 }
2220 ]
2221 }
2222 }"#;
2223 let parsed = parse_template(template, &BTreeMap::new()).unwrap();
2224 let names: Vec<&str> = parsed
2225 .outputs
2226 .iter()
2227 .map(|o| o.logical_id.as_str())
2228 .collect();
2229 assert!(names.contains(&"oneOut"), "got: {names:?}");
2230 assert!(names.contains(&"twoOut"), "got: {names:?}");
2231 let one = parsed
2232 .outputs
2233 .iter()
2234 .find(|o| o.logical_id == "oneOut")
2235 .unwrap();
2236 assert_eq!(one.value, "one-value");
2237 }
2238}
2239
2240mod conditions;
2241mod for_each;
2242mod intrinsics;
2243mod mappings;
2244mod parser;
2245mod resolution;
2246mod sam_events;
2247use conditions::*;
2248use for_each::*;
2249use intrinsics::*;
2250use mappings::*;
2251
2252pub use parser::{
2253 collect_import_value_names, parse_outputs, parse_template, parse_template_with_physical_ids,
2254 parse_template_with_resolution,
2255};
2256pub use resolution::{
2257 dependency_order, resolve_resource_properties, resolve_resource_properties_with_attrs,
2258};