Skip to main content

stratosphere_core/
template.rs

1use crate::resource_specification::ResourceTypeName;
2use crate::value;
3
4#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Serialize)]
5pub struct LogicalResourceName(pub String);
6
7impl From<&str> for LogicalResourceName {
8    fn from(value: &str) -> Self {
9        Self(value.to_string())
10    }
11}
12
13impl From<&LogicalResourceName> for LogicalResourceName {
14    fn from(value: &Self) -> Self {
15        value.clone()
16    }
17}
18
19impl std::fmt::Display for LogicalResourceName {
20    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
21        write!(formatter, "{}", self.0)
22    }
23}
24
25pub type ResourceProperties = serde_json::Map<String, serde_json::Value>;
26
27pub trait ToResource {
28    const RESOURCE_TYPE_NAME: ResourceTypeName<'static>;
29    fn to_resource_properties(&self) -> ResourceProperties;
30}
31
32#[derive(Debug, PartialEq, serde::Serialize)]
33pub struct Resource<'a> {
34    #[serde(rename = "Condition", skip_serializing_if = "Option::is_none")]
35    condition: Option<value::ConditionName>,
36    #[serde(rename = "Type")]
37    resource_type_identifier: ResourceTypeName<'a>,
38    #[serde(rename = "Properties")]
39    resource_properties: ResourceProperties,
40}
41
42#[derive(Debug, Eq, PartialEq)]
43pub enum Version {
44    V2010_09_09,
45}
46
47impl serde::Serialize for Version {
48    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
49        match self {
50            Version::V2010_09_09 => serializer.serialize_str("2010-09-09"),
51        }
52    }
53}
54
55#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Serialize)]
56pub struct ParameterKey(pub String);
57
58impl std::fmt::Display for ParameterKey {
59    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
60        write!(formatter, "{}", self.0)
61    }
62}
63
64impl From<&str> for ParameterKey {
65    fn from(value: &str) -> Self {
66        Self(value.into())
67    }
68}
69
70impl From<ParameterKey> for value::ExpString {
71    fn from(value: ParameterKey) -> Self {
72        (&value).into()
73    }
74}
75
76impl From<&ParameterKey> for value::ExpString {
77    fn from(value: &ParameterKey) -> Self {
78        Self::Ref(value.0.as_str().into())
79    }
80}
81
82#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
83pub enum ParameterType {
84    String,
85}
86
87#[derive(Clone, Debug, PartialEq, serde::Serialize)]
88pub struct Parameter {
89    #[serde(rename = "Description")]
90    pub description: Option<String>,
91    #[serde(rename = "Type")]
92    pub r#type: ParameterType,
93    #[serde(rename = "AllowedPattern", skip_serializing_if = "Option::is_none")]
94    pub allowed_pattern: Option<String>,
95}
96
97pub type ParameterKeys = std::collections::BTreeSet<ParameterKey>;
98
99#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Serialize)]
100pub struct OutputKey(pub String);
101
102impl std::fmt::Display for OutputKey {
103    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
104        write!(formatter, "{}", self.0)
105    }
106}
107
108impl From<&str> for OutputKey {
109    fn from(value: &str) -> Self {
110        Self(value.into())
111    }
112}
113
114impl OutputKey {
115    #[must_use]
116    pub fn as_str(&self) -> &str {
117        &self.0
118    }
119}
120
121#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Serialize)]
122pub struct MapName(pub String);
123
124impl std::fmt::Display for MapName {
125    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
126        write!(formatter, "{}", self.0)
127    }
128}
129
130impl From<&str> for MapName {
131    fn from(value: &str) -> Self {
132        Self(value.into())
133    }
134}
135
136impl From<&MapName> for MapName {
137    fn from(value: &MapName) -> Self {
138        value.clone()
139    }
140}
141
142impl From<MapName> for String {
143    fn from(value: MapName) -> Self {
144        value.0
145    }
146}
147
148impl From<&MapName> for String {
149    fn from(value: &MapName) -> Self {
150        value.0.clone()
151    }
152}
153
154impl MapName {
155    #[must_use]
156    pub fn as_str(&self) -> &str {
157        &self.0
158    }
159}
160
161pub type Mapping =
162    std::collections::BTreeMap<String, std::collections::BTreeMap<String, serde_json::Value>>;
163
164#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
165pub struct Output {
166    #[serde(rename = "Condition", skip_serializing_if = "Option::is_none")]
167    pub condition: Option<value::ConditionName>,
168    #[serde(rename = "Description")]
169    pub description: value::ExpString,
170    #[serde(rename = "Value")]
171    pub value: value::ExpString,
172    #[serde(rename = "Export", skip_serializing_if = "Option::is_none")]
173    pub export: Option<OutputExport>,
174}
175
176#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
177pub struct OutputExport {
178    #[serde(rename = "Name")]
179    pub name: value::ExpString,
180    #[serde(rename = "Value", skip_serializing_if = "Option::is_none")]
181    pub value: Option<value::ExpString>,
182}
183
184#[derive(Debug, PartialEq, serde::Serialize)]
185pub struct Template<'a> {
186    #[serde(rename = "AWSTemplateFormatVersion")]
187    version: Version,
188    #[serde(rename = "Description", skip_serializing_if = "Option::is_none")]
189    description: Option<String>,
190    #[serde(
191        rename = "Conditions",
192        skip_serializing_if = "std::collections::BTreeMap::is_empty"
193    )]
194    conditions: std::collections::BTreeMap<value::ConditionName, value::ExpBool>,
195    #[serde(
196        rename = "Mappings",
197        skip_serializing_if = "std::collections::BTreeMap::is_empty"
198    )]
199    mappings: std::collections::BTreeMap<MapName, Mapping>,
200    #[serde(
201        rename = "Outputs",
202        skip_serializing_if = "std::collections::BTreeMap::is_empty"
203    )]
204    outputs: std::collections::BTreeMap<OutputKey, Output>,
205    #[serde(
206        rename = "Parameters",
207        skip_serializing_if = "std::collections::BTreeMap::is_empty"
208    )]
209    parameters: std::collections::BTreeMap<ParameterKey, Parameter>,
210    #[serde(rename = "Resources")]
211    resources: std::collections::BTreeMap<LogicalResourceName, Resource<'a>>,
212}
213
214impl Default for Template<'_> {
215    fn default() -> Self {
216        Self::new()
217    }
218}
219
220impl Template<'_> {
221    #[must_use]
222    pub fn new() -> Self {
223        Self {
224            conditions: std::collections::BTreeMap::new(),
225            description: None,
226            mappings: std::collections::BTreeMap::new(),
227            outputs: std::collections::BTreeMap::new(),
228            parameters: std::collections::BTreeMap::new(),
229            resources: std::collections::BTreeMap::new(),
230            version: Version::V2010_09_09,
231        }
232    }
233
234    pub fn build(action: fn(&mut Self) -> ()) -> Self {
235        let mut template = Self::new();
236
237        action(&mut template);
238
239        template
240    }
241
242    pub fn resource<R: ToResource>(
243        &mut self,
244        logical_resource_name: impl Into<LogicalResourceName>,
245        resource: R,
246    ) -> LogicalResourceName {
247        let logical_resource_name = logical_resource_name.into();
248
249        let resource = Resource {
250            condition: None,
251            resource_type_identifier: R::RESOURCE_TYPE_NAME,
252            resource_properties: resource.to_resource_properties(),
253        };
254
255        match self
256            .resources
257            .insert(logical_resource_name.clone(), resource)
258        {
259            None => logical_resource_name,
260            Some(_existing) => {
261                panic!("Logical resource with name: {logical_resource_name} already exists")
262            }
263        }
264    }
265
266    pub fn parameter(
267        &mut self,
268        parameter_key: impl Into<ParameterKey>,
269        parameter: Parameter,
270    ) -> ParameterKey {
271        let parameter_key = parameter_key.into();
272
273        if let Some(_existing) = self.parameters.insert(parameter_key.clone(), parameter) {
274            panic!("Parameter key: {parameter_key} already exists")
275        }
276
277        parameter_key
278    }
279
280    pub fn parameter_(
281        mut self,
282        parameter_key: impl Into<ParameterKey>,
283        parameter: Parameter,
284    ) -> Self {
285        self.parameter(parameter_key, parameter);
286        self
287    }
288
289    pub fn condition(
290        &mut self,
291        condition_name: impl Into<value::ConditionName>,
292        expression: value::ExpBool,
293    ) -> value::ConditionName {
294        let condition_name = condition_name.into();
295
296        if let Some(_existing) = self.conditions.insert(condition_name.clone(), expression) {
297            panic!("Condition with name: {condition_name} already exists")
298        }
299
300        condition_name
301    }
302
303    pub fn condition_(
304        mut self,
305        condition_name: impl Into<value::ConditionName>,
306        expression: value::ExpBool,
307    ) -> Self {
308        self.condition(condition_name, expression);
309        self
310    }
311
312    pub fn conditional_resource<R: ToResource>(
313        &mut self,
314        logical_resource_name: impl Into<LogicalResourceName>,
315        condition_name: impl Into<value::ConditionName>,
316        resource: R,
317    ) -> LogicalResourceName {
318        let logical_resource_name = logical_resource_name.into();
319
320        let resource = Resource {
321            condition: Some(condition_name.into()),
322            resource_type_identifier: R::RESOURCE_TYPE_NAME,
323            resource_properties: resource.to_resource_properties(),
324        };
325
326        match self
327            .resources
328            .insert(logical_resource_name.clone(), resource)
329        {
330            None => logical_resource_name,
331            Some(_existing) => {
332                panic!("Logical resource with name: {logical_resource_name} already exists")
333            }
334        }
335    }
336
337    pub fn conditional_resource_<R: ToResource>(
338        mut self,
339        logical_resource_name: impl Into<LogicalResourceName>,
340        condition_name: impl Into<value::ConditionName>,
341        resource: R,
342    ) -> Self {
343        self.conditional_resource(logical_resource_name, condition_name, resource);
344        self
345    }
346
347    pub fn mapping(&mut self, map_name: impl Into<MapName>, mapping: Mapping) -> MapName {
348        let map_name = map_name.into();
349
350        if let Some(_existing) = self.mappings.insert(map_name.clone(), mapping) {
351            panic!("Mapping with name: {map_name} already exists")
352        }
353
354        map_name
355    }
356
357    pub fn mapping_(mut self, map_name: impl Into<MapName>, mapping: Mapping) -> Self {
358        self.mapping(map_name, mapping);
359        self
360    }
361
362    pub fn output(&mut self, output_key: impl Into<OutputKey>, output: Output) -> OutputKey {
363        let output_key = output_key.into();
364
365        if let Some(_old) = self.outputs.insert(output_key.clone(), output) {
366            panic!("Output with name: {output_key} already exists")
367        }
368
369        output_key
370    }
371
372    pub fn output_(mut self, output_key: impl Into<OutputKey>, output: Output) -> Self {
373        self.output(output_key, output);
374        self
375    }
376
377    pub fn output_export(
378        &mut self,
379        output_key: impl Into<OutputKey>,
380        description: &str,
381        value: impl Into<value::ExpString>,
382    ) -> OutputKey {
383        let output_key = output_key.into();
384
385        self.output(
386            output_key.clone(),
387            Output {
388                condition: None,
389                description: description.into(),
390                value: value.into(),
391                export: Some(OutputExport {
392                    name: value::join(":", [value::AWS_STACK_NAME, output_key.as_str().into()]),
393                    value: None,
394                }),
395            },
396        )
397    }
398
399    pub fn resource_<R: ToResource>(
400        mut self,
401        logical_resource_name: impl Into<LogicalResourceName>,
402        resource: R,
403    ) -> Self {
404        self.resource(logical_resource_name, resource);
405        self
406    }
407
408    #[must_use]
409    pub fn render_json_pretty(&self) -> String {
410        let mut string = serde_json::to_string_pretty(&self).unwrap();
411        string.push('\n');
412        string
413    }
414
415    #[must_use]
416    pub fn render_json(&self) -> String {
417        serde_json::to_string(&self).unwrap()
418    }
419
420    #[must_use]
421    pub fn parameter_keys(&self) -> std::collections::BTreeSet<ParameterKey> {
422        self.parameters.keys().cloned().collect()
423    }
424}