docker_wrapper/command/compose/
config.rs

1//! Docker Compose config command implementation using unified trait pattern.
2
3use crate::command::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Docker Compose config command builder
8#[derive(Debug, Clone)]
9#[allow(clippy::struct_excessive_bools)] // Multiple boolean flags are appropriate for config command
10pub struct ComposeConfigCommand {
11    /// Base command executor
12    pub executor: CommandExecutor,
13    /// Base compose configuration
14    pub config: ComposeConfig,
15    /// Output format
16    pub format: Option<ConfigFormat>,
17    /// Resolve image digests
18    pub resolve_image_digests: bool,
19    /// Don't interpolate environment variables
20    pub no_interpolate: bool,
21    /// Don't normalize paths
22    pub no_normalize: bool,
23    /// Don't check consistency
24    pub no_consistency: bool,
25    /// Show services only
26    pub services: bool,
27    /// Show volumes only
28    pub volumes: bool,
29    /// Show profiles only
30    pub profiles: bool,
31    /// Show images only
32    pub images: bool,
33    /// Hash of services to include
34    pub hash: Option<String>,
35    /// Output file path
36    pub output: Option<String>,
37    /// Quiet mode
38    pub quiet: bool,
39}
40
41/// Config output format
42#[derive(Debug, Clone, Copy)]
43pub enum ConfigFormat {
44    /// YAML format (default)
45    Yaml,
46    /// JSON format
47    Json,
48}
49
50impl std::fmt::Display for ConfigFormat {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        match self {
53            Self::Yaml => write!(f, "yaml"),
54            Self::Json => write!(f, "json"),
55        }
56    }
57}
58
59/// Result from compose config command
60#[derive(Debug, Clone)]
61pub struct ComposeConfigResult {
62    /// Raw stdout output (configuration YAML/JSON)
63    pub stdout: String,
64    /// Raw stderr output
65    pub stderr: String,
66    /// Success status
67    pub success: bool,
68    /// Whether configuration is valid
69    pub is_valid: bool,
70}
71
72impl ComposeConfigCommand {
73    /// Create a new compose config command
74    #[must_use]
75    pub fn new() -> Self {
76        Self {
77            executor: CommandExecutor::new(),
78            config: ComposeConfig::new(),
79            format: None,
80            resolve_image_digests: false,
81            no_interpolate: false,
82            no_normalize: false,
83            no_consistency: false,
84            services: false,
85            volumes: false,
86            profiles: false,
87            images: false,
88            hash: None,
89            output: None,
90            quiet: false,
91        }
92    }
93
94    /// Set output format
95    #[must_use]
96    pub fn format(mut self, format: ConfigFormat) -> Self {
97        self.format = Some(format);
98        self
99    }
100
101    /// Set output format to JSON
102    #[must_use]
103    pub fn format_json(mut self) -> Self {
104        self.format = Some(ConfigFormat::Json);
105        self
106    }
107
108    /// Set output format to YAML
109    #[must_use]
110    pub fn format_yaml(mut self) -> Self {
111        self.format = Some(ConfigFormat::Yaml);
112        self
113    }
114
115    /// Resolve image digests
116    #[must_use]
117    pub fn resolve_image_digests(mut self) -> Self {
118        self.resolve_image_digests = true;
119        self
120    }
121
122    /// Don't interpolate environment variables
123    #[must_use]
124    pub fn no_interpolate(mut self) -> Self {
125        self.no_interpolate = true;
126        self
127    }
128
129    /// Don't normalize paths
130    #[must_use]
131    pub fn no_normalize(mut self) -> Self {
132        self.no_normalize = true;
133        self
134    }
135
136    /// Don't check consistency
137    #[must_use]
138    pub fn no_consistency(mut self) -> Self {
139        self.no_consistency = true;
140        self
141    }
142
143    /// Show services only
144    #[must_use]
145    pub fn services(mut self) -> Self {
146        self.services = true;
147        self
148    }
149
150    /// Show volumes only
151    #[must_use]
152    pub fn volumes(mut self) -> Self {
153        self.volumes = true;
154        self
155    }
156
157    /// Show profiles only
158    #[must_use]
159    pub fn profiles(mut self) -> Self {
160        self.profiles = true;
161        self
162    }
163
164    /// Show images only
165    #[must_use]
166    pub fn images(mut self) -> Self {
167        self.images = true;
168        self
169    }
170
171    /// Set hash of services to include
172    #[must_use]
173    pub fn hash(mut self, hash: impl Into<String>) -> Self {
174        self.hash = Some(hash.into());
175        self
176    }
177
178    /// Set output file path
179    #[must_use]
180    pub fn output(mut self, output: impl Into<String>) -> Self {
181        self.output = Some(output.into());
182        self
183    }
184
185    /// Enable quiet mode
186    #[must_use]
187    pub fn quiet(mut self) -> Self {
188        self.quiet = true;
189        self
190    }
191}
192
193impl Default for ComposeConfigCommand {
194    fn default() -> Self {
195        Self::new()
196    }
197}
198
199#[async_trait]
200impl DockerCommand for ComposeConfigCommand {
201    type Output = ComposeConfigResult;
202
203    fn get_executor(&self) -> &CommandExecutor {
204        &self.executor
205    }
206
207    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
208        &mut self.executor
209    }
210
211    fn build_command_args(&self) -> Vec<String> {
212        // Use the ComposeCommand implementation explicitly
213        <Self as ComposeCommand>::build_command_args(self)
214    }
215
216    async fn execute(&self) -> Result<Self::Output> {
217        let args = <Self as ComposeCommand>::build_command_args(self);
218        let output = self.execute_command(args).await?;
219
220        Ok(ComposeConfigResult {
221            stdout: output.stdout.clone(),
222            stderr: output.stderr,
223            success: output.success,
224            is_valid: output.success && !output.stdout.is_empty(),
225        })
226    }
227}
228
229impl ComposeCommand for ComposeConfigCommand {
230    fn get_config(&self) -> &ComposeConfig {
231        &self.config
232    }
233
234    fn get_config_mut(&mut self) -> &mut ComposeConfig {
235        &mut self.config
236    }
237
238    fn subcommand(&self) -> &'static str {
239        "config"
240    }
241
242    fn build_subcommand_args(&self) -> Vec<String> {
243        let mut args = Vec::new();
244
245        if let Some(format) = self.format {
246            args.push("--format".to_string());
247            args.push(format.to_string());
248        }
249
250        if self.resolve_image_digests {
251            args.push("--resolve-image-digests".to_string());
252        }
253
254        if self.no_interpolate {
255            args.push("--no-interpolate".to_string());
256        }
257
258        if self.no_normalize {
259            args.push("--no-normalize".to_string());
260        }
261
262        if self.no_consistency {
263            args.push("--no-consistency".to_string());
264        }
265
266        if self.services {
267            args.push("--services".to_string());
268        }
269
270        if self.volumes {
271            args.push("--volumes".to_string());
272        }
273
274        if self.profiles {
275            args.push("--profiles".to_string());
276        }
277
278        if self.images {
279            args.push("--images".to_string());
280        }
281
282        if let Some(ref hash) = self.hash {
283            args.push("--hash".to_string());
284            args.push(hash.clone());
285        }
286
287        if let Some(ref output) = self.output {
288            args.push("--output".to_string());
289            args.push(output.clone());
290        }
291
292        if self.quiet {
293            args.push("--quiet".to_string());
294        }
295
296        args
297    }
298}
299
300impl ComposeConfigResult {
301    /// Check if the command was successful
302    #[must_use]
303    pub fn success(&self) -> bool {
304        self.success
305    }
306
307    /// Check if the configuration is valid
308    #[must_use]
309    pub fn is_valid(&self) -> bool {
310        self.is_valid
311    }
312
313    /// Get the configuration output
314    #[must_use]
315    pub fn config_output(&self) -> &str {
316        &self.stdout
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn test_compose_config_basic() {
326        let cmd = ComposeConfigCommand::new();
327        let args = cmd.build_subcommand_args();
328        assert!(args.is_empty());
329
330        let full_args = ComposeCommand::build_command_args(&cmd);
331        assert_eq!(full_args[0], "compose");
332        assert!(full_args.contains(&"config".to_string()));
333    }
334
335    #[test]
336    fn test_compose_config_with_format() {
337        let cmd = ComposeConfigCommand::new().format_json();
338        let args = cmd.build_subcommand_args();
339        assert!(args.contains(&"--format".to_string()));
340        assert!(args.contains(&"json".to_string()));
341    }
342
343    #[test]
344    fn test_compose_config_with_flags() {
345        let cmd = ComposeConfigCommand::new()
346            .resolve_image_digests()
347            .no_interpolate()
348            .services()
349            .quiet();
350
351        let args = cmd.build_subcommand_args();
352        assert!(args.contains(&"--resolve-image-digests".to_string()));
353        assert!(args.contains(&"--no-interpolate".to_string()));
354        assert!(args.contains(&"--services".to_string()));
355        assert!(args.contains(&"--quiet".to_string()));
356    }
357
358    #[test]
359    fn test_compose_config_show_options() {
360        let cmd = ComposeConfigCommand::new().volumes().profiles().images();
361
362        let args = cmd.build_subcommand_args();
363        assert!(args.contains(&"--volumes".to_string()));
364        assert!(args.contains(&"--profiles".to_string()));
365        assert!(args.contains(&"--images".to_string()));
366    }
367
368    #[test]
369    fn test_compose_config_with_hash_and_output() {
370        let cmd = ComposeConfigCommand::new()
371            .hash("web=sha256:123")
372            .output("output.yml");
373
374        let args = cmd.build_subcommand_args();
375        assert!(args.contains(&"--hash".to_string()));
376        assert!(args.contains(&"web=sha256:123".to_string()));
377        assert!(args.contains(&"--output".to_string()));
378        assert!(args.contains(&"output.yml".to_string()));
379    }
380
381    #[test]
382    fn test_config_format_display() {
383        assert_eq!(ConfigFormat::Yaml.to_string(), "yaml");
384        assert_eq!(ConfigFormat::Json.to_string(), "json");
385    }
386
387    #[test]
388    fn test_compose_config_integration() {
389        let cmd = ComposeConfigCommand::new()
390            .file("docker-compose.yml")
391            .project_name("myapp")
392            .format_json()
393            .services();
394
395        let args = ComposeCommand::build_command_args(&cmd);
396        assert!(args.contains(&"--file".to_string()));
397        assert!(args.contains(&"docker-compose.yml".to_string()));
398        assert!(args.contains(&"--project-name".to_string()));
399        assert!(args.contains(&"myapp".to_string()));
400        assert!(args.contains(&"--format".to_string()));
401        assert!(args.contains(&"json".to_string()));
402        assert!(args.contains(&"--services".to_string()));
403    }
404}