docker_wrapper/command/
compose_ls.rs1use super::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::Deserialize;
7
8#[derive(Debug, Clone, Default)]
12pub struct ComposeLsCommand {
13 pub executor: CommandExecutor,
15 pub config: ComposeConfig,
17 pub all: bool,
19 pub filter: Option<String>,
21 pub format: Option<LsFormat>,
23 pub quiet: bool,
25}
26
27#[derive(Debug, Clone, Copy)]
29pub enum LsFormat {
30 Table,
32 Json,
34}
35
36impl LsFormat {
37 #[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#[derive(Debug, Clone, Deserialize)]
49#[serde(rename_all = "PascalCase")]
50pub struct ComposeProject {
51 pub name: String,
53 pub status: String,
55 #[serde(default)]
57 pub config_files: String,
58 #[serde(default)]
60 pub created: String,
61}
62
63#[derive(Debug, Clone)]
65pub struct LsResult {
66 pub projects: Vec<ComposeProject>,
68 pub raw_output: String,
70}
71
72impl ComposeLsCommand {
73 #[must_use]
75 pub fn new() -> Self {
76 Self {
77 executor: CommandExecutor::new(),
78 config: ComposeConfig::new(),
79 ..Default::default()
80 }
81 }
82
83 #[must_use]
85 pub fn all(mut self) -> Self {
86 self.all = true;
87 self
88 }
89
90 #[must_use]
92 pub fn filter(mut self, filter: impl Into<String>) -> Self {
93 self.filter = Some(filter.into());
94 self
95 }
96
97 #[must_use]
99 pub fn format(mut self, format: LsFormat) -> Self {
100 self.format = Some(format);
101 self
102 }
103
104 #[must_use]
106 pub fn format_json(mut self) -> Self {
107 self.format = Some(LsFormat::Json);
108 self
109 }
110
111 #[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 <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 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 if self.all {
172 args.push("--all".to_string());
173 }
174 if self.quiet {
175 args.push("--quiet".to_string());
176 }
177
178 if let Some(filter) = &self.filter {
180 args.push("--filter".to_string());
181 args.push(filter.clone());
182 }
183
184 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 #[must_use]
197 pub fn project_names(&self) -> Vec<String> {
198 self.projects.iter().map(|p| p.name.clone()).collect()
199 }
200
201 #[must_use]
203 pub fn has_project(&self, name: &str) -> bool {
204 self.projects.iter().any(|p| p.name == name)
205 }
206
207 #[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 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}