use super::*;
pub fn parse_template(
template_body: &str,
parameters: &BTreeMap<String, String>,
) -> Result<ParsedTemplate, String> {
parse_template_with_physical_ids(template_body, parameters, &BTreeMap::new())
}
pub fn parse_template_with_physical_ids(
template_body: &str,
parameters: &BTreeMap<String, String>,
resource_physical_ids: &BTreeMap<String, String>,
) -> Result<ParsedTemplate, String> {
parse_template_with_resolution(
template_body,
parameters,
resource_physical_ids,
&BTreeMap::new(),
)
}
pub fn parse_template_with_resolution(
template_body: &str,
parameters: &BTreeMap<String, String>,
resource_physical_ids: &BTreeMap<String, String>,
resource_attributes: &BTreeMap<String, BTreeMap<String, String>>,
) -> Result<ParsedTemplate, String> {
let value: Value = if template_body.trim_start().starts_with('{') {
serde_json::from_str(template_body).map_err(|e| format!("Invalid JSON template: {e}"))?
} else {
serde_yaml::from_str(template_body).map_err(|e| format!("Invalid YAML template: {e}"))?
};
let value = expand_for_each(&value, &BTreeMap::new(), parameters)?;
let value = expand_sam(&value);
let description = value
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let conditions = evaluate_conditions(&value, parameters)?;
let mappings = parse_mappings(&value);
let resources_obj = value
.get("Resources")
.and_then(|v| v.as_object())
.ok_or("Template must contain a Resources section")?;
let mut resources = Vec::new();
for (logical_id, resource) in resources_obj {
if let Some(cond_name) = resource.get("Condition").and_then(|v| v.as_str()) {
if !conditions.get(cond_name).copied().unwrap_or(false) {
continue;
}
}
let resource_type = resource
.get("Type")
.and_then(|v| v.as_str())
.ok_or(format!("Resource {logical_id} must have a Type property"))?
.to_string();
let properties = resource
.get("Properties")
.cloned()
.unwrap_or(Value::Object(serde_json::Map::new()));
let properties = apply_mappings(&properties, parameters, &mappings, &conditions)?;
let resolved = resolve_refs_full(
&properties,
parameters,
resources_obj,
resource_physical_ids,
resource_attributes,
&BTreeMap::new(),
&conditions,
);
let resolved = strip_no_value(resolved);
resources.push(ResourceDefinition {
logical_id: logical_id.clone(),
resource_type,
properties: resolved,
});
}
let outputs = parse_outputs(
&value,
parameters,
resources_obj,
resource_physical_ids,
resource_attributes,
&BTreeMap::new(),
)?;
Ok(ParsedTemplate {
description,
resources,
outputs,
})
}
pub fn collect_import_value_names(
template: &Value,
parameters: &BTreeMap<String, String>,
) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
collect_imports_walk(template, parameters, &mut out);
out.sort();
out.dedup();
out
}
pub(super) fn collect_imports_walk(
value: &Value,
parameters: &BTreeMap<String, String>,
out: &mut Vec<String>,
) {
match value {
Value::Object(map) => {
if let Some(arg) = map.get("Fn::ImportValue") {
if let Some(name) = static_import_name(arg, parameters) {
out.push(name);
} else {
collect_imports_walk(arg, parameters, out);
}
}
for (k, v) in map {
if k == "Fn::ImportValue" {
continue;
}
collect_imports_walk(v, parameters, out);
}
}
Value::Array(arr) => {
for v in arr {
collect_imports_walk(v, parameters, out);
}
}
_ => {}
}
}
pub(super) fn static_import_name(
value: &Value,
parameters: &BTreeMap<String, String>,
) -> Option<String> {
match value {
Value::String(s) => Some(s.clone()),
Value::Object(m) => {
if let Some(name) = m.get("Ref").and_then(|v| v.as_str()) {
return parameters.get(name).cloned();
}
if let Some(s) = m.get("Fn::Sub").and_then(|v| v.as_str()) {
let mut result = s.to_string();
for (k, v) in parameters {
result = result.replace(&format!("${{{k}}}"), v);
}
if !result.contains("${") {
return Some(result);
}
}
None
}
_ => None,
}
}
pub fn parse_outputs(
template: &Value,
parameters: &BTreeMap<String, String>,
resources: &serde_json::Map<String, Value>,
resource_physical_ids: &BTreeMap<String, String>,
resource_attributes: &BTreeMap<String, BTreeMap<String, String>>,
imports: &BTreeMap<String, String>,
) -> Result<Vec<TemplateOutput>, String> {
let template_owned = expand_for_each(template, &BTreeMap::new(), parameters)?;
let template = &template_owned;
let outputs_obj = match template.get("Outputs").and_then(|v| v.as_object()) {
Some(o) => o,
None => return Ok(Vec::new()),
};
let conditions = evaluate_conditions(template, parameters)?;
let mut out = Vec::new();
for (logical_id, body) in outputs_obj {
if let Some(cond_name) = body.get("Condition").and_then(|v| v.as_str()) {
if !conditions.get(cond_name).copied().unwrap_or(false) {
continue;
}
}
let raw_value = match body.get("Value") {
Some(v) => v,
None => continue,
};
let resolved = resolve_refs_full(
raw_value,
parameters,
resources,
resource_physical_ids,
resource_attributes,
imports,
&conditions,
);
let resolved = strip_no_value(resolved);
let value = match resolved {
Value::String(s) => s,
other => other.to_string(),
};
let description = body
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let export_name = body.get("Export").and_then(|e| e.get("Name")).map(|n| {
let resolved = resolve_refs_full(
n,
parameters,
resources,
resource_physical_ids,
resource_attributes,
imports,
&conditions,
);
match resolved {
Value::String(s) => s,
other => other.to_string(),
}
});
out.push(TemplateOutput {
logical_id: logical_id.clone(),
value,
description,
export_name,
});
}
Ok(out)
}