use std::{path::Path, sync::Arc};
use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;
use crate::ir::{
ComponentId, Expression, ModuleCall, ProviderBlock, Resource, SourceFile, Span, StateBackend,
TerragruntConfig,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum ComponentKind {
Component,
Module,
}
#[derive(Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
#[builder(field_defaults(setter(into)))]
pub struct Variable {
pub name: Arc<str>,
#[builder(default)]
pub description: Option<Arc<str>>,
#[builder(default)]
pub type_expr: Option<Expression>,
#[builder(default)]
pub default: Option<Expression>,
#[builder(default = false)]
pub sensitive: bool,
pub span: Span,
}
impl std::fmt::Debug for Variable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("Variable");
s.field("name", &self.name)
.field("description", &self.description)
.field("type_expr", &self.type_expr.as_ref().map(|_| "<expr>"))
.field("sensitive", &self.sensitive);
if self.sensitive {
s.field("default", &self.default.as_ref().map(|_| "<redacted>"));
} else {
s.field("default", &self.default);
}
s.field("span", &self.span).finish()
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
#[builder(field_defaults(setter(into)))]
pub struct Local {
pub name: Arc<str>,
pub value: Expression,
pub span: Span,
}
#[derive(Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
#[builder(field_defaults(setter(into)))]
pub struct Output {
pub name: Arc<str>,
pub value: Expression,
#[builder(default)]
pub description: Option<Arc<str>>,
#[builder(default = false)]
pub sensitive: bool,
pub span: Span,
}
impl std::fmt::Debug for Output {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("Output");
s.field("name", &self.name)
.field("description", &self.description)
.field("sensitive", &self.sensitive);
if self.sensitive {
s.field("value", &"<redacted>");
} else {
s.field("value", &self.value);
}
s.field("span", &self.span).finish()
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
#[builder(field_defaults(setter(into)))]
pub struct Component {
pub id: ComponentId,
#[serde(with = "crate::ir::path_serde::arc_path")]
pub path: Arc<Path>,
pub kind: ComponentKind,
#[builder(default)]
pub files: Vec<SourceFile>,
#[builder(default)]
pub variables: Vec<Variable>,
#[builder(default)]
pub locals: Vec<Local>,
#[builder(default)]
pub providers: Vec<ProviderBlock>,
#[builder(default)]
pub resources: Vec<Resource>,
#[builder(default)]
pub modules: Vec<ModuleCall>,
#[builder(default)]
pub outputs: Vec<Output>,
#[builder(default)]
pub terragrunt: Option<TerragruntConfig>,
#[builder(default)]
pub state_backend: Option<StateBackend>,
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use std::path::PathBuf;
use super::*;
use crate::ir::{ComponentId, Value};
#[test]
fn test_should_build_minimal_component() {
let c = Component::builder()
.id(ComponentId::from_index(0))
.path(Arc::<Path>::from(PathBuf::from("services/api-gateway")))
.kind(ComponentKind::Component)
.build();
assert_eq!(c.kind, ComponentKind::Component);
assert!(c.files.is_empty());
assert!(c.resources.is_empty());
assert!(c.terragrunt.is_none());
}
#[test]
fn test_should_redact_sensitive_variable_default_in_debug() {
let v = Variable::builder()
.name(Arc::<str>::from("db_password"))
.default(Some(Expression::Literal(Value::Str(Arc::<str>::from(
"very-secret",
)))))
.sensitive(true)
.span(Span::synthetic())
.build();
let debug = format!("{v:?}");
assert!(!debug.contains("very-secret"), "{debug}");
assert!(debug.contains("redacted"), "{debug}");
}
#[test]
fn test_should_not_redact_non_sensitive_variable_default() {
let v = Variable::builder()
.name(Arc::<str>::from("region"))
.default(Some(Expression::Literal(Value::Str(Arc::<str>::from(
"us-west-2",
)))))
.span(Span::synthetic())
.build();
let debug = format!("{v:?}");
assert!(debug.contains("us-west-2"), "{debug}");
}
#[test]
fn test_should_redact_sensitive_output_value_in_debug() {
let o = Output::builder()
.name(Arc::<str>::from("db_endpoint"))
.value(Expression::Literal(Value::Str(Arc::<str>::from(
"very-secret-endpoint",
))))
.sensitive(true)
.span(Span::synthetic())
.build();
let debug = format!("{o:?}");
assert!(!debug.contains("very-secret-endpoint"), "{debug}");
assert!(debug.contains("redacted"), "{debug}");
}
#[test]
fn test_should_round_trip_local_via_serde() {
let l = Local::builder()
.name(Arc::<str>::from("name_prefix"))
.value(Expression::Literal(Value::Str(Arc::<str>::from(
"northwind",
))))
.span(Span::synthetic())
.build();
let json = serde_json::to_string(&l).unwrap();
let back: Local = serde_json::from_str(&json).unwrap();
assert_eq!(l, back);
}
}