docker_wrapper/compose/
ls.rs1use crate::compose::{ComposeCommandV2 as ComposeCommand, ComposeConfig};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::Deserialize;
7
8#[derive(Debug, Clone, Default)]
12pub struct ComposeLsCommand {
13 pub config: ComposeConfig,
15 pub all: bool,
17 pub filter: Option<String>,
19 pub format: Option<LsFormat>,
21 pub quiet: bool,
23}
24
25#[derive(Debug, Clone, Copy)]
27pub enum LsFormat {
28 Table,
30 Json,
32}
33
34impl LsFormat {
35 #[must_use]
37 pub fn as_arg(&self) -> &str {
38 match self {
39 Self::Table => "table",
40 Self::Json => "json",
41 }
42 }
43}
44
45#[derive(Debug, Clone, Deserialize)]
47#[serde(rename_all = "PascalCase")]
48pub struct ComposeProject {
49 pub name: String,
51 pub status: String,
53 #[serde(default)]
55 pub config_files: String,
56 #[serde(default)]
58 pub created: String,
59}
60
61#[derive(Debug, Clone)]
63pub struct LsResult {
64 pub projects: Vec<ComposeProject>,
66 pub raw_output: String,
68}
69
70impl ComposeLsCommand {
71 #[must_use]
73 pub fn new() -> Self {
74 Self::default()
75 }
76
77 #[must_use]
79 pub fn all(mut self) -> Self {
80 self.all = true;
81 self
82 }
83
84 #[must_use]
86 pub fn filter(mut self, filter: impl Into<String>) -> Self {
87 self.filter = Some(filter.into());
88 self
89 }
90
91 #[must_use]
93 pub fn format(mut self, format: LsFormat) -> Self {
94 self.format = Some(format);
95 self
96 }
97
98 #[must_use]
100 pub fn format_json(mut self) -> Self {
101 self.format = Some(LsFormat::Json);
102 self
103 }
104
105 #[must_use]
107 pub fn quiet(mut self) -> Self {
108 self.quiet = true;
109 self
110 }
111
112 fn build_args(&self) -> Vec<String> {
113 let mut args = vec!["ls".to_string()];
114
115 if self.all {
117 args.push("--all".to_string());
118 }
119 if self.quiet {
120 args.push("--quiet".to_string());
121 }
122
123 if let Some(filter) = &self.filter {
125 args.push("--filter".to_string());
126 args.push(filter.clone());
127 }
128
129 if let Some(format) = &self.format {
131 args.push("--format".to_string());
132 args.push(format.as_arg().to_string());
133 }
134
135 args
136 }
137}
138
139#[async_trait]
140impl ComposeCommand for ComposeLsCommand {
141 type Output = LsResult;
142
143 fn get_config(&self) -> &ComposeConfig {
144 &self.config
145 }
146
147 fn get_config_mut(&mut self) -> &mut ComposeConfig {
148 &mut self.config
149 }
150
151 async fn execute_compose(&self, args: Vec<String>) -> Result<Self::Output> {
152 let output = self.execute_compose_command(args).await?;
153
154 let projects = if matches!(self.format, Some(LsFormat::Json)) {
156 serde_json::from_str(&output.stdout).unwrap_or_default()
157 } else {
158 Vec::new()
159 };
160
161 Ok(LsResult {
162 projects,
163 raw_output: output.stdout,
164 })
165 }
166
167 async fn execute(&self) -> Result<Self::Output> {
168 let args = self.build_args();
169 self.execute_compose(args).await
170 }
171}
172
173impl LsResult {
174 #[must_use]
176 pub fn project_names(&self) -> Vec<String> {
177 self.projects.iter().map(|p| p.name.clone()).collect()
178 }
179
180 #[must_use]
182 pub fn has_project(&self, name: &str) -> bool {
183 self.projects.iter().any(|p| p.name == name)
184 }
185
186 #[must_use]
188 pub fn running_projects(&self) -> Vec<&ComposeProject> {
189 self.projects
190 .iter()
191 .filter(|p| p.status.contains("running"))
192 .collect()
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_ls_command_basic() {
202 let cmd = ComposeLsCommand::new();
203 let args = cmd.build_args();
204 assert_eq!(args[0], "ls");
205 }
206
207 #[test]
208 fn test_ls_command_with_all() {
209 let cmd = ComposeLsCommand::new().all();
210 let args = cmd.build_args();
211 assert!(args.contains(&"--all".to_string()));
212 }
213
214 #[test]
215 fn test_ls_command_with_format() {
216 let cmd = ComposeLsCommand::new().format_json();
217 let args = cmd.build_args();
218 assert!(args.contains(&"--format".to_string()));
219 assert!(args.contains(&"json".to_string()));
220 }
221
222 #[test]
223 fn test_ls_command_with_filter() {
224 let cmd = ComposeLsCommand::new().filter("status=running").quiet();
225 let args = cmd.build_args();
226 assert!(args.contains(&"--filter".to_string()));
227 assert!(args.contains(&"status=running".to_string()));
228 assert!(args.contains(&"--quiet".to_string()));
229 }
230
231 #[test]
232 fn test_ls_result_helpers() {
233 let result = LsResult {
234 projects: vec![
235 ComposeProject {
236 name: "web".to_string(),
237 status: "running(3)".to_string(),
238 config_files: "docker-compose.yml".to_string(),
239 created: "2025-08-23".to_string(),
240 },
241 ComposeProject {
242 name: "db".to_string(),
243 status: "exited(0)".to_string(),
244 config_files: "docker-compose.yml".to_string(),
245 created: "2025-08-23".to_string(),
246 },
247 ],
248 raw_output: String::new(),
249 };
250
251 assert_eq!(result.project_names(), vec!["web", "db"]);
252 assert!(result.has_project("web"));
253 assert!(!result.has_project("cache"));
254 assert_eq!(result.running_projects().len(), 1);
255 }
256}