docker_wrapper/compose/
convert.rs1use crate::compose::{ComposeCommandV2 as ComposeCommand, ComposeConfig};
4use crate::error::Result;
5use async_trait::async_trait;
6
7#[derive(Debug, Clone, Default)]
11#[allow(clippy::struct_excessive_bools)]
12pub struct ComposeConvertCommand {
13 pub config: ComposeConfig,
15 pub format: Option<ConvertFormat>,
17 pub resolve_image_digests: bool,
19 pub no_interpolate: bool,
21 pub no_normalize: bool,
23 pub no_consistency: bool,
25 pub services: bool,
27 pub volumes: bool,
29 pub profiles: bool,
31 pub images: bool,
33 pub hash: Option<String>,
35 pub output: Option<String>,
37 pub quiet: bool,
39}
40
41#[derive(Debug, Clone, Copy)]
43pub enum ConvertFormat {
44 Yaml,
46 Json,
48}
49
50impl ConvertFormat {
51 #[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#[derive(Debug, Clone)]
63pub struct ConvertResult {
64 pub config: String,
66 pub success: bool,
68}
69
70impl ComposeConvertCommand {
71 #[must_use]
73 pub fn new() -> Self {
74 Self::default()
75 }
76
77 #[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 #[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 #[must_use]
93 pub fn format(mut self, format: ConvertFormat) -> Self {
94 self.format = Some(format);
95 self
96 }
97
98 #[must_use]
100 pub fn resolve_image_digests(mut self) -> Self {
101 self.resolve_image_digests = true;
102 self
103 }
104
105 #[must_use]
107 pub fn no_interpolate(mut self) -> Self {
108 self.no_interpolate = true;
109 self
110 }
111
112 #[must_use]
114 pub fn no_normalize(mut self) -> Self {
115 self.no_normalize = true;
116 self
117 }
118
119 #[must_use]
121 pub fn no_consistency(mut self) -> Self {
122 self.no_consistency = true;
123 self
124 }
125
126 #[must_use]
128 pub fn services(mut self) -> Self {
129 self.services = true;
130 self
131 }
132
133 #[must_use]
135 pub fn volumes(mut self) -> Self {
136 self.volumes = true;
137 self
138 }
139
140 #[must_use]
142 pub fn profiles(mut self) -> Self {
143 self.profiles = true;
144 self
145 }
146
147 #[must_use]
149 pub fn images(mut self) -> Self {
150 self.images = true;
151 self
152 }
153
154 #[must_use]
156 pub fn hash(mut self, hash: impl Into<String>) -> Self {
157 self.hash = Some(hash.into());
158 self
159 }
160
161 #[must_use]
163 pub fn output(mut self, path: impl Into<String>) -> Self {
164 self.output = Some(path.into());
165 self
166 }
167
168 #[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!["convert".to_string()];
177
178 if let Some(format) = &self.format {
180 args.push("--format".to_string());
181 args.push(format.as_arg().to_string());
182 }
183
184 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 if let Some(hash) = &self.hash {
215 args.push("--hash".to_string());
216 args.push(hash.clone());
217 }
218
219 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 ComposeConvertCommand {
231 type Output = ConvertResult;
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(ConvertResult {
245 config: output.stdout,
246 success: 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
256impl ConvertResult {
257 #[must_use]
259 pub fn is_json(&self) -> bool {
260 serde_json::from_str::<serde_json::Value>(&self.config).is_ok()
261 }
262
263 #[must_use]
265 pub fn is_yaml(&self) -> bool {
266 !self.config.is_empty() && (self.config.contains(':') || self.config.contains('-'))
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn test_convert_command_basic() {
276 let cmd = ComposeConvertCommand::new();
277 let args = cmd.build_args();
278 assert_eq!(args[0], "convert");
279 }
280
281 #[test]
282 fn test_convert_command_with_format() {
283 let cmd = ComposeConvertCommand::new().format(ConvertFormat::Json);
284 let args = cmd.build_args();
285 assert!(args.contains(&"--format".to_string()));
286 assert!(args.contains(&"json".to_string()));
287 }
288
289 #[test]
290 fn test_convert_command_with_flags() {
291 let cmd = ComposeConvertCommand::new()
292 .resolve_image_digests()
293 .no_interpolate()
294 .services()
295 .quiet();
296 let args = cmd.build_args();
297 assert!(args.contains(&"--resolve-image-digests".to_string()));
298 assert!(args.contains(&"--no-interpolate".to_string()));
299 assert!(args.contains(&"--services".to_string()));
300 assert!(args.contains(&"--quiet".to_string()));
301 }
302
303 #[test]
304 fn test_convert_command_with_output() {
305 let cmd = ComposeConvertCommand::new()
306 .output("docker-compose.json")
307 .format(ConvertFormat::Json);
308 let args = cmd.build_args();
309 assert!(args.contains(&"--output".to_string()));
310 assert!(args.contains(&"docker-compose.json".to_string()));
311 }
312
313 #[test]
314 fn test_convert_result_helpers() {
315 let json_result = ConvertResult {
316 config: r#"{"version": "3.8"}"#.to_string(),
317 success: true,
318 };
319 assert!(json_result.is_json());
320
321 let yaml_result = ConvertResult {
322 config: "version: '3.8'\nservices:\n web:\n image: nginx".to_string(),
323 success: true,
324 };
325 assert!(yaml_result.is_yaml());
326 }
327}