docker_wrapper/command/
compose_ps.rs

1//! Docker Compose ps command implementation using unified trait pattern.
2
3use super::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7
8/// Docker Compose ps command builder
9#[derive(Debug, Clone)]
10pub struct ComposePsCommand {
11    /// Base command executor
12    pub executor: CommandExecutor,
13    /// Base compose configuration
14    pub config: ComposeConfig,
15    /// Services to list (empty for all)
16    pub services: Vec<String>,
17    /// Show all containers (including stopped)
18    pub all: bool,
19    /// Only display container IDs
20    pub quiet: bool,
21    /// Show services
22    pub show_services: bool,
23    /// Filter containers
24    pub filter: Vec<String>,
25    /// Output format
26    pub format: Option<String>,
27    /// Only show running containers
28    pub status: Option<Vec<ContainerStatus>>,
29}
30
31/// Container status filter
32#[derive(Debug, Clone, Copy)]
33pub enum ContainerStatus {
34    /// Paused containers
35    Paused,
36    /// Restarting containers
37    Restarting,
38    /// Running containers
39    Running,
40    /// Stopped containers
41    Stopped,
42}
43
44impl std::fmt::Display for ContainerStatus {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match self {
47            Self::Paused => write!(f, "paused"),
48            Self::Restarting => write!(f, "restarting"),
49            Self::Running => write!(f, "running"),
50            Self::Stopped => write!(f, "stopped"),
51        }
52    }
53}
54
55/// Container information from compose ps
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ComposeContainerInfo {
58    /// Container ID
59    #[serde(rename = "ID")]
60    pub id: String,
61    /// Container name
62    #[serde(rename = "Name")]
63    pub name: String,
64    /// Service name
65    #[serde(rename = "Service")]
66    pub service: String,
67    /// Container state
68    #[serde(rename = "State")]
69    pub state: String,
70    /// Health status
71    #[serde(rename = "Health")]
72    pub health: Option<String>,
73    /// Exit code
74    #[serde(rename = "ExitCode")]
75    pub exit_code: Option<i32>,
76    /// Published ports
77    #[serde(rename = "Publishers")]
78    pub publishers: Option<Vec<PortPublisher>>,
79}
80
81/// Port publishing information
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct PortPublisher {
84    /// Target port
85    #[serde(rename = "TargetPort")]
86    pub target_port: u16,
87    /// Published port
88    #[serde(rename = "PublishedPort")]
89    pub published_port: Option<u16>,
90    /// Protocol
91    #[serde(rename = "Protocol")]
92    pub protocol: String,
93}
94
95/// Result from compose ps command
96#[derive(Debug, Clone)]
97pub struct ComposePsResult {
98    /// Raw stdout output
99    pub stdout: String,
100    /// Raw stderr output
101    pub stderr: String,
102    /// Success status
103    pub success: bool,
104    /// Parsed container information (if JSON format)
105    pub containers: Vec<ComposeContainerInfo>,
106}
107
108impl ComposePsCommand {
109    /// Create a new compose ps command
110    #[must_use]
111    pub fn new() -> Self {
112        Self {
113            executor: CommandExecutor::new(),
114            config: ComposeConfig::new(),
115            services: Vec::new(),
116            all: false,
117            quiet: false,
118            show_services: false,
119            filter: Vec::new(),
120            format: None,
121            status: None,
122        }
123    }
124
125    /// Add a service to list
126    #[must_use]
127    pub fn service(mut self, service: impl Into<String>) -> Self {
128        self.services.push(service.into());
129        self
130    }
131
132    /// Show all containers (default shows only running)
133    #[must_use]
134    pub fn all(mut self) -> Self {
135        self.all = true;
136        self
137    }
138
139    /// Only display container IDs
140    #[must_use]
141    pub fn quiet(mut self) -> Self {
142        self.quiet = true;
143        self
144    }
145
146    /// Display services
147    #[must_use]
148    pub fn services(mut self) -> Self {
149        self.show_services = true;
150        self
151    }
152
153    /// Add a filter
154    #[must_use]
155    pub fn filter(mut self, filter: impl Into<String>) -> Self {
156        self.filter.push(filter.into());
157        self
158    }
159
160    /// Set output format
161    #[must_use]
162    pub fn format(mut self, format: impl Into<String>) -> Self {
163        self.format = Some(format.into());
164        self
165    }
166
167    /// Filter by status
168    #[must_use]
169    pub fn status(mut self, status: ContainerStatus) -> Self {
170        self.status.get_or_insert_with(Vec::new).push(status);
171        self
172    }
173
174    /// Use JSON output format
175    #[must_use]
176    pub fn json(mut self) -> Self {
177        self.format = Some("json".to_string());
178        self
179    }
180
181    /// Parse JSON output into container info
182    fn parse_json_output(stdout: &str) -> Vec<ComposeContainerInfo> {
183        stdout
184            .lines()
185            .filter(|line| !line.trim().is_empty())
186            .filter_map(|line| serde_json::from_str(line).ok())
187            .collect()
188    }
189}
190
191impl Default for ComposePsCommand {
192    fn default() -> Self {
193        Self::new()
194    }
195}
196
197#[async_trait]
198impl DockerCommand for ComposePsCommand {
199    type Output = ComposePsResult;
200
201    fn get_executor(&self) -> &CommandExecutor {
202        &self.executor
203    }
204
205    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
206        &mut self.executor
207    }
208
209    fn build_command_args(&self) -> Vec<String> {
210        // Use the ComposeCommand implementation explicitly
211        <Self as ComposeCommand>::build_command_args(self)
212    }
213
214    async fn execute(&self) -> Result<Self::Output> {
215        let args = <Self as ComposeCommand>::build_command_args(self);
216        let output = self.execute_command(args).await?;
217
218        // Parse JSON output if format is json
219        let containers = if self.format.as_deref() == Some("json") {
220            Self::parse_json_output(&output.stdout)
221        } else {
222            Vec::new()
223        };
224
225        Ok(ComposePsResult {
226            stdout: output.stdout,
227            stderr: output.stderr,
228            success: output.success,
229            containers,
230        })
231    }
232}
233
234impl ComposeCommand for ComposePsCommand {
235    fn get_config(&self) -> &ComposeConfig {
236        &self.config
237    }
238
239    fn get_config_mut(&mut self) -> &mut ComposeConfig {
240        &mut self.config
241    }
242
243    fn subcommand(&self) -> &'static str {
244        "ps"
245    }
246
247    fn build_subcommand_args(&self) -> Vec<String> {
248        let mut args = Vec::new();
249
250        if self.all {
251            args.push("--all".to_string());
252        }
253
254        if self.quiet {
255            args.push("--quiet".to_string());
256        }
257
258        if self.show_services {
259            args.push("--services".to_string());
260        }
261
262        for filter in &self.filter {
263            args.push("--filter".to_string());
264            args.push(filter.clone());
265        }
266
267        if let Some(ref format) = self.format {
268            args.push("--format".to_string());
269            args.push(format.clone());
270        }
271
272        if let Some(ref statuses) = self.status {
273            for status in statuses {
274                args.push("--status".to_string());
275                args.push(status.to_string());
276            }
277        }
278
279        // Add service names at the end
280        args.extend(self.services.clone());
281
282        args
283    }
284}
285
286impl ComposePsResult {
287    /// Check if the command was successful
288    #[must_use]
289    pub fn success(&self) -> bool {
290        self.success
291    }
292
293    /// Get container information
294    #[must_use]
295    pub fn containers(&self) -> &[ComposeContainerInfo] {
296        &self.containers
297    }
298
299    /// Get container IDs from output
300    #[must_use]
301    pub fn container_ids(&self) -> Vec<String> {
302        if self.containers.is_empty() {
303            // Parse from text output if not JSON
304            self.stdout
305                .lines()
306                .skip(1) // Skip header
307                .filter_map(|line| line.split_whitespace().next())
308                .map(String::from)
309                .collect()
310        } else {
311            self.containers.iter().map(|c| c.id.clone()).collect()
312        }
313    }
314
315    /// Get stdout lines
316    #[must_use]
317    pub fn stdout_lines(&self) -> Vec<&str> {
318        self.stdout.lines().collect()
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn test_compose_ps_basic() {
328        let cmd = ComposePsCommand::new();
329        let args = cmd.build_subcommand_args();
330        assert!(args.is_empty());
331
332        let full_args = ComposeCommand::build_command_args(&cmd);
333        assert_eq!(full_args[0], "compose");
334        assert!(full_args.contains(&"ps".to_string()));
335    }
336
337    #[test]
338    fn test_compose_ps_all() {
339        let cmd = ComposePsCommand::new().all();
340        let args = cmd.build_subcommand_args();
341        assert_eq!(args, vec!["--all"]);
342    }
343
344    #[test]
345    fn test_compose_ps_with_format() {
346        let cmd = ComposePsCommand::new().format("json").all();
347        let args = cmd.build_subcommand_args();
348        assert_eq!(args, vec!["--all", "--format", "json"]);
349    }
350
351    #[test]
352    fn test_compose_ps_with_filters() {
353        let cmd = ComposePsCommand::new()
354            .filter("status=running")
355            .quiet()
356            .service("web");
357        let args = cmd.build_subcommand_args();
358        assert_eq!(args, vec!["--quiet", "--filter", "status=running", "web"]);
359    }
360
361    #[test]
362    fn test_container_status_display() {
363        assert_eq!(ContainerStatus::Running.to_string(), "running");
364        assert_eq!(ContainerStatus::Stopped.to_string(), "stopped");
365        assert_eq!(ContainerStatus::Paused.to_string(), "paused");
366        assert_eq!(ContainerStatus::Restarting.to_string(), "restarting");
367    }
368
369    #[test]
370    fn test_compose_config_integration() {
371        let cmd = ComposePsCommand::new()
372            .file("docker-compose.yml")
373            .project_name("my-project")
374            .all();
375
376        let args = ComposeCommand::build_command_args(&cmd);
377        assert!(args.contains(&"--file".to_string()));
378        assert!(args.contains(&"docker-compose.yml".to_string()));
379        assert!(args.contains(&"--project-name".to_string()));
380        assert!(args.contains(&"my-project".to_string()));
381        assert!(args.contains(&"--all".to_string()));
382    }
383}