Skip to main content

terraform_wrapper/commands/
output.rs

1use std::collections::HashMap;
2
3use crate::Terraform;
4use crate::command::TerraformCommand;
5use crate::error::Result;
6use crate::exec;
7
8/// Result from an output command.
9///
10/// The variant returned depends on which flags were set on the command:
11/// - `.json()` returns [`OutputResult::Json`] or [`OutputResult::Single`]
12/// - `.raw()` returns [`OutputResult::Raw`]
13/// - Neither returns [`OutputResult::Plain`]
14#[derive(Debug, Clone)]
15pub enum OutputResult {
16    /// Raw string value from `-raw` flag.
17    Raw(String),
18    /// All output values as JSON (when `.json()` and no `.name()`).
19    #[cfg(feature = "json")]
20    Json(HashMap<String, crate::types::output::OutputValue>),
21    /// Single output value as JSON (when `.json()` and `.name()`).
22    #[cfg(feature = "json")]
23    Single(crate::types::output::OutputValue),
24    /// Plain command output (no `-json` or `-raw`).
25    Plain(exec::CommandOutput),
26}
27
28/// Command for reading Terraform output values.
29///
30/// ```no_run
31/// # async fn example() -> terraform_wrapper::error::Result<()> {
32/// use terraform_wrapper::{Terraform, TerraformCommand};
33/// use terraform_wrapper::commands::output::{OutputCommand, OutputResult};
34///
35/// let tf = Terraform::builder().working_dir("/tmp/infra").build()?;
36///
37/// // Get all outputs as JSON
38/// let result = OutputCommand::new().json().execute(&tf).await?;
39///
40/// // Get a single raw output value
41/// let result = OutputCommand::new()
42///     .name("public_ip")
43///     .raw()
44///     .execute(&tf)
45///     .await?;
46/// # Ok(())
47/// # }
48/// ```
49#[derive(Debug, Clone, Default)]
50pub struct OutputCommand {
51    name: Option<String>,
52    json: bool,
53    raw: bool,
54    raw_args: Vec<String>,
55}
56
57impl OutputCommand {
58    /// Create a new output command.
59    #[must_use]
60    pub fn new() -> Self {
61        Self::default()
62    }
63
64    /// Request a specific named output (positional argument).
65    #[must_use]
66    pub fn name(mut self, name: &str) -> Self {
67        self.name = Some(name.to_string());
68        self
69    }
70
71    /// Request JSON-formatted output (`-json`).
72    #[must_use]
73    pub fn json(mut self) -> Self {
74        self.json = true;
75        self
76    }
77
78    /// Request raw output value (`-raw`). Requires `.name()` to be set.
79    #[must_use]
80    pub fn raw(mut self) -> Self {
81        self.raw = true;
82        self
83    }
84
85    /// Add a raw argument (escape hatch for unsupported options).
86    #[must_use]
87    pub fn arg(mut self, arg: impl Into<String>) -> Self {
88        self.raw_args.push(arg.into());
89        self
90    }
91}
92
93impl TerraformCommand for OutputCommand {
94    type Output = OutputResult;
95
96    fn args(&self) -> Vec<String> {
97        let mut args = vec!["output".to_string()];
98        if self.json {
99            args.push("-json".to_string());
100        }
101        if self.raw {
102            args.push("-raw".to_string());
103        }
104        args.extend(self.raw_args.clone());
105        if let Some(ref name) = self.name {
106            args.push(name.clone());
107        }
108        args
109    }
110
111    async fn execute(&self, tf: &Terraform) -> Result<OutputResult> {
112        let output = exec::run_terraform(tf, self.args()).await?;
113
114        if self.raw {
115            return Ok(OutputResult::Raw(output.stdout.trim_end().to_string()));
116        }
117
118        #[cfg(feature = "json")]
119        if self.json {
120            if self.name.is_some() {
121                let value: crate::types::output::OutputValue = serde_json::from_str(&output.stdout)
122                    .map_err(|e| crate::error::Error::ParseError {
123                        message: format!("failed to parse output json: {e}"),
124                    })?;
125                return Ok(OutputResult::Single(value));
126            }
127            let values: HashMap<String, crate::types::output::OutputValue> =
128                serde_json::from_str(&output.stdout).map_err(|e| {
129                    crate::error::Error::ParseError {
130                        message: format!("failed to parse output json: {e}"),
131                    }
132                })?;
133            return Ok(OutputResult::Json(values));
134        }
135
136        Ok(OutputResult::Plain(output))
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn default_args() {
146        let cmd = OutputCommand::new();
147        assert_eq!(cmd.args(), vec!["output"]);
148    }
149
150    #[test]
151    fn json_all_outputs() {
152        let cmd = OutputCommand::new().json();
153        assert_eq!(cmd.args(), vec!["output", "-json"]);
154    }
155
156    #[test]
157    fn raw_named_output() {
158        let cmd = OutputCommand::new().name("public_ip").raw();
159        let args = cmd.args();
160        assert_eq!(args, vec!["output", "-raw", "public_ip"]);
161    }
162
163    #[test]
164    fn json_named_output() {
165        let cmd = OutputCommand::new().name("vpc_id").json();
166        let args = cmd.args();
167        assert_eq!(args, vec!["output", "-json", "vpc_id"]);
168    }
169
170    #[test]
171    fn name_at_end() {
172        let cmd = OutputCommand::new().name("endpoint").arg("-no-color");
173        let args = cmd.args();
174        // Name should be the last positional argument
175        assert_eq!(args.last().unwrap(), "endpoint");
176    }
177}