docker_wrapper/command/
compose_ls.rs

1//! Docker Compose ls 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;
7
8/// Docker Compose ls command
9///
10/// List running compose projects.
11#[derive(Debug, Clone, Default)]
12pub struct ComposeLsCommand {
13    /// Base command executor
14    pub executor: CommandExecutor,
15    /// Base compose configuration
16    pub config: ComposeConfig,
17    /// Show all projects (including stopped)
18    pub all: bool,
19    /// Filter by name
20    pub filter: Option<String>,
21    /// Format output (table, json)
22    pub format: Option<LsFormat>,
23    /// Only display project names
24    pub quiet: bool,
25}
26
27/// Ls output format
28#[derive(Debug, Clone, Copy)]
29pub enum LsFormat {
30    /// Table format (default)
31    Table,
32    /// JSON format
33    Json,
34}
35
36impl LsFormat {
37    /// Convert to command line argument
38    #[must_use]
39    pub fn as_arg(&self) -> &str {
40        match self {
41            Self::Table => "table",
42            Self::Json => "json",
43        }
44    }
45}
46
47/// Compose project information
48#[derive(Debug, Clone, Deserialize)]
49#[serde(rename_all = "PascalCase")]
50pub struct ComposeProject {
51    /// Project name
52    pub name: String,
53    /// Status
54    pub status: String,
55    /// Configuration files
56    #[serde(default)]
57    pub config_files: String,
58    /// Created timestamp
59    #[serde(default)]
60    pub created: String,
61}
62
63/// Result from ls command
64#[derive(Debug, Clone)]
65pub struct LsResult {
66    /// List of compose projects
67    pub projects: Vec<ComposeProject>,
68    /// Raw output (for non-JSON formats)
69    pub raw_output: String,
70}
71
72impl ComposeLsCommand {
73    /// Create a new ls command
74    #[must_use]
75    pub fn new() -> Self {
76        Self {
77            executor: CommandExecutor::new(),
78            config: ComposeConfig::new(),
79            ..Default::default()
80        }
81    }
82
83    /// Show all projects
84    #[must_use]
85    pub fn all(mut self) -> Self {
86        self.all = true;
87        self
88    }
89
90    /// Filter projects
91    #[must_use]
92    pub fn filter(mut self, filter: impl Into<String>) -> Self {
93        self.filter = Some(filter.into());
94        self
95    }
96
97    /// Set output format
98    #[must_use]
99    pub fn format(mut self, format: LsFormat) -> Self {
100        self.format = Some(format);
101        self
102    }
103
104    /// Set output format to JSON
105    #[must_use]
106    pub fn format_json(mut self) -> Self {
107        self.format = Some(LsFormat::Json);
108        self
109    }
110
111    /// Only display project names
112    #[must_use]
113    pub fn quiet(mut self) -> Self {
114        self.quiet = true;
115        self
116    }
117}
118
119#[async_trait]
120impl DockerCommand for ComposeLsCommand {
121    type Output = LsResult;
122
123    fn get_executor(&self) -> &CommandExecutor {
124        &self.executor
125    }
126
127    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
128        &mut self.executor
129    }
130
131    fn build_command_args(&self) -> Vec<String> {
132        // Use the ComposeCommand implementation explicitly
133        <Self as ComposeCommand>::build_command_args(self)
134    }
135
136    async fn execute(&self) -> Result<Self::Output> {
137        let args = <Self as ComposeCommand>::build_command_args(self);
138        let output = self.execute_command(args).await?;
139
140        // Parse JSON output if format is JSON
141        let projects = if matches!(self.format, Some(LsFormat::Json)) {
142            serde_json::from_str(&output.stdout).unwrap_or_default()
143        } else {
144            Vec::new()
145        };
146
147        Ok(LsResult {
148            projects,
149            raw_output: output.stdout,
150        })
151    }
152}
153
154impl ComposeCommand for ComposeLsCommand {
155    fn get_config(&self) -> &ComposeConfig {
156        &self.config
157    }
158
159    fn get_config_mut(&mut self) -> &mut ComposeConfig {
160        &mut self.config
161    }
162
163    fn subcommand(&self) -> &'static str {
164        "ls"
165    }
166
167    fn build_subcommand_args(&self) -> Vec<String> {
168        let mut args = Vec::new();
169
170        // Add flags
171        if self.all {
172            args.push("--all".to_string());
173        }
174        if self.quiet {
175            args.push("--quiet".to_string());
176        }
177
178        // Add filter
179        if let Some(filter) = &self.filter {
180            args.push("--filter".to_string());
181            args.push(filter.clone());
182        }
183
184        // Add format
185        if let Some(format) = &self.format {
186            args.push("--format".to_string());
187            args.push(format.as_arg().to_string());
188        }
189
190        args
191    }
192}
193
194impl LsResult {
195    /// Get project names
196    #[must_use]
197    pub fn project_names(&self) -> Vec<String> {
198        self.projects.iter().map(|p| p.name.clone()).collect()
199    }
200
201    /// Check if a project exists
202    #[must_use]
203    pub fn has_project(&self, name: &str) -> bool {
204        self.projects.iter().any(|p| p.name == name)
205    }
206
207    /// Get running projects
208    #[must_use]
209    pub fn running_projects(&self) -> Vec<&ComposeProject> {
210        self.projects
211            .iter()
212            .filter(|p| p.status.contains("running"))
213            .collect()
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_ls_command_basic() {
223        let cmd = ComposeLsCommand::new();
224        let _args = cmd.build_subcommand_args();
225        // No specific args for basic command
226
227        let full_args = ComposeCommand::build_command_args(&cmd);
228        assert_eq!(full_args[0], "compose");
229        assert!(full_args.contains(&"ls".to_string()));
230    }
231
232    #[test]
233    fn test_ls_command_with_all() {
234        let cmd = ComposeLsCommand::new().all();
235        let args = cmd.build_subcommand_args();
236        assert!(args.contains(&"--all".to_string()));
237    }
238
239    #[test]
240    fn test_ls_command_with_format() {
241        let cmd = ComposeLsCommand::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_ls_command_with_filter() {
249        let cmd = ComposeLsCommand::new().filter("status=running").quiet();
250        let args = cmd.build_subcommand_args();
251        assert!(args.contains(&"--filter".to_string()));
252        assert!(args.contains(&"status=running".to_string()));
253        assert!(args.contains(&"--quiet".to_string()));
254    }
255
256    #[test]
257    fn test_compose_config_integration() {
258        let cmd = ComposeLsCommand::new()
259            .file("docker-compose.yml")
260            .project_name("my-project");
261
262        let args = ComposeCommand::build_command_args(&cmd);
263        assert!(args.contains(&"--file".to_string()));
264        assert!(args.contains(&"docker-compose.yml".to_string()));
265        assert!(args.contains(&"--project-name".to_string()));
266        assert!(args.contains(&"my-project".to_string()));
267    }
268
269    #[test]
270    fn test_ls_result_helpers() {
271        let result = LsResult {
272            projects: vec![
273                ComposeProject {
274                    name: "web".to_string(),
275                    status: "running(3)".to_string(),
276                    config_files: "docker-compose.yml".to_string(),
277                    created: "2025-08-23".to_string(),
278                },
279                ComposeProject {
280                    name: "db".to_string(),
281                    status: "exited(0)".to_string(),
282                    config_files: "docker-compose.yml".to_string(),
283                    created: "2025-08-23".to_string(),
284                },
285            ],
286            raw_output: String::new(),
287        };
288
289        assert_eq!(result.project_names(), vec!["web", "db"]);
290        assert!(result.has_project("web"));
291        assert!(!result.has_project("cache"));
292        assert_eq!(result.running_projects().len(), 1);
293    }
294}