1use hcl::ObjectKey;
2use std::{
3 collections::HashMap,
4 error, fs, io,
5 path::{Path, PathBuf},
6};
7use thiserror::Error;
8
9type Result<T> = std::result::Result<T, Error>;
10
11#[derive(Debug, Default)]
12pub struct Module {
13 pub path: PathBuf,
14 pub required_core: Vec<String>,
15 pub required_providers: HashMap<String, ProviderRequirement>,
16}
17
18impl Module {
19 pub fn new(path: PathBuf) -> Self {
20 Self {
21 path,
22 ..Default::default()
23 }
24 }
25}
26
27#[derive(Debug, Default)]
28pub struct ProviderRequirement {
29 pub source: String,
30 pub version_constraints: Vec<String>,
31 pub configuration_aliases: Vec<ProviderRef>,
32}
33
34impl ProviderRequirement {
35 pub fn new(source: String, version_constraints: Vec<String>) -> Self {
36 Self {
37 source,
38 version_constraints,
39 configuration_aliases: vec![],
40 }
41 }
42}
43
44#[derive(Debug, Default, Clone)]
45pub struct ProviderRef {
46 pub name: String,
47 pub alias: String,
48}
49
50impl ProviderRef {
51 pub fn new(name: String, alias: String) -> Self {
52 Self { name, alias }
53 }
54}
55
56#[derive(Error, Debug)]
57pub enum Error {
58 #[error(transparent)]
59 Io(#[from] io::Error),
60 #[error(transparent)]
61 Other(#[from] Box<dyn error::Error + Sync + Send>),
62 #[error(transparent)]
63 Parse(#[from] hcl::Error),
64 #[error("unexpected expression for attribute {attribute_key:?} in {file_name}: {expr:?}")]
65 UnexpectedExpr {
66 attribute_key: String,
67 expr: hcl::Expression,
68 file_name: PathBuf,
69 },
70}
71
72pub fn load_module(path: &Path, strict: bool) -> Result<Module> {
79 let mut module = Module::new(path.to_path_buf());
80
81 let files = get_files_in_dir(path)?;
82
83 for file_name in files {
84 match fs::read_to_string(&file_name) {
85 Ok(file_contents) => {
86 let file = match hcl::parse(&file_contents) {
87 Ok(body) => body,
88 Err(e) => match e {
89 hcl::Error::Parse(e) => {
90 if strict {
91 return Err(Error::Parse(hcl::Error::Parse(e)));
92 } else {
93 continue;
94 }
95 }
96 _ => return Err(Error::Other(Box::new(e))),
97 },
98 };
99
100 load_module_from_file(&file_name, file, &mut module)?;
101 }
102 Err(e) => {
103 if strict {
104 return Err(Error::Io(e));
105 }
106 }
107 }
108 }
109
110 Ok(module)
111}
112
113pub fn load_module_from_file(
115 current_file: &Path,
116 file: hcl::Body,
117 module: &mut Module,
118) -> Result<()> {
119 for block in file.blocks() {
120 let body = block.body();
121
122 #[allow(clippy::all)]
123 match block.identifier() {
124 "terraform" => handle_terraform_block(current_file, body, module)?,
125 _ => (),
126 }
127 }
128
129 Ok(())
130}
131
132fn handle_terraform_block(
133 current_file: &Path,
134 body: &hcl::Body,
135 module: &mut Module,
136) -> Result<()> {
137 body.attributes()
138 .filter(|attr| attr.key() == "required_version")
139 .for_each(|attr| {
140 module
141 .required_core
142 .push(attr.expr().to_string().replace('"', ""))
143 });
144
145 for inner_block in body.blocks() {
146 #[allow(clippy::all)]
147 match inner_block.identifier() {
148 "required_providers" => {
149 handle_required_providers_block(current_file, inner_block.body(), module)?
150 }
151 _ => (),
152 }
153 }
154
155 Ok(())
156}
157
158fn handle_required_providers_block(
159 current_file: &Path,
160 required_providers: &hcl::Body,
161 module: &mut Module,
162) -> Result<()> {
163 for provider in required_providers.attributes() {
164 let provider_name = provider.key().to_string();
165 let mut provider_req = ProviderRequirement::default();
166
167 match provider.expr() {
168 hcl::Expression::Object(attr) => {
169 if let Some(source) = attr.get(&ObjectKey::Identifier("source".into())) {
170 provider_req.source = source.to_string().replace('"', "");
171 }
172 if let Some(version) = attr.get(&ObjectKey::Identifier("version".into())) {
173 provider_req
174 .version_constraints
175 .push(version.to_string().replace('"', ""));
176 }
177 }
178 _ => {
179 return Err(Error::UnexpectedExpr {
180 attribute_key: provider_name,
181 expr: provider.expr().clone(),
182 file_name: current_file.to_path_buf(),
183 })
184 }
185 };
186
187 module
188 .required_providers
189 .insert(provider_name, provider_req);
190 }
191
192 Ok(())
193}
194
195fn get_files_in_dir(path: &Path) -> Result<Vec<PathBuf>> {
196 let mut primary = vec![];
197 let mut overrides = vec![];
198
199 for entry in std::fs::read_dir(path)? {
200 let file = entry?.path();
201 if file.is_dir() {
202 continue;
203 }
204
205 match file.extension() {
206 Some(ext) => {
207 match ext.to_str() {
208 Some(ext) => {
209 if ext.starts_with('.')
210 || ext.starts_with('#')
211 || ext.ends_with('~')
212 || ext.ends_with('#')
213 {
214 continue;
215 }
216 }
217 None => continue,
218 };
219 }
220 None => continue,
221 };
222
223 let basename = match file.file_stem() {
224 Some(basename) => basename.to_str().unwrap(),
225 None => continue,
226 };
227 let is_override = basename == "override" || basename.ends_with("_override");
228
229 if is_override {
230 overrides.push(file);
231 } else {
232 primary.push(file);
233 }
234 }
235
236 primary.append(&mut overrides);
237 Ok(primary)
238}