docker_wrapper/compose/
wait.rs1use crate::compose::{ComposeCommandV2 as ComposeCommand, ComposeConfig};
4use crate::error::Result;
5use async_trait::async_trait;
6
7#[derive(Debug, Clone, Default)]
11pub struct ComposeWaitCommand {
12 pub config: ComposeConfig,
14 pub services: Vec<String>,
16 pub down_project: bool,
18}
19
20#[derive(Debug, Clone)]
22pub struct WaitResult {
23 pub output: String,
25 pub success: bool,
27 pub exit_codes: Vec<i32>,
29}
30
31impl ComposeWaitCommand {
32 #[must_use]
34 pub fn new() -> Self {
35 Self::default()
36 }
37
38 #[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 #[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 #[must_use]
54 pub fn down_project(mut self) -> Self {
55 self.down_project = true;
56 self
57 }
58
59 #[must_use]
61 pub fn service(mut self, service: impl Into<String>) -> Self {
62 self.services.push(service.into());
63 self
64 }
65
66 #[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 if self.down_project {
82 args.push("--down-project".to_string());
83 }
84
85 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 let exit_codes = output
109 .stdout
110 .lines()
111 .filter_map(|line| {
112 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 #[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 #[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}