stratosphere_core/value.rs
1use crate::template::LogicalResourceName;
2use serde;
3use serde_json;
4use serde_json::json;
5
6#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
7pub struct AttributeName(String);
8
9impl From<&str> for AttributeName {
10 fn from(value: &str) -> Self {
11 Self(value.into())
12 }
13}
14
15#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
16pub struct ConditionName(String);
17
18impl From<&str> for ConditionName {
19 fn from(value: &str) -> Self {
20 Self(value.to_string())
21 }
22}
23
24pub fn equals_bool<A: Into<ExpBool>, B: Into<ExpBool>>(left: A, right: B) -> ExpBool {
25 ExpBool::Equals(ExpPair::Bool {
26 left: Box::new(left.into()),
27 right: Box::new(right.into()),
28 })
29}
30
31pub fn equals_string<A: Into<ExpString>, B: Into<ExpString>>(left: A, right: B) -> ExpBool {
32 ExpBool::Equals(ExpPair::String {
33 left: Box::new(left.into()),
34 right: Box::new(right.into()),
35 })
36}
37
38/// Trait for expression types that support conditional (`Fn::If`) expressions
39///
40/// This trait enables generic helper functions for constructing conditional
41/// expressions across different expression types (ExpString, ExpBool, etc.)
42pub trait FnIf: Sized {
43 /// Creates a conditional expression using `Fn::If`
44 ///
45 /// # Arguments
46 ///
47 /// * `condition_name` - Name of the condition to evaluate
48 /// * `true_branch` - Value returned when condition is true
49 /// * `else_branch` - Value returned when condition is false
50 fn fn_if(
51 condition_name: impl Into<ConditionName>,
52 true_branch: impl Into<Self>,
53 else_branch: impl Into<Self>,
54 ) -> Self;
55}
56
57/// Generic helper function to create conditional (`Fn::If`) expressions
58///
59/// This function works with any type implementing [`FnIf`],
60/// allowing for type-safe conditional expressions across different expression types.
61///
62/// # Arguments
63///
64/// * `condition_name` - Name of the condition to evaluate
65/// * `true_branch` - Value returned when condition is true
66/// * `else_branch` - Value returned when condition is false
67///
68/// # Examples
69///
70/// ```
71/// # use stratosphere_core::value::*;
72/// let expr: ExpString = fn_if("MyCondition", "value-if-true", "value-if-false");
73/// ```
74pub fn fn_if<T: FnIf>(
75 condition_name: impl Into<ConditionName>,
76 true_branch: impl Into<T>,
77 else_branch: impl Into<T>,
78) -> T {
79 T::fn_if(condition_name, true_branch, else_branch)
80}
81
82/// Type-specific helper for boolean conditional expressions that doesn't require turbofish syntax.
83/// Use this when working with ExpBool values.
84pub fn fn_if_bool(
85 condition_name: impl Into<ConditionName>,
86 true_branch: impl Into<ExpBool>,
87 false_branch: impl Into<ExpBool>,
88) -> ExpBool {
89 ExpBool::fn_if(condition_name, true_branch, false_branch)
90}
91
92/// Trait for expression types that support select (`Fn::Select`) expressions
93///
94/// This trait enables generic helper functions for selecting an item from a list
95/// across different expression types (ExpString, ExpBool, etc.)
96pub trait FnSelect: Sized {
97 /// The type representing a list of values that can be selected from
98 type ValueList;
99
100 /// Creates a select expression using `Fn::Select`
101 ///
102 /// # Arguments
103 ///
104 /// * `index` - Zero-based index of the item to select
105 /// * `values` - List of values to select from
106 fn fn_select(index: u8, values: Self::ValueList) -> Self;
107}
108
109/// Generic helper function to create select (`Fn::Select`) expressions
110///
111/// This function works with any type implementing [`FnSelect`],
112/// allowing for type-safe selection from lists across different expression types.
113///
114/// # Arguments
115///
116/// * `index` - Zero-based index of the item to select
117/// * `values` - List of values to select from
118///
119/// # Examples
120///
121/// ```
122/// # use stratosphere_core::value::*;
123/// let expr: ExpString = fn_select(0, vec!["first".into(), "second".into()].into());
124/// ```
125pub fn fn_select<T: FnSelect>(index: u8, values: T::ValueList) -> T {
126 T::fn_select(index, values)
127}
128
129/// Type-specific helper for boolean select expressions that doesn't require turbofish syntax.
130/// Use this when working with ExpBool values.
131pub fn fn_select_bool(index: u8, values: Vec<ExpBool>) -> ExpBool {
132 ExpBool::fn_select(index, values)
133}
134
135/// Type-specific helper for string select expressions that doesn't require turbofish syntax.
136/// Use this when working with ExpString values.
137pub fn fn_select_string(index: u8, values: impl Into<ExpStringList>) -> ExpString {
138 ExpString::fn_select(index, values.into())
139}
140
141/// Helper function to create a split expression
142///
143/// Returns an ExpString::Split expression representing a Fn::Split operation.
144/// Typically used with fn_select_string to extract specific elements.
145///
146/// # Arguments
147///
148/// * `delimiter` - The delimiter to split on
149/// * `source` - The source string to split
150///
151/// # Examples
152///
153/// ```
154/// # use stratosphere_core::value::*;
155/// // Select first element from split result
156/// let first = fn_select_string(0, vec![fn_split(",", "a,b,c")]);
157/// ```
158pub fn fn_split(delimiter: impl Into<String>, source: impl Into<ExpString>) -> ExpString {
159 ExpString::Split {
160 delimiter: delimiter.into(),
161 source: Box::new(source.into()),
162 }
163}
164
165/// Trait for expression types that support find in map (`Fn::FindInMap`) expressions
166///
167/// This trait enables generic helper functions for retrieving values from mappings
168/// across different expression types (ExpString, ExpBool, etc.)
169pub trait FnFindInMap: Sized {
170 /// Creates a find in map expression using `Fn::FindInMap`
171 ///
172 /// # Arguments
173 ///
174 /// * `map_name` - MapName referencing the mapping to look up
175 /// * `top_level_key` - First-level key in the mapping
176 /// * `second_level_key` - Second-level key in the mapping
177 fn fn_find_in_map(
178 map_name: impl Into<crate::template::MapName>,
179 top_level_key: impl Into<ExpString>,
180 second_level_key: impl Into<String>,
181 ) -> Self;
182}
183
184/// Generic helper function to create find in map (`Fn::FindInMap`) expressions
185///
186/// This function works with any type implementing [`FnFindInMap`],
187/// allowing for type-safe mapping lookups across different expression types.
188///
189/// # Arguments
190///
191/// * `map_name` - MapName referencing the mapping to look up
192/// * `top_level_key` - First-level key in the mapping
193/// * `second_level_key` - Second-level key in the mapping
194///
195/// # Examples
196///
197/// ```
198/// # use stratosphere_core::value::*;
199/// # use stratosphere_core::template::MapName;
200/// let region_map: MapName = "RegionMap".into();
201/// let expr: ExpString = fn_find_in_map(®ion_map, "us-east-1", "AMI");
202/// ```
203pub fn fn_find_in_map<T: FnFindInMap>(
204 map_name: impl Into<crate::template::MapName>,
205 top_level_key: impl Into<ExpString>,
206 second_level_key: impl Into<String>,
207) -> T {
208 T::fn_find_in_map(map_name, top_level_key, second_level_key)
209}
210
211/// Type-specific helper for string find in map expressions that doesn't require turbofish syntax.
212/// Use this when working with ExpString values.
213pub fn fn_find_in_map_string(
214 map_name: impl Into<crate::template::MapName>,
215 top_level_key: impl Into<ExpString>,
216 second_level_key: impl Into<String>,
217) -> ExpString {
218 ExpString::fn_find_in_map(map_name, top_level_key, second_level_key)
219}
220
221/// Type-specific helper for boolean find in map expressions that doesn't require turbofish syntax.
222/// Use this when working with ExpBool values.
223pub fn fn_find_in_map_bool(
224 map_name: impl Into<crate::template::MapName>,
225 top_level_key: impl Into<ExpString>,
226 second_level_key: impl Into<String>,
227) -> ExpBool {
228 ExpBool::fn_find_in_map(map_name, top_level_key, second_level_key)
229}
230
231/// Helper function to create a Fn::GetAZs expression
232///
233/// Returns an ExpStringList representing the list of Availability Zones for a region.
234/// Pass an empty string to get AZs for the current region.
235///
236/// # Arguments
237///
238/// * `region` - The region to get AZs for, or empty string for current region
239///
240/// # Examples
241///
242/// ```
243/// # use stratosphere_core::value::*;
244/// // Get AZs for current region
245/// let azs = fn_get_azs("");
246///
247/// // Get AZs for specific region
248/// let azs = fn_get_azs("us-west-2");
249///
250/// // Use with Fn::Select to pick first AZ
251/// let first_az = fn_select_string(0, fn_get_azs(""));
252/// ```
253pub fn fn_get_azs(region: impl Into<ExpString>) -> ExpStringList {
254 ExpStringList::GetAZs {
255 region: Box::new(region.into()),
256 }
257}
258
259/// Helper function to create a Fn::Cidr expression
260///
261/// Returns an ExpStringList representing a list of CIDR address blocks.
262/// Used to generate subnet CIDR blocks from a larger VPC CIDR block.
263///
264/// # Arguments
265///
266/// * `ip_block` - The CIDR block to divide (e.g., "10.0.0.0/16")
267/// * `count` - The number of CIDRs to generate (1-256)
268/// * `cidr_bits` - The number of subnet bits for the CIDR (typically 8 for /24 subnets)
269///
270/// # Examples
271///
272/// ```
273/// # use stratosphere_core::value::*;
274/// // Generate 6 /24 subnets from a /16 VPC
275/// let subnets = fn_cidr("10.0.0.0/16", 6, 8);
276///
277/// // Use with Fn::Select to pick specific subnet
278/// let first_subnet = fn_select_string(0, fn_cidr("10.0.0.0/16", 6, 8));
279/// ```
280pub fn fn_cidr(ip_block: impl Into<ExpString>, count: u8, cidr_bits: u8) -> ExpStringList {
281 ExpStringList::Cidr {
282 ip_block: Box::new(ip_block.into()),
283 count,
284 cidr_bits,
285 }
286}
287
288pub trait ToValue {
289 fn to_value(&self) -> serde_json::Value;
290}
291
292#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
293pub struct OutputExportName(String);
294
295impl From<&str> for OutputExportName {
296 fn from(value: &str) -> Self {
297 Self(value.to_string())
298 }
299}
300
301impl<T: ToValue> ToValue for Box<T> {
302 fn to_value(&self) -> serde_json::Value {
303 self.as_ref().to_value()
304 }
305}
306
307#[derive(Clone, Debug, Eq, PartialEq)]
308pub enum ExpString {
309 Base64(Box<ExpString>),
310 FindInMap {
311 map_name: crate::template::MapName,
312 top_level_key: Box<ExpString>,
313 second_level_key: String,
314 },
315 GetAtt {
316 logical_resource_name: LogicalResourceName,
317 attribute_name: AttributeName,
318 },
319 If {
320 condition_name: ConditionName,
321 true_branch: Box<ExpString>,
322 else_branch: Box<ExpString>,
323 },
324 ImportValue(OutputExportName),
325 Join {
326 delimiter: String,
327 values: Vec<ExpString>,
328 },
329 Literal(String),
330 Ref(LogicalResourceName),
331 Select {
332 index: u8,
333 values: Box<ExpStringList>,
334 },
335 Split {
336 delimiter: String,
337 source: Box<ExpString>,
338 },
339 Sub {
340 pattern: String,
341 },
342 // Pseudo parameters that return strings
343 AwsAccountId,
344 AwsPartition,
345 AwsRegion,
346 AwsStackId,
347 AwsStackName,
348 AwsUrlSuffix,
349}
350
351impl ExpString {
352 pub fn base64(self) -> ExpString {
353 ExpString::Base64(Box::new(self))
354 }
355}
356
357impl FnIf for ExpString {
358 fn fn_if(
359 condition_name: impl Into<ConditionName>,
360 true_branch: impl Into<Self>,
361 else_branch: impl Into<Self>,
362 ) -> Self {
363 ExpString::If {
364 condition_name: condition_name.into(),
365 true_branch: Box::new(true_branch.into()),
366 else_branch: Box::new(else_branch.into()),
367 }
368 }
369}
370
371impl FnSelect for ExpString {
372 type ValueList = ExpStringList;
373
374 fn fn_select(index: u8, values: Self::ValueList) -> Self {
375 ExpString::Select {
376 index,
377 values: Box::new(values),
378 }
379 }
380}
381
382impl FnFindInMap for ExpString {
383 fn fn_find_in_map(
384 map_name: impl Into<crate::template::MapName>,
385 top_level_key: impl Into<ExpString>,
386 second_level_key: impl Into<String>,
387 ) -> Self {
388 ExpString::FindInMap {
389 map_name: map_name.into(),
390 top_level_key: Box::new(top_level_key.into()),
391 second_level_key: second_level_key.into(),
392 }
393 }
394}
395
396impl serde::Serialize for ExpString {
397 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
398 self.to_value().serialize(serializer)
399 }
400}
401
402impl From<&str> for ExpString {
403 fn from(value: &str) -> Self {
404 Self::Literal(value.to_string())
405 }
406}
407
408impl From<LogicalResourceName> for ExpString {
409 fn from(value: LogicalResourceName) -> Self {
410 Self::Ref(value)
411 }
412}
413
414impl From<&LogicalResourceName> for ExpString {
415 fn from(value: &LogicalResourceName) -> Self {
416 Self::Ref(value.clone())
417 }
418}
419
420pub fn join(delimiter: &str, values: impl IntoIterator<Item = ExpString>) -> ExpString {
421 ExpString::Join {
422 delimiter: delimiter.to_string(),
423 values: values.into_iter().collect(),
424 }
425}
426
427pub fn get_att(
428 logical_resource_name: impl Into<LogicalResourceName>,
429 attribute_name: impl Into<AttributeName>,
430) -> ExpString {
431 ExpString::GetAtt {
432 logical_resource_name: logical_resource_name.into(),
433 attribute_name: attribute_name.into(),
434 }
435}
436
437pub fn get_att_arn(logical_resource_name: impl Into<LogicalResourceName>) -> ExpString {
438 get_att(logical_resource_name, "Arn")
439}
440
441pub fn mk_name(suffix: impl Into<ExpString>) -> ExpString {
442 join("-", [AWS_STACK_NAME, suffix.into()])
443}
444
445impl ToValue for &String {
446 fn to_value(&self) -> serde_json::Value {
447 ExpString::Literal(self.to_string()).to_value()
448 }
449}
450
451impl ToValue for &LogicalResourceName {
452 fn to_value(&self) -> serde_json::Value {
453 serde_json::to_value(&self.0).unwrap()
454 }
455}
456
457impl ToValue for ExpString {
458 /// Render expression to CF template value^
459 ///
460 /// # Panics
461 ///
462 /// On internal errors/bugs, there is no public API that
463 /// allows to construct values that panic on this call.
464 ///
465 /// # Examples
466 ///
467 /// [Fn::Base64](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-base64.html)
468 ///
469 /// ```
470 /// # use stratosphere_core::value::*;
471 /// # use serde_json::json;
472 ///
473 /// assert_eq!(
474 /// json!({"Fn::Base64":"some-literal"}),
475 /// ExpString::from("some-literal").base64().to_value()
476 /// )
477 /// ```
478 ///
479 /// [Fn::If](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#intrinsic-function-reference-conditions-if)
480 ///
481 /// ```
482 /// # use stratosphere_core::template::*;
483 /// # use stratosphere_core::value::*;
484 /// # use serde_json::json;
485 /// assert_eq!(
486 /// json!({"Fn::If":["condition-name",{"Ref":"resource-a"},{"Ref":"resource-b"}]}),
487 /// ExpString::If{
488 /// condition_name: "condition-name".into(),
489 /// true_branch: Box::new(LogicalResourceName::from("resource-a").into()),
490 /// else_branch: Box::new(LogicalResourceName::from("resource-b").into()),
491 /// }.to_value()
492 /// )
493 /// ```
494 ///
495 /// [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html)
496 ///
497 /// ```
498 /// # use stratosphere_core::template::LogicalResourceName;
499 /// # use stratosphere_core::value::*;
500 /// # use serde_json::json;
501 ///
502 /// let logical_resource_name = LogicalResourceName::from("some-logical-resource-name");
503 /// let reference : ExpString = logical_resource_name.into();
504 ///
505 /// assert_eq!(
506 /// json!({"Ref":"some-logical-resource-name"}),
507 /// reference.to_value()
508 /// )
509 /// ```
510 ///
511 /// [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html)
512 ///
513 /// ```
514 /// # use stratosphere_core::value::*;
515 /// # use serde_json::json;
516 /// assert_eq!(
517 /// json!({"Fn::GetAtt":["some-logical-resource-name", "some-attribute-name"]}),
518 /// ExpString::GetAtt{
519 /// logical_resource_name: "some-logical-resource-name".into(),
520 /// attribute_name: "some-attribute-name".into()
521 /// }.to_value()
522 /// )
523 /// ```
524 ///
525 /// String Literal
526 ///
527 /// ```
528 /// # use stratosphere_core::value::*;
529 /// # use serde_json::json;
530 ///
531 /// let exp : ExpString = "some-literal".into();
532 ///
533 /// assert_eq!(json!{"some-literal"}, exp.to_value())
534 /// ```
535 ///
536 /// [Fn::Join](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-join.html)
537 ///
538 /// ```
539 /// # use stratosphere_core::template::*;
540 /// # use stratosphere_core::value::*;
541 /// # use serde_json::json;
542 /// assert_eq!(
543 /// json!({"Fn::Join":[',', [{"Ref": "some-logical-resource-name"}, "some-literal"]]}),
544 /// ExpString::Join{
545 /// delimiter: String::from(","),
546 /// values: vec![
547 /// LogicalResourceName::from("some-logical-resource-name").into(),
548 /// "some-literal".into()
549 /// ]
550 /// }.to_value()
551 /// )
552 /// ```
553 fn to_value(&self) -> serde_json::Value {
554 match self {
555 ExpString::Base64(value) => mk_func("Fn::Base64", value.to_value()),
556 ExpString::FindInMap {
557 map_name,
558 top_level_key,
559 second_level_key,
560 } => mk_func(
561 "Fn::FindInMap",
562 vec![
563 serde_json::to_value(map_name).unwrap(),
564 top_level_key.to_value(),
565 serde_json::to_value(second_level_key).unwrap(),
566 ],
567 ),
568 ExpString::GetAtt {
569 logical_resource_name,
570 attribute_name,
571 } => mk_func(
572 "Fn::GetAtt",
573 &[
574 serde_json::to_value(logical_resource_name).unwrap(),
575 serde_json::to_value(attribute_name).unwrap(),
576 ],
577 ),
578 ExpString::If {
579 condition_name,
580 true_branch,
581 else_branch,
582 } => mk_func(
583 "Fn::If",
584 &[
585 serde_json::to_value(condition_name).unwrap(),
586 true_branch.to_value(),
587 else_branch.to_value(),
588 ],
589 ),
590 ExpString::Join { delimiter, values } => mk_func(
591 "Fn::Join",
592 vec![
593 delimiter.to_value(),
594 serde_json::to_value(
595 values
596 .iter()
597 .map(|item| item.to_value())
598 .collect::<Vec<_>>(),
599 )
600 .unwrap(),
601 ],
602 ),
603 ExpString::Literal(value) => serde_json::to_value(value).unwrap(),
604 ExpString::Ref(value) => mk_ref(value),
605 ExpString::ImportValue(value) => mk_func("Fn::ImportValue", value),
606 ExpString::Sub { pattern } => mk_func("Fn::Sub", pattern),
607 ExpString::Select { index, values } => mk_func(
608 "Fn::Select",
609 vec![serde_json::to_value(index).unwrap(), values.to_value()],
610 ),
611 ExpString::Split { delimiter, source } => mk_func(
612 "Fn::Split",
613 vec![serde_json::to_value(delimiter).unwrap(), source.to_value()],
614 ),
615 // Pseudo parameters serialize as Refs
616 ExpString::AwsAccountId => mk_ref("AWS::AccountId"),
617 ExpString::AwsPartition => mk_ref("AWS::Partition"),
618 ExpString::AwsRegion => mk_ref("AWS::Region"),
619 ExpString::AwsStackId => mk_ref("AWS::StackId"),
620 ExpString::AwsStackName => mk_ref("AWS::StackName"),
621 ExpString::AwsUrlSuffix => mk_ref("AWS::URLSuffix"),
622 }
623 }
624}
625
626impl<A: ToValue> ToValue for Vec<A> {
627 fn to_value(&self) -> serde_json::Value {
628 self.iter().map(ToValue::to_value).collect()
629 }
630}
631
632impl ToValue for i64 {
633 fn to_value(&self) -> serde_json::Value {
634 serde_json::to_value(self).unwrap()
635 }
636}
637
638impl ToValue for f64 {
639 fn to_value(&self) -> serde_json::Value {
640 serde_json::to_value(self).unwrap()
641 }
642}
643
644impl ToValue for bool {
645 fn to_value(&self) -> serde_json::Value {
646 serde_json::to_value(self).unwrap()
647 }
648}
649
650impl ToValue for serde_json::Value {
651 fn to_value(&self) -> serde_json::Value {
652 self.clone()
653 }
654}
655
656impl ToValue for std::collections::BTreeMap<String, ExpString> {
657 fn to_value(&self) -> serde_json::Value {
658 serde_json::Value::Object(serde_json::Map::from_iter(
659 self.iter().map(|(key, exp)| (key.clone(), exp.to_value())),
660 ))
661 }
662}
663
664impl ToValue for chrono::DateTime<chrono::Utc> {
665 /// Converts a timestamp to a JSON string value in RFC3339 format
666 ///
667 /// # Examples
668 ///
669 /// ```
670 /// # use stratosphere_core::value::ToValue;
671 /// # use chrono::{DateTime, Utc, TimeZone};
672 /// # use serde_json::json;
673 ///
674 /// let timestamp = Utc.with_ymd_and_hms(2024, 10, 13, 19, 0, 0).unwrap();
675 /// let value = timestamp.to_value();
676 ///
677 /// assert_eq!(value, json!("2024-10-13T19:00:00+00:00"));
678 /// ```
679 fn to_value(&self) -> serde_json::Value {
680 serde_json::to_value(self.to_rfc3339()).unwrap()
681 }
682}
683
684fn mk_func<T: serde::Serialize>(name: &str, value: T) -> serde_json::Value {
685 json!({name:value})
686}
687
688fn mk_ref<T: serde::Serialize>(value: T) -> serde_json::Value {
689 mk_func("Ref", value)
690}
691
692#[derive(Clone, Debug)]
693pub enum ExpPair {
694 Bool {
695 left: Box<ExpBool>,
696 right: Box<ExpBool>,
697 },
698 String {
699 left: Box<ExpString>,
700 right: Box<ExpString>,
701 },
702}
703
704#[derive(Clone, Debug, Eq, PartialEq)]
705pub enum ExpStringList {
706 Cidr {
707 ip_block: Box<ExpString>,
708 count: u8,
709 cidr_bits: u8,
710 },
711 GetAZs {
712 region: Box<ExpString>,
713 },
714 Literal(Vec<ExpString>),
715 // Pseudo parameter that returns a list of strings
716 AwsNotificationArns,
717}
718
719impl From<Vec<ExpString>> for ExpStringList {
720 fn from(values: Vec<ExpString>) -> Self {
721 ExpStringList::Literal(values)
722 }
723}
724
725impl ToValue for ExpStringList {
726 fn to_value(&self) -> serde_json::Value {
727 match self {
728 ExpStringList::Cidr {
729 ip_block,
730 count,
731 cidr_bits,
732 } => mk_func(
733 "Fn::Cidr",
734 vec![
735 ip_block.to_value(),
736 serde_json::to_value(count).unwrap(),
737 serde_json::to_value(cidr_bits).unwrap(),
738 ],
739 ),
740 ExpStringList::GetAZs { region } => mk_func("Fn::GetAZs", region.to_value()),
741 ExpStringList::Literal(values) => serde_json::to_value(
742 values
743 .iter()
744 .map(|item| item.to_value())
745 .collect::<Vec<_>>(),
746 )
747 .unwrap(),
748 // Pseudo parameter that returns a list serializes as a Ref
749 ExpStringList::AwsNotificationArns => mk_ref("AWS::NotificationARNs"),
750 }
751 }
752}
753
754#[derive(Clone, Debug)]
755pub enum ExpBool {
756 And(Box<ExpBool>, Box<ExpBool>),
757 Equals(ExpPair),
758 FindInMap {
759 map_name: crate::template::MapName,
760 top_level_key: Box<ExpString>,
761 second_level_key: String,
762 },
763 If {
764 condition_name: ConditionName,
765 true_branch: Box<ExpBool>,
766 else_branch: Box<ExpBool>,
767 },
768 Literal(bool),
769 Not(Box<ExpBool>),
770 Or(Vec<ExpBool>),
771 Select {
772 index: u8,
773 values: Vec<ExpBool>,
774 },
775}
776
777impl From<bool> for ExpBool {
778 fn from(value: bool) -> Self {
779 Self::Literal(value)
780 }
781}
782
783impl FnIf for ExpBool {
784 fn fn_if(
785 condition_name: impl Into<ConditionName>,
786 true_branch: impl Into<Self>,
787 else_branch: impl Into<Self>,
788 ) -> Self {
789 Self::If {
790 condition_name: condition_name.into(),
791 true_branch: Box::new(true_branch.into()),
792 else_branch: Box::new(else_branch.into()),
793 }
794 }
795}
796
797impl FnSelect for ExpBool {
798 type ValueList = Vec<ExpBool>;
799
800 fn fn_select(index: u8, values: Self::ValueList) -> Self {
801 ExpBool::Select { index, values }
802 }
803}
804
805impl FnFindInMap for ExpBool {
806 fn fn_find_in_map(
807 map_name: impl Into<crate::template::MapName>,
808 top_level_key: impl Into<ExpString>,
809 second_level_key: impl Into<String>,
810 ) -> Self {
811 ExpBool::FindInMap {
812 map_name: map_name.into(),
813 top_level_key: Box::new(top_level_key.into()),
814 second_level_key: second_level_key.into(),
815 }
816 }
817}
818
819impl ToValue for ExpBool {
820 /// Literal
821 ///
822 /// ```
823 /// # use stratosphere_core::value::*;
824 /// # use serde_json::json;
825 /// assert_eq!(
826 /// serde_json::Value::Bool(true),
827 /// ExpBool::Literal(true).to_value()
828 /// )
829 /// ```
830 ///
831 /// [Fn::Equals](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#intrinsic-function-reference-conditions-equals)
832 ///
833 /// ```
834 /// # use stratosphere_core::template::*;
835 /// # use stratosphere_core::value::*;
836 /// # use serde_json::json;
837 ///
838 /// assert_eq!(
839 /// json!({"Fn::Equals":[{"Ref":"resource-a"},"some-literal"]}),
840 /// equals_string(
841 /// LogicalResourceName::from("resource-a"),
842 /// "some-literal"
843 /// ).to_value()
844 /// );
845 ///
846 /// assert_eq!(
847 /// json!({"Fn::Equals":[true,false]}),
848 /// equals_bool(
849 /// true,
850 /// false
851 /// ).to_value()
852 /// )
853 /// ```
854 ///
855 fn to_value(&self) -> serde_json::Value {
856 match self {
857 ExpBool::And(left, right) => mk_func("Fn::And", [left.to_value(), right.to_value()]),
858 ExpBool::Equals(pair) => match pair {
859 ExpPair::Bool { left, right } => {
860 mk_func("Fn::Equals", [left.to_value(), right.to_value()])
861 }
862 ExpPair::String { left, right } => {
863 mk_func("Fn::Equals", [left.to_value(), right.to_value()])
864 }
865 },
866 ExpBool::FindInMap {
867 map_name,
868 top_level_key,
869 second_level_key,
870 } => mk_func(
871 "Fn::FindInMap",
872 vec![
873 serde_json::to_value(map_name).unwrap(),
874 top_level_key.to_value(),
875 serde_json::to_value(second_level_key).unwrap(),
876 ],
877 ),
878 ExpBool::If {
879 condition_name,
880 true_branch,
881 else_branch,
882 } => mk_func(
883 "Fn::If",
884 [
885 serde_json::to_value(condition_name).unwrap(),
886 true_branch.to_value(),
887 else_branch.to_value(),
888 ],
889 ),
890 ExpBool::Literal(value) => serde_json::Value::Bool(*value),
891 ExpBool::Not(value) => mk_func("Fn::Not", [value.to_value()]),
892 ExpBool::Or(conditions) => mk_func(
893 "Fn::Or",
894 conditions
895 .iter()
896 .map(|condition| condition.to_value())
897 .collect::<Vec<_>>(),
898 ),
899 ExpBool::Select { index, values } => mk_func(
900 "Fn::Select",
901 [
902 serde_json::Value::Number((*index).into()),
903 serde_json::Value::Array(
904 values.iter().map(|v| v.to_value()).collect::<Vec<_>>(),
905 ),
906 ],
907 ),
908 }
909 }
910}
911
912// Pseudo parameter constants
913
914/// AWS account ID (e.g., "123456789012")
915///
916/// This pseudo parameter resolves to the AWS account ID of the account
917/// in which the CloudFormation stack is being created.
918///
919/// # Examples
920///
921/// ```
922/// # use stratosphere_core::value::*;
923/// # use serde_json::json;
924/// assert_eq!(AWS_ACCOUNT_ID.to_value(), json!({"Ref": "AWS::AccountId"}));
925/// ```
926pub const AWS_ACCOUNT_ID: ExpString = ExpString::AwsAccountId;
927
928/// AWS partition (e.g., "aws", "aws-cn", "aws-us-gov")
929///
930/// This pseudo parameter resolves to the partition that the resource is in.
931/// For standard AWS Regions, the partition is "aws". For resources in other
932/// partitions, the partition is "aws-partitionname".
933///
934/// # Examples
935///
936/// ```
937/// # use stratosphere_core::value::*;
938/// # use serde_json::json;
939/// assert_eq!(AWS_PARTITION.to_value(), json!({"Ref": "AWS::Partition"}));
940/// ```
941pub const AWS_PARTITION: ExpString = ExpString::AwsPartition;
942
943/// AWS region (e.g., "us-east-1")
944///
945/// This pseudo parameter resolves to the AWS Region in which the
946/// CloudFormation stack is being created.
947///
948/// # Examples
949///
950/// ```
951/// # use stratosphere_core::value::*;
952/// # use serde_json::json;
953/// assert_eq!(AWS_REGION.to_value(), json!({"Ref": "AWS::Region"}));
954/// ```
955pub const AWS_REGION: ExpString = ExpString::AwsRegion;
956
957/// Stack ID/ARN
958///
959/// This pseudo parameter resolves to the ID (ARN) of the stack.
960///
961/// # Examples
962///
963/// ```
964/// # use stratosphere_core::value::*;
965/// # use serde_json::json;
966/// assert_eq!(AWS_STACK_ID.to_value(), json!({"Ref": "AWS::StackId"}));
967/// ```
968pub const AWS_STACK_ID: ExpString = ExpString::AwsStackId;
969
970/// Stack name
971///
972/// This pseudo parameter resolves to the name of the CloudFormation stack.
973///
974/// # Examples
975///
976/// ```
977/// # use stratosphere_core::value::*;
978/// # use serde_json::json;
979/// assert_eq!(AWS_STACK_NAME.to_value(), json!({"Ref": "AWS::StackName"}));
980/// ```
981pub const AWS_STACK_NAME: ExpString = ExpString::AwsStackName;
982
983/// AWS domain suffix (e.g., "amazonaws.com")
984///
985/// This pseudo parameter resolves to the suffix for a domain. The suffix is
986/// typically "amazonaws.com", but might differ by Region. For example, in
987/// China (Beijing), the suffix is "amazonaws.com.cn".
988///
989/// # Examples
990///
991/// ```
992/// # use stratosphere_core::value::*;
993/// # use serde_json::json;
994/// assert_eq!(AWS_URL_SUFFIX.to_value(), json!({"Ref": "AWS::URLSuffix"}));
995/// ```
996pub const AWS_URL_SUFFIX: ExpString = ExpString::AwsUrlSuffix;
997
998/// List of SNS topic ARNs for stack notifications
999///
1000/// This pseudo parameter resolves to the list of notification Amazon SNS
1001/// topic ARNs for the current stack.
1002///
1003/// # Examples
1004///
1005/// ```
1006/// # use stratosphere_core::value::*;
1007/// # use serde_json::json;
1008/// assert_eq!(AWS_NOTIFICATION_ARNS.to_value(), json!({"Ref": "AWS::NotificationARNs"}));
1009/// ```
1010pub const AWS_NOTIFICATION_ARNS: ExpStringList = ExpStringList::AwsNotificationArns;