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