docker_wrapper/compose/
config.rs

1//! Docker Compose config command implementation.
2
3use crate::compose::{ComposeCommandV2 as ComposeCommand, ComposeConfig};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Docker Compose config command
8///
9/// Validates and displays the Compose configuration.
10#[derive(Debug, Clone, Default)]
11#[allow(clippy::struct_excessive_bools)]
12pub struct ComposeConfigCommand {
13    /// Base configuration
14    pub config: ComposeConfig,
15    /// Format output
16    pub format: Option<ConfigFormat>,
17    /// Resolve image digests
18    pub resolve_image_digests: bool,
19    /// Don't interpolate environment
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
26    pub services: bool,
27    /// Show volumes
28    pub volumes: bool,
29    /// Show profiles
30    pub profiles: bool,
31    /// Show images
32    pub images: bool,
33    /// Hash of services to include
34    pub hash: Option<String>,
35    /// Output file
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 ConfigFormat {
51    /// Convert to command line argument
52    #[must_use]
53    pub fn as_arg(&self) -> &str {
54        match self {
55            Self::Yaml => "yaml",
56            Self::Json => "json",
57        }
58    }
59}
60
61/// Result from config command
62#[derive(Debug, Clone)]
63pub struct ConfigResult {
64    /// The configuration output (YAML or JSON)
65    pub config: String,
66    /// Whether the config is valid
67    pub is_valid: bool,
68}
69
70impl ComposeConfigCommand {
71    /// Create a new config command
72    #[must_use]
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// Add a compose file
78    #[must_use]
79    pub fn file<P: Into<std::path::PathBuf>>(mut self, file: P) -> Self {
80        self.config.files.push(file.into());
81        self
82    }
83
84    /// Set project name
85    #[must_use]
86    pub fn project_name(mut self, name: impl Into<String>) -> Self {
87        self.config.project_name = Some(name.into());
88        self
89    }
90
91    /// Set output format
92    #[must_use]
93    pub fn format(mut self, format: ConfigFormat) -> Self {
94        self.format = Some(format);
95        self
96    }
97
98    /// Resolve image digests
99    #[must_use]
100    pub fn resolve_image_digests(mut self) -> Self {
101        self.resolve_image_digests = true;
102        self
103    }
104
105    /// Don't interpolate environment
106    #[must_use]
107    pub fn no_interpolate(mut self) -> Self {
108        self.no_interpolate = true;
109        self
110    }
111
112    /// Don't normalize paths
113    #[must_use]
114    pub fn no_normalize(mut self) -> Self {
115        self.no_normalize = true;
116        self
117    }
118
119    /// Don't check consistency
120    #[must_use]
121    pub fn no_consistency(mut self) -> Self {
122        self.no_consistency = true;
123        self
124    }
125
126    /// Show services only
127    #[must_use]
128    pub fn services(mut self) -> Self {
129        self.services = true;
130        self
131    }
132
133    /// Show volumes only
134    #[must_use]
135    pub fn volumes(mut self) -> Self {
136        self.volumes = true;
137        self
138    }
139
140    /// Show profiles only
141    #[must_use]
142    pub fn profiles(mut self) -> Self {
143        self.profiles = true;
144        self
145    }
146
147    /// Show images only
148    #[must_use]
149    pub fn images(mut self) -> Self {
150        self.images = true;
151        self
152    }
153
154    /// Set services hash
155    #[must_use]
156    pub fn hash(mut self, hash: impl Into<String>) -> Self {
157        self.hash = Some(hash.into());
158        self
159    }
160
161    /// Set output file
162    #[must_use]
163    pub fn output(mut self, path: impl Into<String>) -> Self {
164        self.output = Some(path.into());
165        self
166    }
167
168    /// Enable quiet mode
169    #[must_use]
170    pub fn quiet(mut self) -> Self {
171        self.quiet = true;
172        self
173    }
174
175    fn build_args(&self) -> Vec<String> {
176        let mut args = vec!["config".to_string()];
177
178        // Add format
179        if let Some(format) = &self.format {
180            args.push("--format".to_string());
181            args.push(format.as_arg().to_string());
182        }
183
184        // Add flags
185        if self.resolve_image_digests {
186            args.push("--resolve-image-digests".to_string());
187        }
188        if self.no_interpolate {
189            args.push("--no-interpolate".to_string());
190        }
191        if self.no_normalize {
192            args.push("--no-normalize".to_string());
193        }
194        if self.no_consistency {
195            args.push("--no-consistency".to_string());
196        }
197        if self.services {
198            args.push("--services".to_string());
199        }
200        if self.volumes {
201            args.push("--volumes".to_string());
202        }
203        if self.profiles {
204            args.push("--profiles".to_string());
205        }
206        if self.images {
207            args.push("--images".to_string());
208        }
209        if self.quiet {
210            args.push("--quiet".to_string());
211        }
212
213        // Add hash
214        if let Some(hash) = &self.hash {
215            args.push("--hash".to_string());
216            args.push(hash.clone());
217        }
218
219        // Add output
220        if let Some(output) = &self.output {
221            args.push("--output".to_string());
222            args.push(output.clone());
223        }
224
225        args
226    }
227}
228
229#[async_trait]
230impl ComposeCommand for ComposeConfigCommand {
231    type Output = ConfigResult;
232
233    fn get_config(&self) -> &ComposeConfig {
234        &self.config
235    }
236
237    fn get_config_mut(&mut self) -> &mut ComposeConfig {
238        &mut self.config
239    }
240
241    async fn execute_compose(&self, args: Vec<String>) -> Result<Self::Output> {
242        let output = self.execute_compose_command(args).await?;
243
244        Ok(ConfigResult {
245            config: output.stdout,
246            is_valid: output.success,
247        })
248    }
249
250    async fn execute(&self) -> Result<Self::Output> {
251        let args = self.build_args();
252        self.execute_compose(args).await
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_config_command_basic() {
262        let cmd = ComposeConfigCommand::new();
263        let args = cmd.build_args();
264        assert_eq!(args[0], "config");
265    }
266
267    #[test]
268    fn test_config_command_with_format() {
269        let cmd = ComposeConfigCommand::new().format(ConfigFormat::Json);
270        let args = cmd.build_args();
271        assert!(args.contains(&"--format".to_string()));
272        assert!(args.contains(&"json".to_string()));
273    }
274
275    #[test]
276    fn test_config_command_with_flags() {
277        let cmd = ComposeConfigCommand::new()
278            .resolve_image_digests()
279            .no_interpolate()
280            .services()
281            .quiet();
282        let args = cmd.build_args();
283        assert!(args.contains(&"--resolve-image-digests".to_string()));
284        assert!(args.contains(&"--no-interpolate".to_string()));
285        assert!(args.contains(&"--services".to_string()));
286        assert!(args.contains(&"--quiet".to_string()));
287    }
288}