use std::{path::Path, sync::Arc};
use crate::{
Diagnostic, Severity,
diagnostic::Diagnostic as Diag,
ir::{AttributeMap, BlockKind, Expression, Local, Span},
loader::{ParseBytesResult, RawBlock},
};
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub(super) struct ParsedTerragrunt {
pub locals: Vec<Local>,
pub inputs: Option<AttributeMap>,
pub includes: Vec<IncludeBlockRaw>,
pub generates: Vec<GenerateBlockRaw>,
pub dependencies: Vec<DependencyBlockRaw>,
pub remote_state: Option<AttributeMap>,
pub terraform: Vec<AttributeMap>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub(super) struct IncludeBlockRaw {
pub label: Arc<str>,
pub path_expr: Option<Expression>,
pub merge_strategy_expr: Option<Expression>,
pub span: Span,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub(super) struct GenerateBlockRaw {
pub label: Arc<str>,
pub attrs: AttributeMap,
pub span: Span,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub(super) struct DependencyBlockRaw {
pub name: Arc<str>,
pub attrs: AttributeMap,
pub span: Span,
}
pub(super) fn project(parsed: &ParseBytesResult, _source_path: &Arc<Path>) -> ParsedTerragrunt {
let mut out = ParsedTerragrunt {
diagnostics: parsed.diagnostics.clone(),
..ParsedTerragrunt::default()
};
for block in &parsed.blocks {
match block.kind {
BlockKind::Locals => {
for (name, value) in &block.body {
out.locals.push(
Local::builder()
.name(Arc::clone(name))
.value(value.clone())
.span(block.span.clone())
.build(),
);
}
}
BlockKind::Inputs => {
out.inputs = Some(block.body.clone());
}
BlockKind::Include => {
project_include(block, &mut out);
}
BlockKind::Generate => {
project_generate(block, &mut out);
}
BlockKind::Dependency => {
project_dependency(block, &mut out);
}
BlockKind::Terraform => {
out.terraform.push(block.body.clone());
}
BlockKind::Unknown => {
if let Some((key, value)) = block.body.first()
&& key.as_ref() == "inputs"
&& let Expression::Object(_) | Expression::Literal(_) = value
{
out.inputs = Some(object_to_attribute_map(value));
} else if let Some((key, value)) = block.body.first()
&& key.as_ref() == "remote_state"
{
out.remote_state = Some(object_to_attribute_map(value));
}
}
_ => {
out.diagnostics.push(Diag::new(
Severity::Trace,
"TG2101",
format!(
"unexpected block kind `{:?}` in terragrunt file; ignored",
block.kind
),
));
}
}
}
out
}
fn project_include(block: &RawBlock, out: &mut ParsedTerragrunt) {
let label = block
.labels
.first()
.cloned()
.unwrap_or_else(|| Arc::from(""));
let mut path_expr: Option<Expression> = None;
let mut merge_strategy_expr: Option<Expression> = None;
for (k, v) in &block.body {
match k.as_ref() {
"path" => path_expr = Some(v.clone()),
"merge_strategy" => merge_strategy_expr = Some(v.clone()),
_ => {}
}
}
out.includes.push(IncludeBlockRaw {
label,
path_expr,
merge_strategy_expr,
span: block.span.clone(),
});
}
fn project_generate(block: &RawBlock, out: &mut ParsedTerragrunt) {
let label = block
.labels
.first()
.cloned()
.unwrap_or_else(|| Arc::from(""));
out.generates.push(GenerateBlockRaw {
label,
attrs: block.body.clone(),
span: block.span.clone(),
});
}
fn project_dependency(block: &RawBlock, out: &mut ParsedTerragrunt) {
let name = block
.labels
.first()
.cloned()
.unwrap_or_else(|| Arc::from(""));
out.dependencies.push(DependencyBlockRaw {
name,
attrs: block.body.clone(),
span: block.span.clone(),
});
}
fn object_to_attribute_map(value: &Expression) -> AttributeMap {
match value {
Expression::Object(entries) => entries
.iter()
.filter_map(|(k, v)| match k {
Expression::Literal(crate::ir::Value::Str(s)) => Some((Arc::clone(s), v.clone())),
_ => None,
})
.collect(),
Expression::Literal(crate::ir::Value::Map(entries)) => entries
.iter()
.map(|(k, v)| (Arc::clone(k), Expression::Literal(v.clone())))
.collect(),
_ => Vec::new(),
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic
)]
mod tests {
use super::*;
use crate::{
ir::Span,
loader::{HclEditLoader, LoaderLimits, ParseBytesResult},
};
fn parse(text: &str) -> ParseBytesResult {
let path: Arc<Path> = Arc::from(Path::new("terragrunt.hcl"));
HclEditLoader::new().parse_bytes(text.as_bytes(), &path, &LoaderLimits::default())
}
#[test]
fn test_should_project_locals_block() {
let parsed = parse("locals { region = \"us-east-2\" }");
let path: Arc<Path> = Arc::from(Path::new("/x/terragrunt.hcl"));
let pt = project(&parsed, &path);
assert_eq!(pt.locals.len(), 1);
assert_eq!(&*pt.locals[0].name, "region");
}
#[test]
fn test_should_project_include_with_label() {
let parsed = parse("include \"root\" { path = \"x\" }");
let path: Arc<Path> = Arc::from(Path::new("/x/terragrunt.hcl"));
let pt = project(&parsed, &path);
assert_eq!(pt.includes.len(), 1);
assert_eq!(&*pt.includes[0].label, "root");
assert!(pt.includes[0].path_expr.is_some());
}
#[test]
fn test_should_project_generate_block() {
let parsed = parse(
"generate \"backend\" {\n path = \"backend.tf\"\n if_exists = \
\"overwrite_terragrunt\"\n contents = \"terraform {}\"\n}",
);
let path: Arc<Path> = Arc::from(Path::new("/x/terragrunt.hcl"));
let pt = project(&parsed, &path);
assert_eq!(pt.generates.len(), 1);
assert_eq!(&*pt.generates[0].label, "backend");
}
#[test]
fn test_should_project_dependency_block() {
let parsed = parse("dependency \"vpc\" { config_path = \"../net\" }");
let path: Arc<Path> = Arc::from(Path::new("/x/terragrunt.hcl"));
let pt = project(&parsed, &path);
assert_eq!(pt.dependencies.len(), 1);
assert_eq!(&*pt.dependencies[0].name, "vpc");
}
#[test]
fn test_should_project_inputs_attribute() {
let parsed = parse("inputs = { foo = \"bar\" }");
let path: Arc<Path> = Arc::from(Path::new("/x/terragrunt.hcl"));
let pt = project(&parsed, &path);
let inputs = pt.inputs.expect("inputs projected");
assert_eq!(inputs.len(), 1);
assert_eq!(&*inputs[0].0, "foo");
}
#[test]
fn test_span_synthetic_returns_synthetic_marker() {
let s = Span::synthetic();
assert!(s.file.as_os_str().is_empty());
}
}