1use std::{path::Path, sync::Arc};
4
5use serde::{Deserialize, Serialize};
6use typed_builder::TypedBuilder;
7
8use crate::ir::{
9 ComponentId, Expression, ModuleCall, ProviderBlock, Resource, SourceFile, Span, StateBackend,
10 TerragruntConfig,
11};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(rename_all = "kebab-case")]
21#[non_exhaustive]
22pub enum ComponentKind {
23 Component,
26 Module,
29}
30
31#[derive(Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
36#[non_exhaustive]
37#[serde(rename_all = "camelCase")]
38#[builder(field_defaults(setter(into)))]
39pub struct Variable {
40 pub name: Arc<str>,
42 #[builder(default)]
44 pub description: Option<Arc<str>>,
45 #[builder(default)]
48 pub type_expr: Option<Expression>,
49 #[builder(default)]
53 pub default: Option<Expression>,
54 #[builder(default = false)]
56 pub sensitive: bool,
57 pub span: Span,
59}
60
61impl std::fmt::Debug for Variable {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 let mut s = f.debug_struct("Variable");
64 s.field("name", &self.name)
65 .field("description", &self.description)
66 .field("type_expr", &self.type_expr.as_ref().map(|_| "<expr>"))
67 .field("sensitive", &self.sensitive);
68 if self.sensitive {
69 s.field("default", &self.default.as_ref().map(|_| "<redacted>"));
70 } else {
71 s.field("default", &self.default);
72 }
73 s.field("span", &self.span).finish()
74 }
75}
76
77#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
79#[non_exhaustive]
80#[serde(rename_all = "camelCase")]
81#[builder(field_defaults(setter(into)))]
82pub struct Local {
83 pub name: Arc<str>,
85 pub value: Expression,
87 pub span: Span,
89}
90
91#[derive(Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
96#[non_exhaustive]
97#[serde(rename_all = "camelCase")]
98#[builder(field_defaults(setter(into)))]
99pub struct Output {
100 pub name: Arc<str>,
102 pub value: Expression,
104 #[builder(default)]
106 pub description: Option<Arc<str>>,
107 #[builder(default = false)]
109 pub sensitive: bool,
110 pub span: Span,
112}
113
114impl std::fmt::Debug for Output {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 let mut s = f.debug_struct("Output");
117 s.field("name", &self.name)
118 .field("description", &self.description)
119 .field("sensitive", &self.sensitive);
120 if self.sensitive {
121 s.field("value", &"<redacted>");
122 } else {
123 s.field("value", &self.value);
124 }
125 s.field("span", &self.span).finish()
126 }
127}
128
129#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
136#[non_exhaustive]
137#[serde(rename_all = "camelCase")]
138#[builder(field_defaults(setter(into)))]
139pub struct Component {
140 pub id: ComponentId,
142
143 #[serde(with = "crate::ir::path_serde::arc_path")]
145 pub path: Arc<Path>,
146
147 pub kind: ComponentKind,
149
150 #[builder(default)]
153 pub files: Vec<SourceFile>,
154
155 #[builder(default)]
157 pub variables: Vec<Variable>,
158
159 #[builder(default)]
161 pub locals: Vec<Local>,
162
163 #[builder(default)]
165 pub providers: Vec<ProviderBlock>,
166
167 #[builder(default)]
170 pub resources: Vec<Resource>,
171
172 #[builder(default)]
178 pub modules: Vec<ModuleCall>,
179
180 #[builder(default)]
182 pub outputs: Vec<Output>,
183
184 #[builder(default)]
186 pub terragrunt: Option<TerragruntConfig>,
187
188 #[builder(default)]
191 pub state_backend: Option<StateBackend>,
192}
193
194#[cfg(test)]
195#[allow(
196 clippy::unwrap_used,
197 clippy::expect_used,
198 clippy::panic,
199 clippy::indexing_slicing
200)]
201mod tests {
202 use std::path::PathBuf;
203
204 use super::*;
205 use crate::ir::{ComponentId, Value};
206
207 #[test]
208 fn test_should_build_minimal_component() {
209 let c = Component::builder()
210 .id(ComponentId::from_index(0))
211 .path(Arc::<Path>::from(PathBuf::from("services/api-gateway")))
212 .kind(ComponentKind::Component)
213 .build();
214 assert_eq!(c.kind, ComponentKind::Component);
215 assert!(c.files.is_empty());
216 assert!(c.resources.is_empty());
217 assert!(c.terragrunt.is_none());
218 }
219
220 #[test]
221 fn test_should_redact_sensitive_variable_default_in_debug() {
222 let v = Variable::builder()
223 .name(Arc::<str>::from("db_password"))
224 .default(Some(Expression::Literal(Value::Str(Arc::<str>::from(
225 "very-secret",
226 )))))
227 .sensitive(true)
228 .span(Span::synthetic())
229 .build();
230 let debug = format!("{v:?}");
231 assert!(!debug.contains("very-secret"), "{debug}");
232 assert!(debug.contains("redacted"), "{debug}");
233 }
234
235 #[test]
236 fn test_should_not_redact_non_sensitive_variable_default() {
237 let v = Variable::builder()
238 .name(Arc::<str>::from("region"))
239 .default(Some(Expression::Literal(Value::Str(Arc::<str>::from(
240 "us-west-2",
241 )))))
242 .span(Span::synthetic())
243 .build();
244 let debug = format!("{v:?}");
245 assert!(debug.contains("us-west-2"), "{debug}");
246 }
247
248 #[test]
249 fn test_should_redact_sensitive_output_value_in_debug() {
250 let o = Output::builder()
251 .name(Arc::<str>::from("db_endpoint"))
252 .value(Expression::Literal(Value::Str(Arc::<str>::from(
253 "very-secret-endpoint",
254 ))))
255 .sensitive(true)
256 .span(Span::synthetic())
257 .build();
258 let debug = format!("{o:?}");
259 assert!(!debug.contains("very-secret-endpoint"), "{debug}");
260 assert!(debug.contains("redacted"), "{debug}");
261 }
262
263 #[test]
264 fn test_should_round_trip_local_via_serde() {
265 let l = Local::builder()
266 .name(Arc::<str>::from("name_prefix"))
267 .value(Expression::Literal(Value::Str(Arc::<str>::from(
268 "northwind",
269 ))))
270 .span(Span::synthetic())
271 .build();
272 let json = serde_json::to_string(&l).unwrap();
273 let back: Local = serde_json::from_str(&json).unwrap();
274 assert_eq!(l, back);
275 }
276}