Skip to main content

terraform_wrapper/commands/
show.rs

1use crate::Terraform;
2use crate::command::TerraformCommand;
3use crate::error::Result;
4use crate::exec;
5
6/// Result from a show command.
7///
8/// The variant depends on whether a plan file was specified:
9/// - No plan file: [`ShowResult::State`] with current state
10/// - With plan file: [`ShowResult::Plan`] with plan details
11/// - Without JSON: [`ShowResult::Plain`] with raw output
12#[derive(Debug, Clone)]
13pub enum ShowResult {
14    /// Current state from `terraform show -json`.
15    #[cfg(feature = "json")]
16    State(crate::types::state::StateRepresentation),
17    /// Saved plan from `terraform show -json <planfile>`.
18    #[cfg(feature = "json")]
19    Plan(Box<crate::types::plan::PlanRepresentation>),
20    /// Plain command output (no `-json`).
21    Plain(exec::CommandOutput),
22}
23
24/// Command for inspecting current state or a saved plan.
25///
26/// With `-json`, returns structured data about the current state or a
27/// saved plan file.
28///
29/// ```no_run
30/// # async fn example() -> terraform_wrapper::error::Result<()> {
31/// use terraform_wrapper::{Terraform, TerraformCommand};
32/// use terraform_wrapper::commands::show::{ShowCommand, ShowResult};
33///
34/// let tf = Terraform::builder().working_dir("/tmp/infra").build()?;
35///
36/// // Show current state
37/// let result = ShowCommand::new().execute(&tf).await?;
38///
39/// // Show a saved plan
40/// let result = ShowCommand::new()
41///     .plan_file("tfplan")
42///     .execute(&tf)
43///     .await?;
44/// # Ok(())
45/// # }
46/// ```
47#[derive(Debug, Clone, Default)]
48pub struct ShowCommand {
49    plan_file: Option<String>,
50    json: bool,
51    raw_args: Vec<String>,
52}
53
54impl ShowCommand {
55    /// Create a new show command.
56    #[must_use]
57    pub fn new() -> Self {
58        Self {
59            json: true,
60            ..Self::default()
61        }
62    }
63
64    /// Show a saved plan file instead of current state.
65    #[must_use]
66    pub fn plan_file(mut self, path: &str) -> Self {
67        self.plan_file = Some(path.to_string());
68        self
69    }
70
71    /// Disable JSON output.
72    #[must_use]
73    pub fn no_json(mut self) -> Self {
74        self.json = false;
75        self
76    }
77
78    /// Add a raw argument (escape hatch for unsupported options).
79    #[must_use]
80    pub fn arg(mut self, arg: impl Into<String>) -> Self {
81        self.raw_args.push(arg.into());
82        self
83    }
84}
85
86impl TerraformCommand for ShowCommand {
87    type Output = ShowResult;
88
89    fn args(&self) -> Vec<String> {
90        let mut args = vec!["show".to_string()];
91        if self.json {
92            args.push("-json".to_string());
93        }
94        args.extend(self.raw_args.clone());
95        // Plan file is a positional argument at the end
96        if let Some(ref plan) = self.plan_file {
97            args.push(plan.clone());
98        }
99        args
100    }
101
102    async fn execute(&self, tf: &Terraform) -> Result<ShowResult> {
103        let output = exec::run_terraform(tf, self.args()).await?;
104
105        if !self.json {
106            return Ok(ShowResult::Plain(output));
107        }
108
109        #[cfg(feature = "json")]
110        if self.plan_file.is_some() {
111            let plan: crate::types::plan::PlanRepresentation = serde_json::from_str(&output.stdout)
112                .map_err(|e| crate::error::Error::ParseError {
113                    message: format!("failed to parse plan json: {e}"),
114                })?;
115            return Ok(ShowResult::Plan(Box::new(plan)));
116        }
117
118        #[cfg(feature = "json")]
119        {
120            let state: crate::types::state::StateRepresentation =
121                serde_json::from_str(&output.stdout).map_err(|e| {
122                    crate::error::Error::ParseError {
123                        message: format!("failed to parse state json: {e}"),
124                    }
125                })?;
126            Ok(ShowResult::State(state))
127        }
128
129        #[cfg(not(feature = "json"))]
130        Ok(ShowResult::Plain(output))
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn default_args_include_json() {
140        let cmd = ShowCommand::new();
141        assert_eq!(cmd.args(), vec!["show", "-json"]);
142    }
143
144    #[test]
145    fn plan_file_at_end() {
146        let cmd = ShowCommand::new().plan_file("tfplan");
147        assert_eq!(cmd.args(), vec!["show", "-json", "tfplan"]);
148    }
149
150    #[test]
151    fn no_json_args() {
152        let cmd = ShowCommand::new().no_json();
153        assert_eq!(cmd.args(), vec!["show"]);
154    }
155
156    #[test]
157    fn no_json_with_plan_file() {
158        let cmd = ShowCommand::new().no_json().plan_file("tfplan");
159        assert_eq!(cmd.args(), vec!["show", "tfplan"]);
160    }
161}