docker_wrapper/command/
compose_rm.rs

1//! Docker Compose rm command implementation using unified trait pattern.
2
3use super::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Docker Compose rm command builder
8#[derive(Debug, Clone)]
9#[allow(clippy::struct_excessive_bools)] // Multiple boolean flags are appropriate for rm command
10pub struct ComposeRmCommand {
11    /// Base command executor
12    pub executor: CommandExecutor,
13    /// Base compose configuration
14    pub config: ComposeConfig,
15    /// Services to remove (empty for all)
16    pub services: Vec<String>,
17    /// Force removal without confirmation
18    pub force: bool,
19    /// Stop containers if running
20    pub stop: bool,
21    /// Remove volumes associated with containers
22    pub volumes: bool,
23}
24
25/// Result from compose rm command
26#[derive(Debug, Clone)]
27pub struct ComposeRmResult {
28    /// Raw stdout output
29    pub stdout: String,
30    /// Raw stderr output
31    pub stderr: String,
32    /// Success status
33    pub success: bool,
34    /// Services that were removed
35    pub services: Vec<String>,
36    /// Whether volumes were removed
37    pub volumes_removed: bool,
38}
39
40impl ComposeRmCommand {
41    /// Create a new compose rm command
42    #[must_use]
43    pub fn new() -> Self {
44        Self {
45            executor: CommandExecutor::new(),
46            config: ComposeConfig::new(),
47            services: Vec::new(),
48            force: false,
49            stop: false,
50            volumes: false,
51        }
52    }
53
54    /// Add a service to remove
55    #[must_use]
56    pub fn service(mut self, service: impl Into<String>) -> Self {
57        self.services.push(service.into());
58        self
59    }
60
61    /// Add multiple services to remove
62    #[must_use]
63    pub fn services<I, S>(mut self, services: I) -> Self
64    where
65        I: IntoIterator<Item = S>,
66        S: Into<String>,
67    {
68        self.services.extend(services.into_iter().map(Into::into));
69        self
70    }
71
72    /// Force removal without confirmation
73    #[must_use]
74    pub fn force(mut self) -> Self {
75        self.force = true;
76        self
77    }
78
79    /// Stop containers if running
80    #[must_use]
81    pub fn stop(mut self) -> Self {
82        self.stop = true;
83        self
84    }
85
86    /// Remove volumes associated with containers
87    #[must_use]
88    pub fn volumes(mut self) -> Self {
89        self.volumes = true;
90        self
91    }
92}
93
94impl Default for ComposeRmCommand {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100#[async_trait]
101impl DockerCommand for ComposeRmCommand {
102    type Output = ComposeRmResult;
103
104    fn get_executor(&self) -> &CommandExecutor {
105        &self.executor
106    }
107
108    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
109        &mut self.executor
110    }
111
112    fn build_command_args(&self) -> Vec<String> {
113        // Use the ComposeCommand implementation explicitly
114        <Self as ComposeCommand>::build_command_args(self)
115    }
116
117    async fn execute(&self) -> Result<Self::Output> {
118        let args = <Self as ComposeCommand>::build_command_args(self);
119        let output = self.execute_command(args).await?;
120
121        Ok(ComposeRmResult {
122            stdout: output.stdout,
123            stderr: output.stderr,
124            success: output.success,
125            services: self.services.clone(),
126            volumes_removed: self.volumes,
127        })
128    }
129}
130
131impl ComposeCommand for ComposeRmCommand {
132    fn get_config(&self) -> &ComposeConfig {
133        &self.config
134    }
135
136    fn get_config_mut(&mut self) -> &mut ComposeConfig {
137        &mut self.config
138    }
139
140    fn subcommand(&self) -> &'static str {
141        "rm"
142    }
143
144    fn build_subcommand_args(&self) -> Vec<String> {
145        let mut args = Vec::new();
146
147        if self.force {
148            args.push("--force".to_string());
149        }
150
151        if self.stop {
152            args.push("--stop".to_string());
153        }
154
155        if self.volumes {
156            args.push("--volumes".to_string());
157        }
158
159        // Add service names at the end
160        args.extend(self.services.clone());
161
162        args
163    }
164}
165
166impl ComposeRmResult {
167    /// Check if the command was successful
168    #[must_use]
169    pub fn success(&self) -> bool {
170        self.success
171    }
172
173    /// Get the services that were removed
174    #[must_use]
175    pub fn services(&self) -> &[String] {
176        &self.services
177    }
178
179    /// Check if volumes were removed
180    #[must_use]
181    pub fn volumes_removed(&self) -> bool {
182        self.volumes_removed
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_compose_rm_basic() {
192        let cmd = ComposeRmCommand::new();
193        let args = cmd.build_subcommand_args();
194        assert!(args.is_empty());
195
196        let full_args = ComposeCommand::build_command_args(&cmd);
197        assert_eq!(full_args[0], "compose");
198        assert!(full_args.contains(&"rm".to_string()));
199    }
200
201    #[test]
202    fn test_compose_rm_with_services() {
203        let cmd = ComposeRmCommand::new().service("web").service("worker");
204        let args = cmd.build_subcommand_args();
205        assert_eq!(args, vec!["web", "worker"]);
206    }
207
208    #[test]
209    fn test_compose_rm_with_flags() {
210        let cmd = ComposeRmCommand::new()
211            .force()
212            .stop()
213            .volumes()
214            .service("app");
215
216        let args = cmd.build_subcommand_args();
217        assert!(args.contains(&"--force".to_string()));
218        assert!(args.contains(&"--stop".to_string()));
219        assert!(args.contains(&"--volumes".to_string()));
220        assert!(args.contains(&"app".to_string()));
221    }
222
223    #[test]
224    fn test_compose_rm_force_only() {
225        let cmd = ComposeRmCommand::new().force().service("database");
226        let args = cmd.build_subcommand_args();
227        assert_eq!(args, vec!["--force", "database"]);
228    }
229
230    #[test]
231    fn test_compose_rm_with_volumes() {
232        let cmd = ComposeRmCommand::new()
233            .volumes()
234            .stop()
235            .services(vec!["cache", "queue"]);
236
237        let args = cmd.build_subcommand_args();
238        assert!(args.contains(&"--volumes".to_string()));
239        assert!(args.contains(&"--stop".to_string()));
240        assert!(args.contains(&"cache".to_string()));
241        assert!(args.contains(&"queue".to_string()));
242    }
243
244    #[test]
245    fn test_compose_config_integration() {
246        let cmd = ComposeRmCommand::new()
247            .file("docker-compose.yml")
248            .project_name("myapp")
249            .force()
250            .volumes()
251            .service("web");
252
253        let args = ComposeCommand::build_command_args(&cmd);
254        assert!(args.contains(&"--file".to_string()));
255        assert!(args.contains(&"docker-compose.yml".to_string()));
256        assert!(args.contains(&"--project-name".to_string()));
257        assert!(args.contains(&"myapp".to_string()));
258        assert!(args.contains(&"--force".to_string()));
259        assert!(args.contains(&"--volumes".to_string()));
260        assert!(args.contains(&"web".to_string()));
261    }
262}