docker_wrapper/command/compose/
images.rs

1//! Docker Compose images command implementation using unified trait pattern.
2
3use crate::command::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::Deserialize;
7
8/// Docker Compose images command builder
9#[derive(Debug, Clone)]
10pub struct ComposeImagesCommand {
11    /// Base command executor
12    pub executor: CommandExecutor,
13    /// Base compose configuration
14    pub config: ComposeConfig,
15    /// Output format
16    pub format: Option<ImagesFormat>,
17    /// Only display image IDs
18    pub quiet: bool,
19    /// Services to list images for (empty for all)
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 std::fmt::Display for ImagesFormat {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            Self::Table => write!(f, "table"),
36            Self::Json => write!(f, "json"),
37        }
38    }
39}
40
41/// Image information from JSON output
42#[derive(Debug, Clone, Deserialize)]
43pub struct ImageInfo {
44    /// Container name
45    pub container: String,
46    /// Repository
47    pub repository: String,
48    /// Tag
49    pub tag: String,
50    /// Image ID
51    #[serde(rename = "ID")]
52    pub id: String,
53    /// Size
54    pub size: String,
55}
56
57/// Result from compose images command
58#[derive(Debug, Clone)]
59pub struct ComposeImagesResult {
60    /// Raw stdout output
61    pub stdout: String,
62    /// Raw stderr output
63    pub stderr: String,
64    /// Success status
65    pub success: bool,
66    /// Parsed image information (if JSON format)
67    pub images: Vec<ImageInfo>,
68    /// Services that were queried
69    pub services: Vec<String>,
70}
71
72impl ComposeImagesCommand {
73    /// Create a new compose images command
74    #[must_use]
75    pub fn new() -> Self {
76        Self {
77            executor: CommandExecutor::new(),
78            config: ComposeConfig::new(),
79            format: None,
80            quiet: false,
81            services: Vec::new(),
82        }
83    }
84
85    /// Set output format
86    #[must_use]
87    pub fn format(mut self, format: ImagesFormat) -> Self {
88        self.format = Some(format);
89        self
90    }
91
92    /// Set output format to JSON
93    #[must_use]
94    pub fn format_json(mut self) -> Self {
95        self.format = Some(ImagesFormat::Json);
96        self
97    }
98
99    /// Set output format to table
100    #[must_use]
101    pub fn format_table(mut self) -> Self {
102        self.format = Some(ImagesFormat::Table);
103        self
104    }
105
106    /// Only display image IDs
107    #[must_use]
108    pub fn quiet(mut self) -> Self {
109        self.quiet = true;
110        self
111    }
112
113    /// Add a service to list images for
114    #[must_use]
115    pub fn service(mut self, service: impl Into<String>) -> Self {
116        self.services.push(service.into());
117        self
118    }
119
120    /// Add multiple services to list images for
121    #[must_use]
122    pub fn services<I, S>(mut self, services: I) -> Self
123    where
124        I: IntoIterator<Item = S>,
125        S: Into<String>,
126    {
127        self.services.extend(services.into_iter().map(Into::into));
128        self
129    }
130}
131
132impl Default for ComposeImagesCommand {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138#[async_trait]
139impl DockerCommand for ComposeImagesCommand {
140    type Output = ComposeImagesResult;
141
142    fn get_executor(&self) -> &CommandExecutor {
143        &self.executor
144    }
145
146    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
147        &mut self.executor
148    }
149
150    fn build_command_args(&self) -> Vec<String> {
151        <Self as ComposeCommand>::build_command_args(self)
152    }
153
154    async fn execute(&self) -> Result<Self::Output> {
155        let args = <Self as ComposeCommand>::build_command_args(self);
156        let output = self.execute_command(args).await?;
157
158        let images = if matches!(self.format, Some(ImagesFormat::Json)) {
159            serde_json::from_str(&output.stdout).unwrap_or_default()
160        } else {
161            Vec::new()
162        };
163
164        Ok(ComposeImagesResult {
165            stdout: output.stdout,
166            stderr: output.stderr,
167            success: output.success,
168            images,
169            services: self.services.clone(),
170        })
171    }
172}
173
174impl ComposeCommand for ComposeImagesCommand {
175    fn get_config(&self) -> &ComposeConfig {
176        &self.config
177    }
178
179    fn get_config_mut(&mut self) -> &mut ComposeConfig {
180        &mut self.config
181    }
182
183    fn subcommand(&self) -> &'static str {
184        "images"
185    }
186
187    fn build_subcommand_args(&self) -> Vec<String> {
188        let mut args = Vec::new();
189
190        if let Some(format) = self.format {
191            args.push("--format".to_string());
192            args.push(format.to_string());
193        }
194
195        if self.quiet {
196            args.push("--quiet".to_string());
197        }
198
199        args.extend(self.services.clone());
200        args
201    }
202}
203
204impl ComposeImagesResult {
205    /// Check if the command was successful
206    #[must_use]
207    pub fn success(&self) -> bool {
208        self.success
209    }
210
211    /// Get parsed image information
212    #[must_use]
213    pub fn images(&self) -> &[ImageInfo] {
214        &self.images
215    }
216
217    /// Get the services that were queried
218    #[must_use]
219    pub fn services(&self) -> &[String] {
220        &self.services
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_compose_images_basic() {
230        let cmd = ComposeImagesCommand::new();
231        let args = cmd.build_subcommand_args();
232        assert!(args.is_empty());
233
234        let full_args = ComposeCommand::build_command_args(&cmd);
235        assert_eq!(full_args[0], "compose");
236        assert!(full_args.contains(&"images".to_string()));
237    }
238
239    #[test]
240    fn test_compose_images_with_format() {
241        let cmd = ComposeImagesCommand::new().format_json();
242        let args = cmd.build_subcommand_args();
243        assert!(args.contains(&"--format".to_string()));
244        assert!(args.contains(&"json".to_string()));
245    }
246
247    #[test]
248    fn test_compose_images_quiet() {
249        let cmd = ComposeImagesCommand::new().quiet();
250        let args = cmd.build_subcommand_args();
251        assert!(args.contains(&"--quiet".to_string()));
252    }
253
254    #[test]
255    fn test_compose_images_with_services() {
256        let cmd = ComposeImagesCommand::new().services(vec!["web", "db"]);
257        let args = cmd.build_subcommand_args();
258        assert!(args.contains(&"web".to_string()));
259        assert!(args.contains(&"db".to_string()));
260    }
261}