Skip to main content

tfparser_core/graph/
registry.rs

1//! Module registry — index of [`EvaluatedComponent`] bodies keyed by their
2//! canonical directory path.
3//!
4//! Per [15-resource-graph.md § 2], the graph phase consumes
5//! `Vec<EvaluatedComponent>` (per-component evaluator output) plus a
6//! [`ModuleRegistry`] that resolves `ModuleCall.source` to a walked module
7//! body. The orchestrator (Phase 5 pipeline wiring) builds the registry by
8//! evaluating every module dir the discovery phase classified as
9//! `DirKind::Module` and keying it by `canonical_path`.
10//!
11//! Non-local sources (Registry, Git, External) cannot be walked source-only;
12//! they live in [`ExternalModuleRef`] so the dependency-graph phase can still
13//! emit a row in `modules.parquet`.
14//!
15//! [15-resource-graph.md § 2]: ../../../specs/15-resource-graph.md
16
17use std::{collections::HashMap, path::Path, sync::Arc};
18
19use crate::{
20    eval::EvaluatedComponent,
21    ir::{ModuleSource, Span},
22};
23
24/// An external module reference captured for the dependency graph but **not**
25/// walked source-only (Registry, Git, or generic External source).
26#[derive(Clone, Debug)]
27#[non_exhaustive]
28pub struct ExternalModuleRef {
29    /// Verbatim `source = "..."` value.
30    pub source_raw: Arc<str>,
31    /// Classified source.
32    pub source: ModuleSource,
33    /// Call site span (one of the call sites; the registry de-dupes by
34    /// `source_raw`).
35    pub first_seen: Span,
36}
37
38/// Map of local module dirs (canonical paths) to their evaluator output.
39///
40/// The graph phase looks up `ModuleCall.source` after canonicalising it
41/// relative to the calling component's dir; a hit drops the module's
42/// `EvaluatedComponent` into the expansion pipeline.
43///
44/// External (non-local) sources land in `external_refs` so the
45/// `modules.parquet` writer (Phase 8) can still emit a row for them.
46#[derive(Clone, Debug, Default)]
47#[non_exhaustive]
48pub struct ModuleRegistry {
49    /// Canonical-path keyed map. The path is **absolute** and canonical so
50    /// `..`-laden sources from different call sites resolve to the same
51    /// entry.
52    pub local_modules: HashMap<Arc<Path>, EvaluatedComponent>,
53    /// External / unwalked references, de-duplicated by `source_raw`.
54    pub external_refs: Vec<ExternalModuleRef>,
55}
56
57impl ModuleRegistry {
58    /// Construct an empty registry.
59    #[must_use]
60    pub fn new() -> Self {
61        Self::default()
62    }
63
64    /// Insert a local module body keyed by its canonical absolute path.
65    /// Idempotent — re-inserting the same path replaces the value (last
66    /// writer wins; orchestrator guarantees uniqueness).
67    pub fn insert_local(&mut self, canonical: Arc<Path>, component: EvaluatedComponent) {
68        self.local_modules.insert(canonical, component);
69    }
70
71    /// Record an external reference if not already present (key:
72    /// `source_raw`).
73    pub fn record_external(&mut self, source_raw: Arc<str>, source: ModuleSource, span: Span) {
74        if self
75            .external_refs
76            .iter()
77            .any(|e| e.source_raw == source_raw)
78        {
79            return;
80        }
81        self.external_refs.push(ExternalModuleRef {
82            source_raw,
83            source,
84            first_seen: span,
85        });
86    }
87
88    /// Look up a local module body by canonical path.
89    #[must_use]
90    pub fn get_local(&self, canonical: &Path) -> Option<&EvaluatedComponent> {
91        self.local_modules.get(canonical)
92    }
93
94    /// Count of local modules in the registry.
95    #[must_use]
96    pub fn local_count(&self) -> usize {
97        self.local_modules.len()
98    }
99
100    /// Count of external references.
101    #[must_use]
102    pub fn external_count(&self) -> usize {
103        self.external_refs.len()
104    }
105}
106
107#[cfg(test)]
108#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
109mod tests {
110    use std::path::PathBuf;
111
112    use super::*;
113    use crate::{
114        eval::EvaluatedComponent,
115        ir::{Component, ComponentId, ComponentKind, Span},
116    };
117
118    fn evaluated_component(path: &str) -> EvaluatedComponent {
119        EvaluatedComponent {
120            raw: Arc::new(
121                Component::builder()
122                    .id(ComponentId::from_index(0))
123                    .path(Arc::<Path>::from(PathBuf::from(path)))
124                    .kind(ComponentKind::Module)
125                    .build(),
126            ),
127            variables: Vec::new(),
128            locals: Vec::new(),
129            providers: Vec::new(),
130            resources: Vec::new(),
131            modules: Vec::new(),
132            outputs: Vec::new(),
133            diagnostics: Vec::new(),
134        }
135    }
136
137    #[test]
138    fn test_should_insert_and_lookup_local_module() {
139        let mut reg = ModuleRegistry::new();
140        let canonical: Arc<Path> = Arc::from(PathBuf::from("/repo/modules/s3"));
141        reg.insert_local(Arc::clone(&canonical), evaluated_component("modules/s3"));
142        assert_eq!(reg.local_count(), 1);
143        assert!(reg.get_local(&canonical).is_some());
144    }
145
146    #[test]
147    fn test_should_dedup_external_refs_by_source_raw() {
148        let mut reg = ModuleRegistry::new();
149        let span = Span::synthetic();
150        reg.record_external(
151            Arc::<str>::from("terraform-aws-modules/eks/aws"),
152            ModuleSource::Registry(Arc::<str>::from("terraform-aws-modules/eks/aws")),
153            span.clone(),
154        );
155        reg.record_external(
156            Arc::<str>::from("terraform-aws-modules/eks/aws"),
157            ModuleSource::Registry(Arc::<str>::from("terraform-aws-modules/eks/aws")),
158            span,
159        );
160        assert_eq!(reg.external_count(), 1);
161    }
162}