tfparser_core/ir/
module.rs1use std::{path::Path, sync::Arc};
4
5use serde::{Deserialize, Serialize};
6use typed_builder::TypedBuilder;
7
8use crate::ir::{Address, AttributeMap, Component, Expression, ModuleId, ProviderRef, Span};
9
10#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[non_exhaustive]
17#[serde(rename_all = "camelCase", tag = "kind", content = "value")]
18pub enum ModuleSource {
19 Local(Arc<str>),
21
22 Registry(Arc<str>),
24
25 Git(Arc<str>),
27
28 External(Arc<str>),
30}
31
32impl ModuleSource {
33 #[must_use]
35 pub fn classify(raw: &str) -> Self {
36 let raw_arc: Arc<str> = Arc::from(raw);
37 if raw.starts_with("./") || raw.starts_with("../") {
38 Self::Local(raw_arc)
39 } else if raw.starts_with("git::")
40 || raw.starts_with("git@")
41 || raw.contains(".git")
42 || raw.starts_with("github.com/")
43 {
44 Self::Git(raw_arc)
45 } else if raw.contains('/') && !raw.starts_with('/') && raw.matches('/').count() >= 2 {
46 Self::Registry(raw_arc)
48 } else {
49 Self::External(raw_arc)
50 }
51 }
52}
53
54#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
58#[non_exhaustive]
59#[serde(rename_all = "camelCase")]
60#[builder(field_defaults(setter(into)))]
61pub struct ModuleCall {
62 pub address: Address,
64
65 pub source_raw: Arc<str>,
67
68 pub source: ModuleSource,
70
71 #[builder(default)]
74 pub resolved: Option<ModuleId>,
75
76 #[builder(default)]
78 pub providers: Vec<(Arc<str>, ProviderRef)>,
79
80 #[builder(default)]
82 pub inputs: AttributeMap,
83
84 #[builder(default)]
86 pub count_expr: Option<Expression>,
87
88 #[builder(default)]
90 pub for_each_expr: Option<Expression>,
91
92 pub span: Span,
94}
95
96#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
101#[non_exhaustive]
102#[serde(rename_all = "camelCase")]
103#[builder(field_defaults(setter(into)))]
104pub struct Module {
105 pub id: ModuleId,
107
108 pub source: ModuleSource,
110
111 #[builder(default)]
113 #[serde(with = "crate::ir::path_serde::arc_path_opt")]
114 pub canonical_path: Option<Arc<Path>>,
115
116 pub component: Component,
119}
120
121#[cfg(test)]
122#[allow(clippy::unwrap_used)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_should_classify_local_source() {
128 assert!(matches!(
129 ModuleSource::classify("./foo"),
130 ModuleSource::Local(_)
131 ));
132 assert!(matches!(
133 ModuleSource::classify("../../modules/rds"),
134 ModuleSource::Local(_)
135 ));
136 }
137
138 #[test]
139 fn test_should_classify_git_source() {
140 for s in [
141 "git::https://github.com/x/y.git",
142 "github.com/x/y",
143 "git@github.com:x/y.git",
144 ] {
145 assert!(
146 matches!(ModuleSource::classify(s), ModuleSource::Git(_)),
147 "expected Git classification for {s}"
148 );
149 }
150 }
151
152 #[test]
153 fn test_should_classify_registry_source() {
154 assert!(matches!(
155 ModuleSource::classify("terraform-aws-modules/eks/aws"),
156 ModuleSource::Registry(_)
157 ));
158 }
159
160 #[test]
161 fn test_should_round_trip_module_source_via_serde() {
162 let s = ModuleSource::Local(Arc::<str>::from("./foo"));
163 let json = serde_json::to_string(&s).unwrap();
164 let back: ModuleSource = serde_json::from_str(&json).unwrap();
165 assert_eq!(s, back);
166 }
167
168 #[test]
169 fn test_should_classify_absolute_unix_path_as_external() {
170 assert!(matches!(
174 ModuleSource::classify("/abs/path"),
175 ModuleSource::External(_)
176 ));
177 }
178
179 #[test]
180 fn test_should_classify_bare_name_as_external() {
181 assert!(matches!(
182 ModuleSource::classify("just_a_name"),
183 ModuleSource::External(_)
184 ));
185 }
186
187 #[test]
188 fn test_should_classify_single_slash_as_external_not_registry() {
189 assert!(matches!(
192 ModuleSource::classify("foo/bar"),
193 ModuleSource::External(_)
194 ));
195 }
196}