Skip to main content

tfparser_core/ir/
edge.rs

1//! Inter-resource and component-to-component dependency edges.
2//!
3//! Phase 8 (M5) introduces the dependency-graph projection. An [`Edge`]
4//! links a [`Resource`](crate::ir::Resource) (or [`Component`](crate::ir::Component))
5//! address to the address it references — either explicitly via
6//! `depends_on`, implicitly via a symbolic attribute reference, or across
7//! components via a Terragrunt `dependency` block.
8//!
9//! Per [10-data-model.md § 5.1] (the `dependencies.parquet` schema) and
10//! [15-resource-graph.md § 4].
11//!
12//! [10-data-model.md § 5.1]: ../../specs/10-data-model.md
13//! [15-resource-graph.md § 4]: ../../specs/15-resource-graph.md
14
15use std::sync::Arc;
16
17use serde::{Deserialize, Serialize};
18use typed_builder::TypedBuilder;
19
20use crate::ir::{Address, Span};
21
22/// How an edge was discovered.
23///
24/// `#[non_exhaustive]` per CLAUDE.md § Type Design — a future
25/// `EdgeKind::LifecycleReference` lands additively without breaking the
26/// secondary-table schema.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29#[non_exhaustive]
30pub enum EdgeKind {
31    /// `depends_on = [aws_x.y]` — explicit dependency declared in source.
32    ExplicitDependsOn,
33    /// An attribute body referenced the target (e.g. `policy = aws_iam_policy.p.arn`).
34    AttrRef,
35    /// Module call input contained a reference to a sibling resource / module.
36    ModuleInput,
37    /// Terragrunt `dependency "x" { config_path = "..." }` — points at a
38    /// **component**, not a resource address.
39    TerragruntDependency,
40}
41
42impl EdgeKind {
43    /// Stable string discriminator used for the `edge_kind` Parquet column.
44    /// Spec 10 § 5.1 pins the exact tokens.
45    #[must_use]
46    pub const fn as_str(self) -> &'static str {
47        match self {
48            Self::ExplicitDependsOn => "explicit_depends_on",
49            Self::AttrRef => "attr_ref",
50            Self::ModuleInput => "module_input",
51            Self::TerragruntDependency => "terragrunt_dependency",
52        }
53    }
54}
55
56/// One dependency edge.
57///
58/// Field order matches [10-data-model.md § 5.1]. Build via the generated
59/// [`EdgeBuilder`].
60#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TypedBuilder)]
61#[non_exhaustive]
62#[serde(rename_all = "camelCase")]
63#[builder(field_defaults(setter(into)))]
64pub struct Edge {
65    /// Address holding the reference (the "from" / source of the arrow).
66    pub from: Address,
67    /// Address being referenced.
68    pub to: Address,
69    /// How the edge was discovered.
70    pub kind: EdgeKind,
71    /// Attribute path that introduced the edge (e.g. `"policy"`,
72    /// `"subnets[0].id"`). `None` for explicit `depends_on` edges and
73    /// Terragrunt dependencies.
74    #[builder(default)]
75    pub attr: Option<Arc<str>>,
76    /// Where the reference appears in source.
77    pub span: Span,
78}
79
80#[cfg(test)]
81#[allow(clippy::unwrap_used)]
82mod tests {
83    use std::sync::Arc;
84
85    use super::*;
86    use crate::ir::Span;
87
88    #[test]
89    fn test_edge_kind_string_form_is_stable() {
90        assert_eq!(EdgeKind::ExplicitDependsOn.as_str(), "explicit_depends_on");
91        assert_eq!(EdgeKind::AttrRef.as_str(), "attr_ref");
92        assert_eq!(EdgeKind::ModuleInput.as_str(), "module_input");
93        assert_eq!(
94            EdgeKind::TerragruntDependency.as_str(),
95            "terragrunt_dependency"
96        );
97    }
98
99    #[test]
100    fn test_should_build_minimal_edge() {
101        let e = Edge::builder()
102            .from(Address::new("aws_iam_role.r").unwrap())
103            .to(Address::new("aws_iam_policy.p").unwrap())
104            .kind(EdgeKind::AttrRef)
105            .attr(Some(Arc::<str>::from("policy")))
106            .span(Span::synthetic())
107            .build();
108        assert_eq!(e.from.as_str(), "aws_iam_role.r");
109        assert_eq!(e.kind, EdgeKind::AttrRef);
110    }
111
112    #[test]
113    fn test_should_serde_round_trip_edge() {
114        let e = Edge::builder()
115            .from(Address::new("aws_iam_role.r").unwrap())
116            .to(Address::new("aws_iam_policy.p").unwrap())
117            .kind(EdgeKind::AttrRef)
118            .span(Span::synthetic())
119            .build();
120        let json = serde_json::to_string(&e).unwrap();
121        let back: Edge = serde_json::from_str(&json).unwrap();
122        assert_eq!(e, back);
123    }
124}