docker_wrapper/compose/
images.rs1use crate::compose::{ComposeCommandV2 as ComposeCommand, ComposeConfig};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::Deserialize;
7
8#[derive(Debug, Clone, Default)]
12pub struct ComposeImagesCommand {
13 pub config: ComposeConfig,
15 pub format: Option<ImagesFormat>,
17 pub quiet: bool,
19 pub services: Vec<String>,
21}
22
23#[derive(Debug, Clone, Copy)]
25pub enum ImagesFormat {
26 Table,
28 Json,
30}
31
32impl ImagesFormat {
33 #[must_use]
35 pub fn as_arg(&self) -> &str {
36 match self {
37 Self::Table => "table",
38 Self::Json => "json",
39 }
40 }
41}
42
43#[derive(Debug, Clone, Deserialize)]
45#[serde(rename_all = "PascalCase")]
46pub struct ImageInfo {
47 pub container: String,
49 pub repository: String,
51 pub tag: String,
53 pub image_id: String,
55 pub size: String,
57}
58
59#[derive(Debug, Clone)]
61pub struct ImagesResult {
62 pub images: Vec<ImageInfo>,
64 pub raw_output: String,
66}
67
68impl ComposeImagesCommand {
69 #[must_use]
71 pub fn new() -> Self {
72 Self::default()
73 }
74
75 #[must_use]
77 pub fn file<P: Into<std::path::PathBuf>>(mut self, file: P) -> Self {
78 self.config.files.push(file.into());
79 self
80 }
81
82 #[must_use]
84 pub fn project_name(mut self, name: impl Into<String>) -> Self {
85 self.config.project_name = Some(name.into());
86 self
87 }
88
89 #[must_use]
91 pub fn format(mut self, format: ImagesFormat) -> Self {
92 self.format = Some(format);
93 self
94 }
95
96 #[must_use]
98 pub fn format_json(mut self) -> Self {
99 self.format = Some(ImagesFormat::Json);
100 self
101 }
102
103 #[must_use]
105 pub fn quiet(mut self) -> Self {
106 self.quiet = true;
107 self
108 }
109
110 #[must_use]
112 pub fn service(mut self, service: impl Into<String>) -> Self {
113 self.services.push(service.into());
114 self
115 }
116
117 #[must_use]
119 pub fn services<I, S>(mut self, services: I) -> Self
120 where
121 I: IntoIterator<Item = S>,
122 S: Into<String>,
123 {
124 self.services.extend(services.into_iter().map(Into::into));
125 self
126 }
127
128 fn build_args(&self) -> Vec<String> {
129 let mut args = vec!["images".to_string()];
130
131 if self.quiet {
133 args.push("--quiet".to_string());
134 }
135
136 if let Some(format) = &self.format {
138 args.push("--format".to_string());
139 args.push(format.as_arg().to_string());
140 }
141
142 args.extend(self.services.clone());
144
145 args
146 }
147}
148
149#[async_trait]
150impl ComposeCommand for ComposeImagesCommand {
151 type Output = ImagesResult;
152
153 fn get_config(&self) -> &ComposeConfig {
154 &self.config
155 }
156
157 fn get_config_mut(&mut self) -> &mut ComposeConfig {
158 &mut self.config
159 }
160
161 async fn execute_compose(&self, args: Vec<String>) -> Result<Self::Output> {
162 let output = self.execute_compose_command(args).await?;
163
164 let images = if matches!(self.format, Some(ImagesFormat::Json)) {
166 serde_json::from_str(&output.stdout).unwrap_or_default()
167 } else {
168 Vec::new()
169 };
170
171 Ok(ImagesResult {
172 images,
173 raw_output: output.stdout,
174 })
175 }
176
177 async fn execute(&self) -> Result<Self::Output> {
178 let args = self.build_args();
179 self.execute_compose(args).await
180 }
181}
182
183impl ImagesResult {
184 #[must_use]
186 pub fn unique_images(&self) -> Vec<String> {
187 let mut images: Vec<_> = self
188 .images
189 .iter()
190 .map(|img| format!("{}:{}", img.repository, img.tag))
191 .collect();
192 images.sort();
193 images.dedup();
194 images
195 }
196
197 #[must_use]
199 pub fn total_size(&self) -> String {
200 "N/A".to_string()
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_images_command_basic() {
212 let cmd = ComposeImagesCommand::new();
213 let args = cmd.build_args();
214 assert_eq!(args[0], "images");
215 }
216
217 #[test]
218 fn test_images_command_with_format() {
219 let cmd = ComposeImagesCommand::new().format_json();
220 let args = cmd.build_args();
221 assert!(args.contains(&"--format".to_string()));
222 assert!(args.contains(&"json".to_string()));
223 }
224
225 #[test]
226 fn test_images_command_with_quiet() {
227 let cmd = ComposeImagesCommand::new().quiet();
228 let args = cmd.build_args();
229 assert!(args.contains(&"--quiet".to_string()));
230 }
231
232 #[test]
233 fn test_images_command_with_services() {
234 let cmd = ComposeImagesCommand::new().service("web").service("db");
235 let args = cmd.build_args();
236 assert!(args.contains(&"web".to_string()));
237 assert!(args.contains(&"db".to_string()));
238 }
239}