docker_wrapper/command/compose/
scale.rs

1//! Docker Compose scale command implementation using unified trait pattern.
2
3use crate::command::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use std::collections::HashMap;
7
8/// Docker Compose scale command builder
9#[derive(Debug, Clone)]
10pub struct ComposeScaleCommand {
11    /// Base command executor
12    pub executor: CommandExecutor,
13    /// Base compose configuration
14    pub config: ComposeConfig,
15    /// Service scaling configurations (`service_name` -> replicas)
16    pub services: HashMap<String, u32>,
17    /// Don't start linked services
18    pub no_deps: bool,
19}
20
21/// Result from compose scale command
22#[derive(Debug, Clone)]
23pub struct ComposeScaleResult {
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 scaled
31    pub scaled_services: HashMap<String, u32>,
32}
33
34impl ComposeScaleCommand {
35    /// Create a new compose scale command
36    #[must_use]
37    pub fn new() -> Self {
38        Self {
39            executor: CommandExecutor::new(),
40            config: ComposeConfig::new(),
41            services: HashMap::new(),
42            no_deps: false,
43        }
44    }
45
46    /// Add a service to scale
47    #[must_use]
48    pub fn service(mut self, service: impl Into<String>, replicas: u32) -> Self {
49        self.services.insert(service.into(), replicas);
50        self
51    }
52
53    /// Add multiple services to scale
54    #[must_use]
55    pub fn services(mut self, services: HashMap<String, u32>) -> Self {
56        self.services.extend(services);
57        self
58    }
59
60    /// Don't start linked services
61    #[must_use]
62    pub fn no_deps(mut self) -> Self {
63        self.no_deps = true;
64        self
65    }
66}
67
68impl Default for ComposeScaleCommand {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74#[async_trait]
75impl DockerCommand for ComposeScaleCommand {
76    type Output = ComposeScaleResult;
77
78    fn get_executor(&self) -> &CommandExecutor {
79        &self.executor
80    }
81
82    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
83        &mut self.executor
84    }
85
86    fn build_command_args(&self) -> Vec<String> {
87        <Self as ComposeCommand>::build_command_args(self)
88    }
89
90    async fn execute(&self) -> Result<Self::Output> {
91        let args = <Self as ComposeCommand>::build_command_args(self);
92        let output = self.execute_command(args).await?;
93
94        Ok(ComposeScaleResult {
95            stdout: output.stdout,
96            stderr: output.stderr,
97            success: output.success,
98            scaled_services: self.services.clone(),
99        })
100    }
101}
102
103impl ComposeCommand for ComposeScaleCommand {
104    fn get_config(&self) -> &ComposeConfig {
105        &self.config
106    }
107
108    fn get_config_mut(&mut self) -> &mut ComposeConfig {
109        &mut self.config
110    }
111
112    fn subcommand(&self) -> &'static str {
113        "scale"
114    }
115
116    fn build_subcommand_args(&self) -> Vec<String> {
117        let mut args = Vec::new();
118
119        if self.no_deps {
120            args.push("--no-deps".to_string());
121        }
122
123        for (service, replicas) in &self.services {
124            args.push(format!("{service}={replicas}"));
125        }
126
127        args
128    }
129}
130
131impl ComposeScaleResult {
132    /// Check if the command was successful
133    #[must_use]
134    pub fn success(&self) -> bool {
135        self.success
136    }
137
138    /// Get the services that were scaled
139    #[must_use]
140    pub fn scaled_services(&self) -> &HashMap<String, u32> {
141        &self.scaled_services
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_compose_scale_basic() {
151        let cmd = ComposeScaleCommand::new().service("web", 3);
152        let args = cmd.build_subcommand_args();
153        assert!(args.contains(&"web=3".to_string()));
154
155        let full_args = ComposeCommand::build_command_args(&cmd);
156        assert_eq!(full_args[0], "compose");
157        assert!(full_args.contains(&"scale".to_string()));
158    }
159
160    #[test]
161    fn test_compose_scale_multiple_services() {
162        let cmd = ComposeScaleCommand::new()
163            .service("web", 3)
164            .service("worker", 2);
165
166        let args = cmd.build_subcommand_args();
167        assert!(args.contains(&"web=3".to_string()));
168        assert!(args.contains(&"worker=2".to_string()));
169    }
170
171    #[test]
172    fn test_compose_scale_no_deps() {
173        let cmd = ComposeScaleCommand::new().service("api", 5).no_deps();
174
175        let args = cmd.build_subcommand_args();
176        assert!(args.contains(&"--no-deps".to_string()));
177        assert!(args.contains(&"api=5".to_string()));
178    }
179}