Skip to main content

tfparser_core/ir/
files.rs

1//! Source file classification used during discovery and loading.
2//!
3//! Per [11-discovery.md § 2], every file the workspace walker emits is
4//! classified by extension. Anything outside [`FileExt`] is silently skipped
5//! at discovery time.
6//!
7//! [11-discovery.md § 2]: ../../specs/11-discovery.md
8
9use std::{path::Path, sync::Arc};
10
11use serde::{Deserialize, Serialize};
12use typed_builder::TypedBuilder;
13
14/// Recognised source-file extensions.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[serde(rename_all = "kebab-case")]
17#[non_exhaustive]
18pub enum FileExt {
19    /// `.tf` — Terraform configuration.
20    Tf,
21    /// `.tfvars` — Terraform variable values.
22    Tfvars,
23    /// `.hcl` — generic HCL (non-Terragrunt).
24    Hcl,
25    /// `terragrunt.hcl` and other Terragrunt-shaped `.hcl` files at the
26    /// top level of a component dir.
27    TerragruntHcl,
28    /// `.json` — JSON fragments referenced by Terraform configurations
29    /// (e.g. IAM policy files in a `files/` subdir).
30    Json,
31}
32
33impl FileExt {
34    /// Classify a path by its filename. Returns `None` for unrecognised
35    /// extensions.
36    ///
37    /// A file named exactly `terragrunt.hcl` is classified as
38    /// [`FileExt::TerragruntHcl`]. Other `.hcl` files (e.g.
39    /// `common.terragrunt.hcl`, `staging.terragrunt.hcl`, `root.hcl`) are
40    /// classified as [`FileExt::Hcl`] — the Terragrunt resolver decides
41    /// downstream whether to treat them as Terragrunt-shaped based on
42    /// content.
43    #[must_use]
44    pub fn classify(path: &Path) -> Option<Self> {
45        let name = path.file_name()?.to_str()?;
46        if name == "terragrunt.hcl" {
47            return Some(Self::TerragruntHcl);
48        }
49        let ext = path.extension()?.to_str()?;
50        match ext {
51            "tf" => Some(Self::Tf),
52            "tfvars" => Some(Self::Tfvars),
53            "hcl" => Some(Self::Hcl),
54            "json" => Some(Self::Json),
55            _ => None,
56        }
57    }
58
59    /// Whether this extension carries HCL syntax (i.e. should be fed to the
60    /// HCL loader).
61    #[must_use]
62    pub const fn is_hcl(self) -> bool {
63        matches!(
64            self,
65            Self::Tf | Self::Hcl | Self::TerragruntHcl | Self::Tfvars
66        )
67    }
68}
69
70/// A single source file inside a [`Component`](crate::ir::Component).
71///
72/// Cheap to clone — the path is `Arc`-shared with the rest of the IR.
73#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TypedBuilder)]
74#[non_exhaustive]
75#[serde(rename_all = "camelCase")]
76#[builder(field_defaults(setter(into)))]
77pub struct SourceFile {
78    /// Path of the file, relative to the workspace root.
79    #[serde(with = "crate::ir::path_serde::arc_path")]
80    pub path: Arc<Path>,
81    /// Classified extension.
82    pub ext: FileExt,
83    /// Size in bytes, as observed during discovery.
84    pub size: u64,
85}
86
87#[cfg(test)]
88mod tests {
89    use std::path::PathBuf;
90
91    use super::*;
92
93    #[test]
94    fn test_should_classify_terraform_extensions() {
95        assert_eq!(
96            FileExt::classify(&PathBuf::from("main.tf")),
97            Some(FileExt::Tf)
98        );
99        assert_eq!(
100            FileExt::classify(&PathBuf::from("prod.tfvars")),
101            Some(FileExt::Tfvars)
102        );
103        assert_eq!(
104            FileExt::classify(&PathBuf::from("root.hcl")),
105            Some(FileExt::Hcl)
106        );
107        assert_eq!(
108            FileExt::classify(&PathBuf::from("path/to/terragrunt.hcl")),
109            Some(FileExt::TerragruntHcl)
110        );
111        assert_eq!(
112            FileExt::classify(&PathBuf::from("policy.json")),
113            Some(FileExt::Json)
114        );
115    }
116
117    #[test]
118    fn test_should_reject_unknown_extension() {
119        assert!(FileExt::classify(&PathBuf::from("README.md")).is_none());
120    }
121
122    #[test]
123    fn test_should_classify_hcl_files_as_hcl_input() {
124        for ext in [
125            FileExt::Tf,
126            FileExt::Tfvars,
127            FileExt::Hcl,
128            FileExt::TerragruntHcl,
129        ] {
130            assert!(ext.is_hcl(), "{ext:?} should be HCL-shaped");
131        }
132        assert!(!FileExt::Json.is_hcl());
133    }
134}