docker_wrapper/compose/
images.rs

1//! Docker Compose images command implementation.
2
3use crate::compose::{ComposeCommandV2 as ComposeCommand, ComposeConfig};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::Deserialize;
7
8/// Docker Compose images command
9///
10/// List images used by services.
11#[derive(Debug, Clone, Default)]
12pub struct ComposeImagesCommand {
13    /// Base configuration
14    pub config: ComposeConfig,
15    /// Format output (table, json)
16    pub format: Option<ImagesFormat>,
17    /// Only display image IDs
18    pub quiet: bool,
19    /// Services to list images for
20    pub services: Vec<String>,
21}
22
23/// Images output format
24#[derive(Debug, Clone, Copy)]
25pub enum ImagesFormat {
26    /// Table format (default)
27    Table,
28    /// JSON format
29    Json,
30}
31
32impl ImagesFormat {
33    /// Convert to command line argument
34    #[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/// Image information
44#[derive(Debug, Clone, Deserialize)]
45#[serde(rename_all = "PascalCase")]
46pub struct ImageInfo {
47    /// Container name
48    pub container: String,
49    /// Repository
50    pub repository: String,
51    /// Tag
52    pub tag: String,
53    /// Image ID
54    pub image_id: String,
55    /// Size
56    pub size: String,
57}
58
59/// Result from images command
60#[derive(Debug, Clone)]
61pub struct ImagesResult {
62    /// List of images
63    pub images: Vec<ImageInfo>,
64    /// Raw output (for non-JSON formats)
65    pub raw_output: String,
66}
67
68impl ComposeImagesCommand {
69    /// Create a new images command
70    #[must_use]
71    pub fn new() -> Self {
72        Self::default()
73    }
74
75    /// Add a compose file
76    #[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    /// Set project name
83    #[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    /// Set output format
90    #[must_use]
91    pub fn format(mut self, format: ImagesFormat) -> Self {
92        self.format = Some(format);
93        self
94    }
95
96    /// Set output format to JSON
97    #[must_use]
98    pub fn format_json(mut self) -> Self {
99        self.format = Some(ImagesFormat::Json);
100        self
101    }
102
103    /// Only display image IDs
104    #[must_use]
105    pub fn quiet(mut self) -> Self {
106        self.quiet = true;
107        self
108    }
109
110    /// Add a service to list images for
111    #[must_use]
112    pub fn service(mut self, service: impl Into<String>) -> Self {
113        self.services.push(service.into());
114        self
115    }
116
117    /// Add multiple services
118    #[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        // Add flags
132        if self.quiet {
133            args.push("--quiet".to_string());
134        }
135
136        // Add format
137        if let Some(format) = &self.format {
138            args.push("--format".to_string());
139            args.push(format.as_arg().to_string());
140        }
141
142        // Add services
143        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        // Parse JSON output if format is JSON
165        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    /// Get unique images
185    #[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    /// Get total size
198    #[must_use]
199    pub fn total_size(&self) -> String {
200        // This would need proper size parsing
201        // For now just return placeholder
202        "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}