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}