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(&region_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;