docker_wrapper/command/
compose_stop.rs

1//! Docker Compose stop 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 stop command builder
9#[derive(Debug, Clone)]
10pub struct ComposeStopCommand {
11    /// Base command executor
12    pub executor: CommandExecutor,
13    /// Base compose configuration
14    pub config: ComposeConfig,
15    /// Services to stop (empty for all)
16    pub services: Vec<String>,
17    /// Timeout for stopping containers
18    pub timeout: Option<Duration>,
19}
20
21/// Result from compose stop command
22#[derive(Debug, Clone)]
23pub struct ComposeStopResult {
24    /// Raw stdout output
25    pub stdout: String,
26    /// Raw stderr output
27    pub stderr: String,
28    /// Success status
29    pub success: bool,
30    /// Services that were stopped
31    pub services: Vec<String>,
32}
33
34impl ComposeStopCommand {
35    /// Create a new compose stop command
36    #[must_use]
37    pub fn new() -> Self {
38        Self {
39            executor: CommandExecutor::new(),
40            config: ComposeConfig::new(),
41            services: Vec::new(),
42            timeout: None,
43        }
44    }
45
46    /// Add a service to stop
47    #[must_use]
48    pub fn service(mut self, service: impl Into<String>) -> Self {
49        self.services.push(service.into());
50        self
51    }
52
53    /// Add multiple services to stop
54    #[must_use]
55    pub fn services<I, S>(mut self, services: I) -> Self
56    where
57        I: IntoIterator<Item = S>,
58        S: Into<String>,
59    {
60        self.services.extend(services.into_iter().map(Into::into));
61        self
62    }
63
64    /// Set the timeout for stopping containers
65    #[must_use]
66    pub fn timeout(mut self, timeout: Duration) -> Self {
67        self.timeout = Some(timeout);
68        self
69    }
70}
71
72impl Default for ComposeStopCommand {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78#[async_trait]
79impl DockerCommand for ComposeStopCommand {
80    type Output = ComposeStopResult;
81
82    fn get_executor(&self) -> &CommandExecutor {
83        &self.executor
84    }
85
86    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
87        &mut self.executor
88    }
89
90    fn build_command_args(&self) -> Vec<String> {
91        // Use the ComposeCommand implementation explicitly
92        <Self as ComposeCommand>::build_command_args(self)
93    }
94
95    async fn execute(&self) -> Result<Self::Output> {
96        let args = <Self as ComposeCommand>::build_command_args(self);
97        let output = self.execute_command(args).await?;
98
99        Ok(ComposeStopResult {
100            stdout: output.stdout,
101            stderr: output.stderr,
102            success: output.success,
103            services: self.services.clone(),
104        })
105    }
106}
107
108impl ComposeCommand for ComposeStopCommand {
109    fn get_config(&self) -> &ComposeConfig {
110        &self.config
111    }
112
113    fn get_config_mut(&mut self) -> &mut ComposeConfig {
114        &mut self.config
115    }
116
117    fn subcommand(&self) -> &'static str {
118        "stop"
119    }
120
121    fn build_subcommand_args(&self) -> Vec<String> {
122        let mut args = Vec::new();
123
124        if let Some(timeout) = self.timeout {
125            args.push("--timeout".to_string());
126            args.push(timeout.as_secs().to_string());
127        }
128
129        // Add service names at the end
130        args.extend(self.services.clone());
131
132        args
133    }
134}
135
136impl ComposeStopResult {
137    /// Check if the command was successful
138    #[must_use]
139    pub fn success(&self) -> bool {
140        self.success
141    }
142
143    /// Get the services that were stopped
144    #[must_use]
145    pub fn services(&self) -> &[String] {
146        &self.services
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_compose_stop_basic() {
156        let cmd = ComposeStopCommand::new();
157        let args = cmd.build_subcommand_args();
158        assert!(args.is_empty());
159
160        let full_args = ComposeCommand::build_command_args(&cmd);
161        assert_eq!(full_args[0], "compose");
162        assert!(full_args.contains(&"stop".to_string()));
163    }
164
165    #[test]
166    fn test_compose_stop_with_services() {
167        let cmd = ComposeStopCommand::new().service("web").service("worker");
168        let args = cmd.build_subcommand_args();
169        assert_eq!(args, vec!["web", "worker"]);
170    }
171
172    #[test]
173    fn test_compose_stop_with_timeout() {
174        let cmd = ComposeStopCommand::new()
175            .timeout(Duration::from_secs(10))
176            .service("db");
177
178        let args = cmd.build_subcommand_args();
179        assert_eq!(args, vec!["--timeout", "10", "db"]);
180    }
181
182    #[test]
183    fn test_compose_stop_with_services_method() {
184        let cmd = ComposeStopCommand::new().services(vec!["redis", "postgres"]);
185        let args = cmd.build_subcommand_args();
186        assert_eq!(args, vec!["redis", "postgres"]);
187    }
188
189    #[test]
190    fn test_compose_stop_zero_timeout() {
191        let cmd = ComposeStopCommand::new()
192            .timeout(Duration::from_secs(0))
193            .service("app");
194
195        let args = cmd.build_subcommand_args();
196        assert_eq!(args, vec!["--timeout", "0", "app"]);
197    }
198
199    #[test]
200    fn test_compose_stop_all_services_with_timeout() {
201        let cmd = ComposeStopCommand::new().timeout(Duration::from_secs(30));
202        let args = cmd.build_subcommand_args();
203        assert_eq!(args, vec!["--timeout", "30"]);
204    }
205
206    #[test]
207    fn test_compose_config_integration() {
208        let cmd = ComposeStopCommand::new()
209            .file("compose.yml")
210            .project_name("webapp")
211            .timeout(Duration::from_secs(5))
212            .service("redis");
213
214        let args = ComposeCommand::build_command_args(&cmd);
215        assert!(args.contains(&"--file".to_string()));
216        assert!(args.contains(&"compose.yml".to_string()));
217        assert!(args.contains(&"--project-name".to_string()));
218        assert!(args.contains(&"webapp".to_string()));
219        assert!(args.contains(&"--timeout".to_string()));
220        assert!(args.contains(&"5".to_string()));
221        assert!(args.contains(&"redis".to_string()));
222    }
223}