Skip to main content

terraform_wrapper/commands/
validate.rs

1use crate::Terraform;
2use crate::command::TerraformCommand;
3use crate::error::Result;
4use crate::exec;
5
6#[cfg(feature = "json")]
7use crate::types::validation::ValidationResult;
8
9/// Command for validating Terraform configuration.
10///
11/// Checks that the configuration is syntactically valid and internally
12/// consistent. Does not access any remote services (state, providers).
13///
14/// ```no_run
15/// # async fn example() -> terraform_wrapper::error::Result<()> {
16/// use terraform_wrapper::{Terraform, TerraformCommand};
17/// use terraform_wrapper::commands::validate::ValidateCommand;
18///
19/// let tf = Terraform::builder().working_dir("/tmp/infra").build()?;
20/// let result = ValidateCommand::new().execute(&tf).await?;
21/// # Ok(())
22/// # }
23/// ```
24#[derive(Debug, Clone)]
25pub struct ValidateCommand {
26    json: bool,
27    raw_args: Vec<String>,
28}
29
30impl Default for ValidateCommand {
31    fn default() -> Self {
32        Self {
33            json: true,
34            raw_args: Vec::new(),
35        }
36    }
37}
38
39impl ValidateCommand {
40    /// Create a new validate command (JSON output enabled by default).
41    #[must_use]
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Disable JSON output.
47    #[must_use]
48    pub fn no_json(mut self) -> Self {
49        self.json = false;
50        self
51    }
52
53    /// Add a raw argument (escape hatch for unsupported options).
54    #[must_use]
55    pub fn arg(mut self, arg: impl Into<String>) -> Self {
56        self.raw_args.push(arg.into());
57        self
58    }
59}
60
61#[cfg(feature = "json")]
62impl TerraformCommand for ValidateCommand {
63    type Output = ValidationResult;
64
65    fn args(&self) -> Vec<String> {
66        let mut args = vec!["validate".to_string()];
67        if self.json {
68            args.push("-json".to_string());
69        }
70        args.extend(self.raw_args.clone());
71        args
72    }
73
74    async fn execute(&self, tf: &Terraform) -> Result<ValidationResult> {
75        // validate returns exit code 1 for invalid config, but with -json
76        // it still writes valid JSON to stdout. Accept both exit codes.
77        let output = exec::run_terraform_allow_exit_codes(tf, self.args(), &[0, 1]).await?;
78        serde_json::from_str(&output.stdout).map_err(|e| crate::error::Error::Json {
79            message: "failed to parse validate json".to_string(),
80            source: e,
81        })
82    }
83}
84
85#[cfg(not(feature = "json"))]
86impl TerraformCommand for ValidateCommand {
87    type Output = exec::CommandOutput;
88
89    fn args(&self) -> Vec<String> {
90        let mut args = vec!["validate".to_string()];
91        if self.json {
92            args.push("-json".to_string());
93        }
94        args.extend(self.raw_args.clone());
95        args
96    }
97
98    async fn execute(&self, tf: &Terraform) -> Result<exec::CommandOutput> {
99        exec::run_terraform(tf, self.args()).await
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn default_args_include_json() {
109        let cmd = ValidateCommand::new();
110        assert_eq!(cmd.args(), vec!["validate", "-json"]);
111    }
112
113    #[test]
114    fn no_json_args() {
115        let cmd = ValidateCommand::new().no_json();
116        assert_eq!(cmd.args(), vec!["validate"]);
117    }
118
119    #[test]
120    fn raw_arg_escape_hatch() {
121        let cmd = ValidateCommand::new().arg("-test-directory=tests");
122        let args = cmd.args();
123        assert!(args.contains(&"-test-directory=tests".to_string()));
124    }
125}