docker_wrapper/compose/
wait.rs

1//! Docker Compose wait command implementation.
2
3use crate::compose::{ComposeCommandV2 as ComposeCommand, ComposeConfig};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Docker Compose wait command
8///
9/// Wait for services to reach a desired state.
10#[derive(Debug, Clone, Default)]
11pub struct ComposeWaitCommand {
12    /// Base configuration
13    pub config: ComposeConfig,
14    /// Services to wait for
15    pub services: Vec<String>,
16    /// Wait for services to be running and healthy
17    pub down_project: bool,
18}
19
20/// Result from wait command
21#[derive(Debug, Clone)]
22pub struct WaitResult {
23    /// Output from the command
24    pub output: String,
25    /// Whether the operation succeeded
26    pub success: bool,
27    /// Exit codes from services
28    pub exit_codes: Vec<i32>,
29}
30
31impl ComposeWaitCommand {
32    /// Create a new wait command
33    #[must_use]
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Add a compose file
39    #[must_use]
40    pub fn file<P: Into<std::path::PathBuf>>(mut self, file: P) -> Self {
41        self.config.files.push(file.into());
42        self
43    }
44
45    /// Set project name
46    #[must_use]
47    pub fn project_name(mut self, name: impl Into<String>) -> Self {
48        self.config.project_name = Some(name.into());
49        self
50    }
51
52    /// Wait for the entire project to stop
53    #[must_use]
54    pub fn down_project(mut self) -> Self {
55        self.down_project = true;
56        self
57    }
58
59    /// Add a service to wait for
60    #[must_use]
61    pub fn service(mut self, service: impl Into<String>) -> Self {
62        self.services.push(service.into());
63        self
64    }
65
66    /// Add multiple services to wait for
67    #[must_use]
68    pub fn services<I, S>(mut self, services: I) -> Self
69    where
70        I: IntoIterator<Item = S>,
71        S: Into<String>,
72    {
73        self.services.extend(services.into_iter().map(Into::into));
74        self
75    }
76
77    fn build_args(&self) -> Vec<String> {
78        let mut args = vec!["wait".to_string()];
79
80        // Add flags
81        if self.down_project {
82            args.push("--down-project".to_string());
83        }
84
85        // Add services
86        args.extend(self.services.clone());
87
88        args
89    }
90}
91
92#[async_trait]
93impl ComposeCommand for ComposeWaitCommand {
94    type Output = WaitResult;
95
96    fn get_config(&self) -> &ComposeConfig {
97        &self.config
98    }
99
100    fn get_config_mut(&mut self) -> &mut ComposeConfig {
101        &mut self.config
102    }
103
104    async fn execute_compose(&self, args: Vec<String>) -> Result<Self::Output> {
105        let output = self.execute_compose_command(args).await?;
106
107        // Parse exit codes from output if available
108        let exit_codes = output
109            .stdout
110            .lines()
111            .filter_map(|line| {
112                // Try to parse exit codes from output
113                line.split_whitespace()
114                    .last()
115                    .and_then(|s| s.parse::<i32>().ok())
116            })
117            .collect();
118
119        Ok(WaitResult {
120            output: output.stdout,
121            success: output.success,
122            exit_codes,
123        })
124    }
125
126    async fn execute(&self) -> Result<Self::Output> {
127        let args = self.build_args();
128        self.execute_compose(args).await
129    }
130}
131
132impl WaitResult {
133    /// Check if all services exited successfully (code 0)
134    #[must_use]
135    pub fn all_successful(&self) -> bool {
136        !self.exit_codes.is_empty() && self.exit_codes.iter().all(|&code| code == 0)
137    }
138
139    /// Get the first non-zero exit code
140    #[must_use]
141    pub fn first_failure(&self) -> Option<i32> {
142        self.exit_codes.iter().find(|&&code| code != 0).copied()
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_wait_command_basic() {
152        let cmd = ComposeWaitCommand::new();
153        let args = cmd.build_args();
154        assert_eq!(args[0], "wait");
155    }
156
157    #[test]
158    fn test_wait_command_with_services() {
159        let cmd = ComposeWaitCommand::new().service("web").service("db");
160        let args = cmd.build_args();
161        assert!(args.contains(&"web".to_string()));
162        assert!(args.contains(&"db".to_string()));
163    }
164
165    #[test]
166    fn test_wait_command_with_down_project() {
167        let cmd = ComposeWaitCommand::new().down_project();
168        let args = cmd.build_args();
169        assert!(args.contains(&"--down-project".to_string()));
170    }
171
172    #[test]
173    fn test_wait_result_helpers() {
174        let result = WaitResult {
175            output: String::new(),
176            success: true,
177            exit_codes: vec![0, 0, 0],
178        };
179        assert!(result.all_successful());
180        assert_eq!(result.first_failure(), None);
181
182        let result_with_failure = WaitResult {
183            output: String::new(),
184            success: false,
185            exit_codes: vec![0, 1, 0],
186        };
187        assert!(!result_with_failure.all_successful());
188        assert_eq!(result_with_failure.first_failure(), Some(1));
189    }
190}