1use super::{execute_compose_command, ComposeCommand, ComposeConfig, ComposeOutput};
4use crate::error::Result;
5use async_trait::async_trait;
6use std::time::Duration;
7
8#[derive(Debug, Clone)]
10#[allow(clippy::struct_excessive_bools)] pub struct ComposeUpCommand {
12 config: ComposeConfig,
14 services: Vec<String>,
16 detach: bool,
18 no_deps: bool,
20 force_recreate: bool,
22 always_recreate_deps: bool,
24 no_recreate: bool,
26 no_build: bool,
28 no_start: bool,
30 build: bool,
32 remove_orphans: bool,
34 scale: Vec<(String, u32)>,
36 timeout: Option<Duration>,
38 exit_code_from: Option<String>,
40 abort_on_container_exit: bool,
42 attach_dependencies: bool,
44 renew_anon_volumes: bool,
46 wait: bool,
48 wait_timeout: Option<Duration>,
50 pull: Option<PullPolicy>,
52}
53
54#[derive(Debug, Clone, Copy)]
56pub enum PullPolicy {
57 Always,
59 Never,
61 Missing,
63}
64
65impl std::fmt::Display for PullPolicy {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 Self::Always => write!(f, "always"),
69 Self::Never => write!(f, "never"),
70 Self::Missing => write!(f, "missing"),
71 }
72 }
73}
74
75impl ComposeUpCommand {
76 #[must_use]
78 pub fn new() -> Self {
79 Self {
80 config: ComposeConfig::new(),
81 services: Vec::new(),
82 detach: false,
83 no_deps: false,
84 force_recreate: false,
85 always_recreate_deps: false,
86 no_recreate: false,
87 no_build: false,
88 no_start: false,
89 build: false,
90 remove_orphans: false,
91 scale: Vec::new(),
92 timeout: None,
93 exit_code_from: None,
94 abort_on_container_exit: false,
95 attach_dependencies: false,
96 renew_anon_volumes: false,
97 wait: false,
98 wait_timeout: None,
99 pull: None,
100 }
101 }
102
103 #[must_use]
105 pub fn with_config(config: ComposeConfig) -> Self {
106 Self {
107 config,
108 ..Self::new()
109 }
110 }
111
112 #[must_use]
114 pub fn service(mut self, service: impl Into<String>) -> Self {
115 self.services.push(service.into());
116 self
117 }
118
119 #[must_use]
121 pub fn services<I, S>(mut self, services: I) -> Self
122 where
123 I: IntoIterator<Item = S>,
124 S: Into<String>,
125 {
126 self.services.extend(services.into_iter().map(Into::into));
127 self
128 }
129
130 #[must_use]
132 pub fn detach(mut self) -> Self {
133 self.detach = true;
134 self
135 }
136
137 #[must_use]
139 pub fn no_deps(mut self) -> Self {
140 self.no_deps = true;
141 self
142 }
143
144 #[must_use]
146 pub fn force_recreate(mut self) -> Self {
147 self.force_recreate = true;
148 self
149 }
150
151 #[must_use]
153 pub fn always_recreate_deps(mut self) -> Self {
154 self.always_recreate_deps = true;
155 self
156 }
157
158 #[must_use]
160 pub fn no_recreate(mut self) -> Self {
161 self.no_recreate = true;
162 self
163 }
164
165 #[must_use]
167 pub fn no_build(mut self) -> Self {
168 self.no_build = true;
169 self
170 }
171
172 #[must_use]
174 pub fn no_start(mut self) -> Self {
175 self.no_start = true;
176 self
177 }
178
179 #[must_use]
181 pub fn build(mut self) -> Self {
182 self.build = true;
183 self
184 }
185
186 #[must_use]
188 pub fn remove_orphans(mut self) -> Self {
189 self.remove_orphans = true;
190 self
191 }
192
193 #[must_use]
195 pub fn scale(mut self, service: impl Into<String>, instances: u32) -> Self {
196 self.scale.push((service.into(), instances));
197 self
198 }
199
200 #[must_use]
202 pub fn timeout(mut self, timeout: Duration) -> Self {
203 self.timeout = Some(timeout);
204 self
205 }
206
207 #[must_use]
209 pub fn exit_code_from(mut self, service: impl Into<String>) -> Self {
210 self.exit_code_from = Some(service.into());
211 self
212 }
213
214 #[must_use]
216 pub fn abort_on_container_exit(mut self) -> Self {
217 self.abort_on_container_exit = true;
218 self
219 }
220
221 #[must_use]
223 pub fn attach_dependencies(mut self) -> Self {
224 self.attach_dependencies = true;
225 self
226 }
227
228 #[must_use]
230 pub fn renew_anon_volumes(mut self) -> Self {
231 self.renew_anon_volumes = true;
232 self
233 }
234
235 #[must_use]
237 pub fn wait(mut self) -> Self {
238 self.wait = true;
239 self
240 }
241
242 #[must_use]
244 pub fn wait_timeout(mut self, timeout: Duration) -> Self {
245 self.wait_timeout = Some(timeout);
246 self
247 }
248
249 #[must_use]
251 pub fn pull(mut self, policy: PullPolicy) -> Self {
252 self.pull = Some(policy);
253 self
254 }
255
256 #[must_use]
258 pub fn file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
259 self.config = self.config.file(path);
260 self
261 }
262
263 #[must_use]
265 pub fn project_name(mut self, name: impl Into<String>) -> Self {
266 self.config = self.config.project_name(name);
267 self
268 }
269
270 pub async fn run(&self) -> Result<ComposeUpResult> {
279 let output = self.execute().await?;
280
281 Ok(ComposeUpResult {
282 output,
283 services: self.services.clone(),
284 detached: self.detach,
285 })
286 }
287}
288
289impl Default for ComposeUpCommand {
290 fn default() -> Self {
291 Self::new()
292 }
293}
294
295#[async_trait]
296impl ComposeCommand for ComposeUpCommand {
297 type Output = ComposeOutput;
298
299 fn subcommand(&self) -> &'static str {
300 "up"
301 }
302
303 fn build_args(&self) -> Vec<String> {
304 let mut args = Vec::new();
305
306 if self.detach {
307 args.push("--detach".to_string());
308 }
309
310 if self.no_deps {
311 args.push("--no-deps".to_string());
312 }
313
314 if self.force_recreate {
315 args.push("--force-recreate".to_string());
316 }
317
318 if self.always_recreate_deps {
319 args.push("--always-recreate-deps".to_string());
320 }
321
322 if self.no_recreate {
323 args.push("--no-recreate".to_string());
324 }
325
326 if self.no_build {
327 args.push("--no-build".to_string());
328 }
329
330 if self.no_start {
331 args.push("--no-start".to_string());
332 }
333
334 if self.build {
335 args.push("--build".to_string());
336 }
337
338 if self.remove_orphans {
339 args.push("--remove-orphans".to_string());
340 }
341
342 for (service, count) in &self.scale {
343 args.push("--scale".to_string());
344 args.push(format!("{service}={count}"));
345 }
346
347 if let Some(timeout) = self.timeout {
348 args.push("--timeout".to_string());
349 args.push(timeout.as_secs().to_string());
350 }
351
352 if let Some(ref service) = self.exit_code_from {
353 args.push("--exit-code-from".to_string());
354 args.push(service.clone());
355 }
356
357 if self.abort_on_container_exit {
358 args.push("--abort-on-container-exit".to_string());
359 }
360
361 if self.attach_dependencies {
362 args.push("--attach-dependencies".to_string());
363 }
364
365 if self.renew_anon_volumes {
366 args.push("--renew-anon-volumes".to_string());
367 }
368
369 if self.wait {
370 args.push("--wait".to_string());
371 }
372
373 if let Some(timeout) = self.wait_timeout {
374 args.push("--wait-timeout".to_string());
375 args.push(timeout.as_secs().to_string());
376 }
377
378 if let Some(ref pull) = self.pull {
379 args.push("--pull".to_string());
380 args.push(pull.to_string());
381 }
382
383 args.extend(self.services.clone());
385
386 args
387 }
388
389 async fn execute(&self) -> Result<Self::Output> {
390 execute_compose_command(&self.config, self.subcommand(), self.build_args()).await
391 }
392
393 fn config(&self) -> &ComposeConfig {
394 &self.config
395 }
396}
397
398#[derive(Debug, Clone)]
400pub struct ComposeUpResult {
401 pub output: ComposeOutput,
403 pub services: Vec<String>,
405 pub detached: bool,
407}
408
409impl ComposeUpResult {
410 #[must_use]
412 pub fn success(&self) -> bool {
413 self.output.success
414 }
415
416 #[must_use]
418 pub fn services(&self) -> &[String] {
419 &self.services
420 }
421
422 #[must_use]
424 pub fn is_detached(&self) -> bool {
425 self.detached
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn test_compose_up_basic() {
435 let cmd = ComposeUpCommand::new();
436 let args = cmd.build_args();
437 assert!(args.is_empty());
438 }
439
440 #[test]
441 fn test_compose_up_detached() {
442 let cmd = ComposeUpCommand::new().detach();
443 let args = cmd.build_args();
444 assert_eq!(args, vec!["--detach"]);
445 }
446
447 #[test]
448 fn test_compose_up_with_services() {
449 let cmd = ComposeUpCommand::new().service("web").service("db");
450 let args = cmd.build_args();
451 assert_eq!(args, vec!["web", "db"]);
452 }
453
454 #[test]
455 fn test_compose_up_all_options() {
456 let cmd = ComposeUpCommand::new()
457 .detach()
458 .build()
459 .remove_orphans()
460 .scale("web", 3)
461 .wait()
462 .pull(PullPolicy::Always)
463 .service("web")
464 .service("db");
465
466 let args = cmd.build_args();
467 assert!(args.contains(&"--detach".to_string()));
468 assert!(args.contains(&"--build".to_string()));
469 assert!(args.contains(&"--remove-orphans".to_string()));
470 assert!(args.contains(&"--scale".to_string()));
471 assert!(args.contains(&"web=3".to_string()));
472 assert!(args.contains(&"--wait".to_string()));
473 assert!(args.contains(&"--pull".to_string()));
474 assert!(args.contains(&"always".to_string()));
475 }
476
477 #[test]
478 fn test_pull_policy_display() {
479 assert_eq!(PullPolicy::Always.to_string(), "always");
480 assert_eq!(PullPolicy::Never.to_string(), "never");
481 assert_eq!(PullPolicy::Missing.to_string(), "missing");
482 }
483}