docker_wrapper/command/
compose_down.rs

1//! Docker Compose down command implementation using unified trait pattern.
2
3use super::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use std::time::Duration;
7
8/// Docker Compose down command builder
9#[derive(Debug, Clone)]
10pub struct ComposeDownCommand {
11    /// Base command executor
12    pub executor: CommandExecutor,
13    /// Base compose configuration
14    pub config: ComposeConfig,
15    /// Remove images
16    pub remove_images: Option<RemoveImages>,
17    /// Remove named volumes
18    pub volumes: bool,
19    /// Remove orphan containers
20    pub remove_orphans: bool,
21    /// Timeout for container shutdown
22    pub timeout: Option<Duration>,
23    /// Services to stop (empty for all)
24    pub services: Vec<String>,
25}
26
27/// Image removal options for compose down
28#[derive(Debug, Clone, Copy)]
29pub enum RemoveImages {
30    /// Remove all images used by services
31    All,
32    /// Remove only images that don't have a custom tag
33    Local,
34}
35
36impl std::fmt::Display for RemoveImages {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            Self::All => write!(f, "all"),
40            Self::Local => write!(f, "local"),
41        }
42    }
43}
44
45/// Result from compose down command
46#[derive(Debug, Clone)]
47pub struct ComposeDownResult {
48    /// Raw stdout output
49    pub stdout: String,
50    /// Raw stderr output
51    pub stderr: String,
52    /// Success status
53    pub success: bool,
54    /// Whether volumes were removed
55    pub removed_volumes: bool,
56    /// Whether images were removed
57    pub removed_images: bool,
58}
59
60impl ComposeDownCommand {
61    /// Create a new compose down command
62    #[must_use]
63    pub fn new() -> Self {
64        Self {
65            executor: CommandExecutor::new(),
66            config: ComposeConfig::new(),
67            remove_images: None,
68            volumes: false,
69            remove_orphans: false,
70            timeout: None,
71            services: Vec::new(),
72        }
73    }
74
75    /// Remove images (all or local)
76    #[must_use]
77    pub fn remove_images(mut self, policy: RemoveImages) -> Self {
78        self.remove_images = Some(policy);
79        self
80    }
81
82    /// Remove named volumes declared in the volumes section
83    #[must_use]
84    pub fn volumes(mut self) -> Self {
85        self.volumes = true;
86        self
87    }
88
89    /// Remove containers for services not defined in the compose file
90    #[must_use]
91    pub fn remove_orphans(mut self) -> Self {
92        self.remove_orphans = true;
93        self
94    }
95
96    /// Set timeout for container shutdown
97    #[must_use]
98    pub fn timeout(mut self, timeout: Duration) -> Self {
99        self.timeout = Some(timeout);
100        self
101    }
102
103    /// Add a service to stop
104    #[must_use]
105    pub fn service(mut self, service: impl Into<String>) -> Self {
106        self.services.push(service.into());
107        self
108    }
109
110    /// Add multiple services
111    #[must_use]
112    pub fn services<I, S>(mut self, services: I) -> Self
113    where
114        I: IntoIterator<Item = S>,
115        S: Into<String>,
116    {
117        self.services.extend(services.into_iter().map(Into::into));
118        self
119    }
120}
121
122impl Default for ComposeDownCommand {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128#[async_trait]
129impl DockerCommand for ComposeDownCommand {
130    type Output = ComposeDownResult;
131
132    fn get_executor(&self) -> &CommandExecutor {
133        &self.executor
134    }
135
136    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
137        &mut self.executor
138    }
139
140    fn build_command_args(&self) -> Vec<String> {
141        // Use the ComposeCommand implementation explicitly
142        <Self as ComposeCommand>::build_command_args(self)
143    }
144
145    async fn execute(&self) -> Result<Self::Output> {
146        let args = <Self as ComposeCommand>::build_command_args(self);
147        let output = self.execute_command(args).await?;
148
149        Ok(ComposeDownResult {
150            stdout: output.stdout,
151            stderr: output.stderr,
152            success: output.success,
153            removed_volumes: self.volumes,
154            removed_images: self.remove_images.is_some(),
155        })
156    }
157}
158
159impl ComposeCommand for ComposeDownCommand {
160    fn get_config(&self) -> &ComposeConfig {
161        &self.config
162    }
163
164    fn get_config_mut(&mut self) -> &mut ComposeConfig {
165        &mut self.config
166    }
167
168    fn subcommand(&self) -> &'static str {
169        "down"
170    }
171
172    fn build_subcommand_args(&self) -> Vec<String> {
173        let mut args = Vec::new();
174
175        if let Some(ref remove) = self.remove_images {
176            args.push("--rmi".to_string());
177            args.push(remove.to_string());
178        }
179
180        if self.volumes {
181            args.push("--volumes".to_string());
182        }
183
184        if self.remove_orphans {
185            args.push("--remove-orphans".to_string());
186        }
187
188        if let Some(timeout) = self.timeout {
189            args.push("--timeout".to_string());
190            args.push(timeout.as_secs().to_string());
191        }
192
193        // Add service names at the end
194        args.extend(self.services.clone());
195
196        args
197    }
198}
199
200impl ComposeDownResult {
201    /// Check if the command was successful
202    #[must_use]
203    pub fn success(&self) -> bool {
204        self.success
205    }
206
207    /// Check if volumes were removed
208    #[must_use]
209    pub fn volumes_removed(&self) -> bool {
210        self.removed_volumes
211    }
212
213    /// Check if images were removed
214    #[must_use]
215    pub fn images_removed(&self) -> bool {
216        self.removed_images
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_compose_down_basic() {
226        let cmd = ComposeDownCommand::new();
227        let args = cmd.build_subcommand_args();
228        assert!(args.is_empty());
229
230        let full_args = ComposeCommand::build_command_args(&cmd);
231        assert_eq!(full_args[0], "compose");
232        assert!(full_args.contains(&"down".to_string()));
233    }
234
235    #[test]
236    fn test_compose_down_with_volumes() {
237        let cmd = ComposeDownCommand::new().volumes();
238        let args = cmd.build_subcommand_args();
239        assert_eq!(args, vec!["--volumes"]);
240    }
241
242    #[test]
243    fn test_compose_down_remove_images() {
244        let cmd = ComposeDownCommand::new().remove_images(RemoveImages::All);
245        let args = cmd.build_subcommand_args();
246        assert_eq!(args, vec!["--rmi", "all"]);
247    }
248
249    #[test]
250    fn test_compose_down_all_options() {
251        let cmd = ComposeDownCommand::new()
252            .remove_images(RemoveImages::Local)
253            .volumes()
254            .remove_orphans()
255            .timeout(Duration::from_secs(30))
256            .service("web")
257            .service("db");
258
259        let args = cmd.build_subcommand_args();
260        assert_eq!(
261            args,
262            vec![
263                "--rmi",
264                "local",
265                "--volumes",
266                "--remove-orphans",
267                "--timeout",
268                "30",
269                "web",
270                "db"
271            ]
272        );
273    }
274
275    #[test]
276    fn test_remove_images_display() {
277        assert_eq!(RemoveImages::All.to_string(), "all");
278        assert_eq!(RemoveImages::Local.to_string(), "local");
279    }
280
281    #[test]
282    fn test_compose_config_integration() {
283        let cmd = ComposeDownCommand::new()
284            .file("docker-compose.yml")
285            .project_name("my-project")
286            .volumes()
287            .remove_orphans();
288
289        let args = ComposeCommand::build_command_args(&cmd);
290        assert!(args.contains(&"--file".to_string()));
291        assert!(args.contains(&"docker-compose.yml".to_string()));
292        assert!(args.contains(&"--project-name".to_string()));
293        assert!(args.contains(&"my-project".to_string()));
294        assert!(args.contains(&"--volumes".to_string()));
295        assert!(args.contains(&"--remove-orphans".to_string()));
296    }
297}