Skip to main content

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