Skip to main content

terraform_wrapper/commands/
graph.rs

1use crate::Terraform;
2use crate::command::TerraformCommand;
3use crate::error::Result;
4use crate::exec::{self, CommandOutput};
5
6/// Command for generating a visual dependency graph in DOT format.
7///
8/// Produces a representation of the dependency graph between Terraform
9/// resources, suitable for rendering with Graphviz.
10///
11/// ```no_run
12/// # async fn example() -> terraform_wrapper::error::Result<()> {
13/// use terraform_wrapper::{Terraform, TerraformCommand};
14/// use terraform_wrapper::commands::graph::GraphCommand;
15///
16/// let tf = Terraform::builder().working_dir("/tmp/infra").build()?;
17/// let output = GraphCommand::new()
18///     .draw_cycles()
19///     .execute(&tf)
20///     .await?;
21/// // output.stdout contains DOT format graph
22/// # Ok(())
23/// # }
24/// ```
25#[derive(Debug, Clone, Default)]
26pub struct GraphCommand {
27    graph_type: Option<String>,
28    plan_file: Option<String>,
29    draw_cycles: bool,
30    raw_args: Vec<String>,
31}
32
33impl GraphCommand {
34    /// Create a new graph command with default options.
35    #[must_use]
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Set the graph type (`-type=plan|plan-refresh-only|plan-destroy|apply`).
41    #[must_use]
42    pub fn graph_type(mut self, graph_type: &str) -> Self {
43        self.graph_type = Some(graph_type.to_string());
44        self
45    }
46
47    /// Use a saved plan file (`-plan=<file>`).
48    ///
49    /// Automatically sets the graph type to `apply`.
50    #[must_use]
51    pub fn plan_file(mut self, path: &str) -> Self {
52        self.plan_file = Some(path.to_string());
53        self
54    }
55
56    /// Highlight circular dependencies (`-draw-cycles`).
57    #[must_use]
58    pub fn draw_cycles(mut self) -> Self {
59        self.draw_cycles = true;
60        self
61    }
62
63    /// Add a raw argument (escape hatch for unsupported options).
64    #[must_use]
65    pub fn arg(mut self, arg: impl Into<String>) -> Self {
66        self.raw_args.push(arg.into());
67        self
68    }
69}
70
71impl TerraformCommand for GraphCommand {
72    type Output = CommandOutput;
73
74    fn args(&self) -> Vec<String> {
75        let mut args = vec!["graph".to_string()];
76        if let Some(ref graph_type) = self.graph_type {
77            args.push(format!("-type={graph_type}"));
78        }
79        if let Some(ref plan_file) = self.plan_file {
80            args.push(format!("-plan={plan_file}"));
81        }
82        if self.draw_cycles {
83            args.push("-draw-cycles".to_string());
84        }
85        args.extend(self.raw_args.clone());
86        args
87    }
88
89    async fn execute(&self, tf: &Terraform) -> Result<CommandOutput> {
90        exec::run_terraform(tf, self.args()).await
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn default_args() {
100        let cmd = GraphCommand::new();
101        assert_eq!(cmd.args(), vec!["graph"]);
102    }
103
104    #[test]
105    fn draw_cycles() {
106        let cmd = GraphCommand::new().draw_cycles();
107        assert_eq!(cmd.args(), vec!["graph", "-draw-cycles"]);
108    }
109
110    #[test]
111    fn graph_type() {
112        let cmd = GraphCommand::new().graph_type("plan-destroy");
113        assert_eq!(cmd.args(), vec!["graph", "-type=plan-destroy"]);
114    }
115
116    #[test]
117    fn plan_file() {
118        let cmd = GraphCommand::new().plan_file("tfplan");
119        assert_eq!(cmd.args(), vec!["graph", "-plan=tfplan"]);
120    }
121
122    #[test]
123    fn all_options() {
124        let cmd = GraphCommand::new()
125            .graph_type("apply")
126            .plan_file("tfplan")
127            .draw_cycles();
128        let args = cmd.args();
129        assert_eq!(args[0], "graph");
130        assert!(args.contains(&"-type=apply".to_string()));
131        assert!(args.contains(&"-plan=tfplan".to_string()));
132        assert!(args.contains(&"-draw-cycles".to_string()));
133    }
134}