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}