Skip to main content

tfparser_core/ir/
terragrunt.rs

1//! Terragrunt configuration attached to a [`Component`](crate::ir::Component).
2//!
3//! Phase 1 only defines the shape; population happens in Phase 6 per
4//! [14-terragrunt.md](../../specs/14-terragrunt.md). The diagnostic field is
5//! intentionally a flat `Vec` (not the workspace-level [`Diagnostic`]
6//! `Vec`) — Terragrunt-resolution diagnostics are reported per component.
7
8use std::{path::Path, sync::Arc};
9
10use serde::{Deserialize, Serialize};
11use typed_builder::TypedBuilder;
12
13use crate::{
14    diagnostic::Diagnostic,
15    ir::{AccountId, AttributeMap, Map, Region, Span},
16};
17
18/// One entry on the include load chain (deepest last).
19#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TypedBuilder)]
20#[non_exhaustive]
21#[serde(rename_all = "camelCase")]
22#[builder(field_defaults(setter(into)))]
23pub struct IncludePath {
24    /// Resolved absolute path of the included Terragrunt file.
25    #[serde(with = "crate::ir::path_serde::arc_path")]
26    pub path: Arc<Path>,
27    /// Optional `name` label on the `include` block.
28    #[builder(default)]
29    pub label: Option<Arc<str>>,
30    /// Span of the `include` block in the consumer file.
31    pub span: Span,
32}
33
34/// `generate "label" { ... }` block, captured verbatim. The parser **does
35/// not** write the file; consumers can synthesize it if needed.
36#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
37#[non_exhaustive]
38#[serde(rename_all = "camelCase")]
39#[builder(field_defaults(setter(into)))]
40pub struct GenerateBlock {
41    /// Label on the `generate` block (e.g. `"backend"`).
42    pub label: Arc<str>,
43    /// Target path that would be written (relative to the component dir).
44    #[serde(with = "crate::ir::path_serde::arc_path")]
45    pub path: Arc<Path>,
46    /// `if_exists` policy as declared (e.g. `"overwrite_terragrunt"`).
47    pub if_exists: Arc<str>,
48    /// Verbatim contents.
49    pub contents: Arc<str>,
50    /// Span of the `generate` keyword.
51    pub span: Span,
52}
53
54/// `dependency "label" { config_path = "..."; mock_outputs = ... }` block.
55#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
56#[non_exhaustive]
57#[serde(rename_all = "camelCase")]
58#[builder(field_defaults(setter(into)))]
59pub struct DependencyBlock {
60    /// Dependency name (e.g. `"vpc"`).
61    pub name: Arc<str>,
62    /// Resolved absolute path of the dependency component dir.
63    #[serde(with = "crate::ir::path_serde::arc_path")]
64    pub config_path: Arc<Path>,
65    /// Optional `mock_outputs = { ... }` attribute body, kept verbatim.
66    #[builder(default)]
67    pub mock_outputs: AttributeMap,
68    /// Span of the `dependency` block.
69    pub span: Span,
70}
71
72/// Terraform state backend description.
73///
74/// Source can be `terraform { backend "s3" {} }` declared in the component
75/// directly, or extracted from a `generate "backend"` block's `contents`.
76/// `state_account_id` is derived later from `profile` / `role_arn` by the
77/// provider resolver.
78#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
79#[non_exhaustive]
80#[serde(rename_all = "camelCase")]
81#[builder(field_defaults(setter(into)))]
82pub struct StateBackend {
83    /// Backend kind (e.g. `"s3"`, `"local"`, `"remote"`).
84    pub kind: Arc<str>,
85    /// Verbatim attribute body of the backend block.
86    pub attributes: AttributeMap,
87    /// Account id derived from the backend profile / `role_arn`, if any.
88    #[builder(default)]
89    pub state_account_id: Option<AccountId>,
90    /// Region declared in the backend block, if any.
91    #[builder(default)]
92    pub state_region: Option<Region>,
93    /// Span of the backend block.
94    pub span: Span,
95}
96
97/// Per-component Terragrunt configuration.
98#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
99#[non_exhaustive]
100#[serde(rename_all = "camelCase")]
101#[builder(field_defaults(setter(into)))]
102pub struct TerragruntConfig {
103    /// Component directory (absolute path).
104    #[serde(with = "crate::ir::path_serde::arc_path")]
105    pub component_dir: Arc<Path>,
106
107    /// Locals post-cascade merge. Drives the evaluator context for the
108    /// component's `.tf` files.
109    #[builder(default)]
110    pub effective_locals: Map,
111
112    /// Resolved `inputs { ... }` from the component's terragrunt.hcl.
113    #[builder(default)]
114    pub inputs: Map,
115
116    /// Include chain (deepest last).
117    #[builder(default)]
118    pub includes: Vec<IncludePath>,
119
120    /// `generate "label" { ... }` blocks captured for downstream consumers.
121    #[builder(default)]
122    pub generates: Vec<GenerateBlock>,
123
124    /// `dependency "label" { ... }` blocks.
125    #[builder(default)]
126    pub dependencies: Vec<DependencyBlock>,
127
128    /// State backend extracted from `terraform { backend ... }` or
129    /// `generate "backend"`.
130    #[builder(default)]
131    pub state_backend: Option<StateBackend>,
132
133    /// Non-fatal diagnostics emitted during resolution.
134    #[builder(default)]
135    pub diagnostics: Vec<Diagnostic>,
136}
137
138#[cfg(test)]
139#[allow(clippy::unwrap_used)]
140mod tests {
141    use std::path::PathBuf;
142
143    use super::*;
144    use crate::ir::Span;
145
146    #[test]
147    fn test_should_build_minimal_terragrunt_config() {
148        let cfg = TerragruntConfig::builder()
149            .component_dir(Arc::<Path>::from(PathBuf::from("/repo/services/x")))
150            .build();
151        assert!(cfg.effective_locals.is_empty());
152        assert!(cfg.includes.is_empty());
153    }
154
155    #[test]
156    fn test_should_serde_round_trip_generate_block() {
157        let g = GenerateBlock {
158            label: Arc::<str>::from("backend"),
159            path: Arc::<Path>::from(PathBuf::from("generated_backend.tf")),
160            if_exists: Arc::<str>::from("overwrite_terragrunt"),
161            contents: Arc::<str>::from("terraform { backend \"s3\" {} }"),
162            span: Span::synthetic(),
163        };
164        let json = serde_json::to_string(&g).unwrap();
165        let back: GenerateBlock = serde_json::from_str(&json).unwrap();
166        assert_eq!(g, back);
167    }
168}