Skip to main content

tfparser_core/ir/
resource.rs

1//! Resource / data-source IR nodes — the unit of analysis the exporter
2//! emits one row per.
3
4use std::sync::Arc;
5
6use serde::{Deserialize, Serialize};
7use typed_builder::TypedBuilder;
8
9use crate::ir::{AccountId, Address, AttributeMap, Expression, ProviderRef, Region, Span};
10
11/// Whether an IR node was declared as `resource` or `data`.
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "kebab-case")]
14#[non_exhaustive]
15pub enum ResourceKind {
16    /// `resource "type" "name" {}` — a managed resource.
17    Managed,
18    /// `data "type" "name" {}` — a data source.
19    Data,
20}
21
22/// Top-level HCL block kinds the loader distinguishes.
23///
24/// Mirrors [12-hcl-loader.md § 2]'s `BlockKind`.
25///
26/// [12-hcl-loader.md § 2]: ../../specs/12-hcl-loader.md
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "kebab-case")]
29#[non_exhaustive]
30pub enum BlockKind {
31    /// `resource "type" "name" {}`
32    Resource,
33    /// `data "type" "name" {}`
34    Data,
35    /// `module "name" { source = ... }`
36    Module,
37    /// `provider "name" { alias = ..., ... }`
38    Provider,
39    /// `variable "name" {}`
40    Variable,
41    /// `locals { ... }`
42    Locals,
43    /// `output "name" {}`
44    Output,
45    /// `terraform { ... }`
46    Terraform,
47    /// `include "label" { ... }` (Terragrunt)
48    Include,
49    /// `generate "label" { ... }` (Terragrunt)
50    Generate,
51    /// `dependency "label" { ... }` (Terragrunt)
52    Dependency,
53    /// `inputs { ... }` (Terragrunt)
54    Inputs,
55    /// Anything else (e.g. user-defined `dynamic` block).
56    Unknown,
57}
58
59/// A `resource` or `data` block, post-evaluator. Phase 1 only defines the
60/// shape; population happens in Phase 3 (loader → IR) and onwards.
61///
62/// Field order matches [10-data-model.md § 2.2]. Build with the generated
63/// [`ResourceBuilder`] (typed-builder) — `Resource { … }` is rejected
64/// outside this crate by `#[non_exhaustive]`.
65///
66/// [10-data-model.md § 2.2]: ../../specs/10-data-model.md
67#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
68#[non_exhaustive]
69#[serde(rename_all = "camelCase")]
70#[builder(field_defaults(setter(into)))]
71pub struct Resource {
72    /// Full TF address (e.g. `module.pacer_db.aws_db_instance.this`).
73    pub address: Address,
74
75    /// `resource` vs `data`.
76    pub kind: ResourceKind,
77
78    /// Block label 1 (e.g. `aws_db_instance`).
79    pub type_: Arc<str>,
80
81    /// Block label 2 (e.g. `this`).
82    pub name: Arc<str>,
83
84    /// Per-resource provider selection (`provider = aws.<alias>` attribute).
85    #[builder(default)]
86    pub provider_ref: Option<ProviderRef>,
87
88    /// `count = ...` expression.
89    #[builder(default)]
90    pub count_expr: Option<Expression>,
91
92    /// `for_each = ...` expression.
93    #[builder(default)]
94    pub for_each_expr: Option<Expression>,
95
96    /// Explicit + inferred dependencies (graph phase fills inferred).
97    #[builder(default)]
98    pub depends_on: Vec<Address>,
99
100    /// Top-level attributes of the body. Nested blocks land here as
101    /// [`crate::ir::Value::Map`] structures inside an
102    /// [`Expression::Literal`] once the loader has lowered them.
103    #[builder(default)]
104    pub attributes: AttributeMap,
105
106    /// Resolved AWS account id (12 digits). Filled by the provider resolver
107    /// (Phase 7) per [16-provider-resolver.md § 4]. `None` until resolution
108    /// runs or when no profile mapping could be inferred (the column then
109    /// emits `""` per spec 10 § 3).
110    ///
111    /// [16-provider-resolver.md § 4]: ../../specs/16-provider-resolver.md
112    #[builder(default)]
113    pub account_id: Option<AccountId>,
114
115    /// Human-friendly account name, from the profile-map entry the resolver
116    /// matched. `None` when unresolved or absent.
117    #[builder(default)]
118    pub account_name: Option<Arc<str>>,
119
120    /// Resolved AWS region. Filled by the provider resolver (Phase 7) per
121    /// [16-provider-resolver.md § 4]. `None` until resolution runs.
122    #[builder(default)]
123    pub region: Option<Region>,
124
125    /// Source position of the opening `resource`/`data` keyword.
126    pub span: Span,
127}
128
129#[cfg(test)]
130#[allow(clippy::unwrap_used)]
131mod tests {
132    use std::sync::Arc;
133
134    use super::*;
135    use crate::ir::{Address, Span};
136
137    #[test]
138    fn test_should_build_minimal_resource() {
139        let r = Resource::builder()
140            .address(Address::new("aws_db_instance.x").unwrap())
141            .kind(ResourceKind::Managed)
142            .type_(Arc::<str>::from("aws_db_instance"))
143            .name(Arc::<str>::from("x"))
144            .span(Span::synthetic())
145            .build();
146        assert_eq!(r.address.as_str(), "aws_db_instance.x");
147        assert!(r.depends_on.is_empty());
148        assert!(r.attributes.is_empty());
149        assert!(r.provider_ref.is_none());
150    }
151
152    #[test]
153    fn test_should_serde_round_trip_resource() {
154        let r = Resource::builder()
155            .address(Address::new("aws_iam_role.r").unwrap())
156            .kind(ResourceKind::Managed)
157            .type_(Arc::<str>::from("aws_iam_role"))
158            .name(Arc::<str>::from("r"))
159            .span(Span::synthetic())
160            .build();
161        let json = serde_json::to_string(&r).unwrap();
162        let back: Resource = serde_json::from_str(&json).unwrap();
163        assert_eq!(r, back);
164    }
165}