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(®ion_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;