docker_wrapper/command/
compose_create.rs

1//! Docker Compose create 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 create command builder
8#[derive(Debug, Clone)]
9#[allow(clippy::struct_excessive_bools)] // Multiple boolean flags are appropriate for create command
10pub struct ComposeCreateCommand {
11    /// Base command executor
12    pub executor: CommandExecutor,
13    /// Base compose configuration
14    pub config: ComposeConfig,
15    /// Build images before creating containers
16    pub build: bool,
17    /// Don't build images, even if missing
18    pub no_build: bool,
19    /// Force recreate containers
20    pub force_recreate: bool,
21    /// Don't recreate containers if they exist
22    pub no_recreate: bool,
23    /// Pull images before creating
24    pub pull: Option<PullPolicy>,
25    /// Remove orphaned containers
26    pub remove_orphans: bool,
27    /// Services to create (empty for all)
28    pub services: Vec<String>,
29}
30
31/// Pull policy for images
32#[derive(Debug, Clone, Copy)]
33pub enum PullPolicy {
34    /// Always pull images
35    Always,
36    /// Never pull images
37    Never,
38    /// Pull missing images (default)
39    Missing,
40    /// Pull images if local is older
41    Build,
42}
43
44impl std::fmt::Display for PullPolicy {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match self {
47            Self::Always => write!(f, "always"),
48            Self::Never => write!(f, "never"),
49            Self::Missing => write!(f, "missing"),
50            Self::Build => write!(f, "build"),
51        }
52    }
53}
54
55/// Result from compose create command
56#[derive(Debug, Clone)]
57pub struct ComposeCreateResult {
58    /// Raw stdout output
59    pub stdout: String,
60    /// Raw stderr output
61    pub stderr: String,
62    /// Success status
63    pub success: bool,
64    /// Services that were created
65    pub services: Vec<String>,
66}
67
68impl ComposeCreateCommand {
69    /// Create a new compose create command
70    #[must_use]
71    pub fn new() -> Self {
72        Self {
73            executor: CommandExecutor::new(),
74            config: ComposeConfig::new(),
75            build: false,
76            no_build: false,
77            force_recreate: false,
78            no_recreate: false,
79            pull: None,
80            remove_orphans: false,
81            services: Vec::new(),
82        }
83    }
84
85    /// Build images before creating containers
86    #[must_use]
87    pub fn build(mut self) -> Self {
88        self.build = true;
89        self
90    }
91
92    /// Don't build images, even if missing
93    #[must_use]
94    pub fn no_build(mut self) -> Self {
95        self.no_build = true;
96        self
97    }
98
99    /// Force recreate containers
100    #[must_use]
101    pub fn force_recreate(mut self) -> Self {
102        self.force_recreate = true;
103        self
104    }
105
106    /// Don't recreate containers if they exist
107    #[must_use]
108    pub fn no_recreate(mut self) -> Self {
109        self.no_recreate = true;
110        self
111    }
112
113    /// Set pull policy
114    #[must_use]
115    pub fn pull(mut self, policy: PullPolicy) -> Self {
116        self.pull = Some(policy);
117        self
118    }
119
120    /// Remove orphaned containers
121    #[must_use]
122    pub fn remove_orphans(mut self) -> Self {
123        self.remove_orphans = true;
124        self
125    }
126
127    /// Add a service to create
128    #[must_use]
129    pub fn service(mut self, service: impl Into<String>) -> Self {
130        self.services.push(service.into());
131        self
132    }
133
134    /// Add multiple services to create
135    #[must_use]
136    pub fn services<I, S>(mut self, services: I) -> Self
137    where
138        I: IntoIterator<Item = S>,
139        S: Into<String>,
140    {
141        self.services.extend(services.into_iter().map(Into::into));
142        self
143    }
144}
145
146impl Default for ComposeCreateCommand {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152#[async_trait]
153impl DockerCommand for ComposeCreateCommand {
154    type Output = ComposeCreateResult;
155
156    fn get_executor(&self) -> &CommandExecutor {
157        &self.executor
158    }
159
160    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
161        &mut self.executor
162    }
163
164    fn build_command_args(&self) -> Vec<String> {
165        // Use the ComposeCommand implementation explicitly
166        <Self as ComposeCommand>::build_command_args(self)
167    }
168
169    async fn execute(&self) -> Result<Self::Output> {
170        let args = <Self as ComposeCommand>::build_command_args(self);
171        let output = self.execute_command(args).await?;
172
173        Ok(ComposeCreateResult {
174            stdout: output.stdout,
175            stderr: output.stderr,
176            success: output.success,
177            services: self.services.clone(),
178        })
179    }
180}
181
182impl ComposeCommand for ComposeCreateCommand {
183    fn get_config(&self) -> &ComposeConfig {
184        &self.config
185    }
186
187    fn get_config_mut(&mut self) -> &mut ComposeConfig {
188        &mut self.config
189    }
190
191    fn subcommand(&self) -> &'static str {
192        "create"
193    }
194
195    fn build_subcommand_args(&self) -> Vec<String> {
196        let mut args = Vec::new();
197
198        if self.build {
199            args.push("--build".to_string());
200        }
201
202        if self.no_build {
203            args.push("--no-build".to_string());
204        }
205
206        if self.force_recreate {
207            args.push("--force-recreate".to_string());
208        }
209
210        if self.no_recreate {
211            args.push("--no-recreate".to_string());
212        }
213
214        if self.remove_orphans {
215            args.push("--remove-orphans".to_string());
216        }
217
218        // Add pull policy
219        if let Some(pull) = self.pull {
220            args.push("--pull".to_string());
221            args.push(pull.to_string());
222        }
223
224        // Add service names at the end
225        args.extend(self.services.clone());
226
227        args
228    }
229}
230
231impl ComposeCreateResult {
232    /// Check if the command was successful
233    #[must_use]
234    pub fn success(&self) -> bool {
235        self.success
236    }
237
238    /// Get the services that were created
239    #[must_use]
240    pub fn services(&self) -> &[String] {
241        &self.services
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn test_compose_create_basic() {
251        let cmd = ComposeCreateCommand::new();
252        let args = cmd.build_subcommand_args();
253        assert!(args.is_empty());
254
255        let full_args = ComposeCommand::build_command_args(&cmd);
256        assert_eq!(full_args[0], "compose");
257        assert!(full_args.contains(&"create".to_string()));
258    }
259
260    #[test]
261    fn test_compose_create_with_build() {
262        let cmd = ComposeCreateCommand::new().build().force_recreate();
263        let args = cmd.build_subcommand_args();
264        assert!(args.contains(&"--build".to_string()));
265        assert!(args.contains(&"--force-recreate".to_string()));
266    }
267
268    #[test]
269    fn test_compose_create_with_pull() {
270        let cmd = ComposeCreateCommand::new()
271            .pull(PullPolicy::Always)
272            .no_recreate();
273        let args = cmd.build_subcommand_args();
274        assert!(args.contains(&"--pull".to_string()));
275        assert!(args.contains(&"always".to_string()));
276        assert!(args.contains(&"--no-recreate".to_string()));
277    }
278
279    #[test]
280    fn test_compose_create_with_services() {
281        let cmd = ComposeCreateCommand::new()
282            .service("web")
283            .service("db")
284            .remove_orphans();
285        let args = cmd.build_subcommand_args();
286        assert!(args.contains(&"web".to_string()));
287        assert!(args.contains(&"db".to_string()));
288        assert!(args.contains(&"--remove-orphans".to_string()));
289    }
290
291    #[test]
292    fn test_pull_policy_display() {
293        assert_eq!(PullPolicy::Always.to_string(), "always");
294        assert_eq!(PullPolicy::Never.to_string(), "never");
295        assert_eq!(PullPolicy::Missing.to_string(), "missing");
296        assert_eq!(PullPolicy::Build.to_string(), "build");
297    }
298
299    #[test]
300    fn test_compose_config_integration() {
301        let cmd = ComposeCreateCommand::new()
302            .file("docker-compose.yml")
303            .project_name("my-project")
304            .build()
305            .service("web");
306
307        let args = ComposeCommand::build_command_args(&cmd);
308        assert!(args.contains(&"--file".to_string()));
309        assert!(args.contains(&"docker-compose.yml".to_string()));
310        assert!(args.contains(&"--project-name".to_string()));
311        assert!(args.contains(&"my-project".to_string()));
312        assert!(args.contains(&"--build".to_string()));
313        assert!(args.contains(&"web".to_string()));
314    }
315}