fakecloud_cloudformation/template/
parser.rs1use super::*;
4
5pub fn parse_template(
7 template_body: &str,
8 parameters: &BTreeMap<String, String>,
9) -> Result<ParsedTemplate, String> {
10 parse_template_with_physical_ids(template_body, parameters, &BTreeMap::new())
11}
12
13pub fn parse_template_with_physical_ids(
15 template_body: &str,
16 parameters: &BTreeMap<String, String>,
17 resource_physical_ids: &BTreeMap<String, String>,
18) -> Result<ParsedTemplate, String> {
19 parse_template_with_resolution(
20 template_body,
21 parameters,
22 resource_physical_ids,
23 &BTreeMap::new(),
24 )
25}
26
27pub fn parse_template_with_resolution(
31 template_body: &str,
32 parameters: &BTreeMap<String, String>,
33 resource_physical_ids: &BTreeMap<String, String>,
34 resource_attributes: &BTreeMap<String, BTreeMap<String, String>>,
35) -> Result<ParsedTemplate, String> {
36 let value: Value = if template_body.trim_start().starts_with('{') {
37 serde_json::from_str(template_body).map_err(|e| format!("Invalid JSON template: {e}"))?
38 } else {
39 serde_yaml::from_str(template_body).map_err(|e| format!("Invalid YAML template: {e}"))?
40 };
41
42 let value = expand_for_each(&value, &BTreeMap::new(), parameters)?;
48 let value = expand_sam(&value);
49
50 let description = value
51 .get("Description")
52 .and_then(|v| v.as_str())
53 .map(|s| s.to_string());
54
55 let conditions = evaluate_conditions(&value, parameters)?;
56 let mappings = parse_mappings(&value);
57
58 let resources_obj = value
59 .get("Resources")
60 .and_then(|v| v.as_object())
61 .ok_or("Template must contain a Resources section")?;
62
63 let mut resources = Vec::new();
64 for (logical_id, resource) in resources_obj {
65 if let Some(cond_name) = resource.get("Condition").and_then(|v| v.as_str()) {
68 if !conditions.get(cond_name).copied().unwrap_or(false) {
69 continue;
70 }
71 }
72 let resource_type = resource
73 .get("Type")
74 .and_then(|v| v.as_str())
75 .ok_or(format!("Resource {logical_id} must have a Type property"))?
76 .to_string();
77
78 let properties = resource
79 .get("Properties")
80 .cloned()
81 .unwrap_or(Value::Object(serde_json::Map::new()));
82
83 let properties = apply_mappings(&properties, parameters, &mappings, &conditions)?;
88
89 let resolved = resolve_refs_full(
91 &properties,
92 parameters,
93 resources_obj,
94 resource_physical_ids,
95 resource_attributes,
96 &BTreeMap::new(),
97 &conditions,
98 );
99 let resolved = strip_no_value(resolved);
100
101 resources.push(ResourceDefinition {
102 logical_id: logical_id.clone(),
103 resource_type,
104 properties: resolved,
105 });
106 }
107
108 let outputs = parse_outputs(
109 &value,
110 parameters,
111 resources_obj,
112 resource_physical_ids,
113 resource_attributes,
114 &BTreeMap::new(),
115 )?;
116
117 Ok(ParsedTemplate {
118 description,
119 resources,
120 outputs,
121 })
122}
123
124pub fn collect_import_value_names(
130 template: &Value,
131 parameters: &BTreeMap<String, String>,
132) -> Vec<String> {
133 let mut out: Vec<String> = Vec::new();
134 collect_imports_walk(template, parameters, &mut out);
135 out.sort();
136 out.dedup();
137 out
138}
139
140pub(super) fn collect_imports_walk(
141 value: &Value,
142 parameters: &BTreeMap<String, String>,
143 out: &mut Vec<String>,
144) {
145 match value {
146 Value::Object(map) => {
147 if let Some(arg) = map.get("Fn::ImportValue") {
148 if let Some(name) = static_import_name(arg, parameters) {
149 out.push(name);
150 } else {
151 collect_imports_walk(arg, parameters, out);
153 }
154 }
155 for (k, v) in map {
156 if k == "Fn::ImportValue" {
157 continue;
158 }
159 collect_imports_walk(v, parameters, out);
160 }
161 }
162 Value::Array(arr) => {
163 for v in arr {
164 collect_imports_walk(v, parameters, out);
165 }
166 }
167 _ => {}
168 }
169}
170
171pub(super) fn static_import_name(
172 value: &Value,
173 parameters: &BTreeMap<String, String>,
174) -> Option<String> {
175 match value {
176 Value::String(s) => Some(s.clone()),
177 Value::Object(m) => {
178 if let Some(name) = m.get("Ref").and_then(|v| v.as_str()) {
179 return parameters.get(name).cloned();
180 }
181 if let Some(s) = m.get("Fn::Sub").and_then(|v| v.as_str()) {
182 let mut result = s.to_string();
183 for (k, v) in parameters {
184 result = result.replace(&format!("${{{k}}}"), v);
185 }
186 if !result.contains("${") {
187 return Some(result);
188 }
189 }
190 None
191 }
192 _ => None,
193 }
194}
195
196pub fn parse_outputs(
200 template: &Value,
201 parameters: &BTreeMap<String, String>,
202 resources: &serde_json::Map<String, Value>,
203 resource_physical_ids: &BTreeMap<String, String>,
204 resource_attributes: &BTreeMap<String, BTreeMap<String, String>>,
205 imports: &BTreeMap<String, String>,
206) -> Result<Vec<TemplateOutput>, String> {
207 let template_owned = expand_for_each(template, &BTreeMap::new(), parameters)?;
211 let template = &template_owned;
212 let outputs_obj = match template.get("Outputs").and_then(|v| v.as_object()) {
213 Some(o) => o,
214 None => return Ok(Vec::new()),
215 };
216
217 let conditions = evaluate_conditions(template, parameters)?;
218 let mut out = Vec::new();
219 for (logical_id, body) in outputs_obj {
220 if let Some(cond_name) = body.get("Condition").and_then(|v| v.as_str()) {
223 if !conditions.get(cond_name).copied().unwrap_or(false) {
224 continue;
225 }
226 }
227 let raw_value = match body.get("Value") {
228 Some(v) => v,
229 None => continue,
230 };
231 let resolved = resolve_refs_full(
232 raw_value,
233 parameters,
234 resources,
235 resource_physical_ids,
236 resource_attributes,
237 imports,
238 &conditions,
239 );
240 let resolved = strip_no_value(resolved);
241 let value = match resolved {
242 Value::String(s) => s,
243 other => other.to_string(),
244 };
245 let description = body
246 .get("Description")
247 .and_then(|v| v.as_str())
248 .map(|s| s.to_string());
249 let export_name = body.get("Export").and_then(|e| e.get("Name")).map(|n| {
250 let resolved = resolve_refs_full(
251 n,
252 parameters,
253 resources,
254 resource_physical_ids,
255 resource_attributes,
256 imports,
257 &conditions,
258 );
259 match resolved {
260 Value::String(s) => s,
261 other => other.to_string(),
262 }
263 });
264 out.push(TemplateOutput {
265 logical_id: logical_id.clone(),
266 value,
267 description,
268 export_name,
269 });
270 }
271 Ok(out)
272}