docker_wrapper/command/compose/
events.rs1use crate::command::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::Deserialize;
7
8#[derive(Debug, Clone)]
10pub struct ComposeEventsCommand {
11 pub executor: CommandExecutor,
13 pub config: ComposeConfig,
15 pub json: bool,
17 pub since: Option<String>,
19 pub until: Option<String>,
21 pub services: Vec<String>,
23}
24
25#[derive(Debug, Clone, Deserialize)]
27pub struct ComposeEvent {
28 pub time: String,
30 #[serde(rename = "type")]
32 pub event_type: String,
33 pub action: String,
35 pub service: Option<String>,
37 pub container: Option<String>,
39 pub attributes: Option<serde_json::Value>,
41}
42
43#[derive(Debug, Clone)]
45pub struct ComposeEventsResult {
46 pub stdout: String,
48 pub stderr: String,
50 pub success: bool,
52 pub events: Vec<ComposeEvent>,
54 pub services: Vec<String>,
56}
57
58impl ComposeEventsCommand {
59 #[must_use]
61 pub fn new() -> Self {
62 Self {
63 executor: CommandExecutor::new(),
64 config: ComposeConfig::new(),
65 json: false,
66 since: None,
67 until: None,
68 services: Vec::new(),
69 }
70 }
71
72 #[must_use]
74 pub fn json(mut self) -> Self {
75 self.json = true;
76 self
77 }
78
79 #[must_use]
81 pub fn since(mut self, timestamp: impl Into<String>) -> Self {
82 self.since = Some(timestamp.into());
83 self
84 }
85
86 #[must_use]
88 pub fn until(mut self, timestamp: impl Into<String>) -> Self {
89 self.until = Some(timestamp.into());
90 self
91 }
92
93 #[must_use]
95 pub fn service(mut self, service: impl Into<String>) -> Self {
96 self.services.push(service.into());
97 self
98 }
99
100 #[must_use]
102 pub fn services<I, S>(mut self, services: I) -> Self
103 where
104 I: IntoIterator<Item = S>,
105 S: Into<String>,
106 {
107 self.services.extend(services.into_iter().map(Into::into));
108 self
109 }
110}
111
112impl Default for ComposeEventsCommand {
113 fn default() -> Self {
114 Self::new()
115 }
116}
117
118#[async_trait]
119impl DockerCommand for ComposeEventsCommand {
120 type Output = ComposeEventsResult;
121
122 fn get_executor(&self) -> &CommandExecutor {
123 &self.executor
124 }
125
126 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
127 &mut self.executor
128 }
129
130 fn build_command_args(&self) -> Vec<String> {
131 <Self as ComposeCommand>::build_command_args(self)
133 }
134
135 async fn execute(&self) -> Result<Self::Output> {
136 let args = <Self as ComposeCommand>::build_command_args(self);
137 let output = self.execute_command(args).await?;
138
139 let events = if self.json {
141 output
142 .stdout
143 .lines()
144 .filter_map(|line| {
145 if line.trim().is_empty() {
146 None
147 } else {
148 serde_json::from_str(line).ok()
149 }
150 })
151 .collect()
152 } else {
153 Vec::new()
154 };
155
156 Ok(ComposeEventsResult {
157 stdout: output.stdout,
158 stderr: output.stderr,
159 success: output.success,
160 events,
161 services: self.services.clone(),
162 })
163 }
164}
165
166impl ComposeCommand for ComposeEventsCommand {
167 fn get_config(&self) -> &ComposeConfig {
168 &self.config
169 }
170
171 fn get_config_mut(&mut self) -> &mut ComposeConfig {
172 &mut self.config
173 }
174
175 fn subcommand(&self) -> &'static str {
176 "events"
177 }
178
179 fn build_subcommand_args(&self) -> Vec<String> {
180 let mut args = Vec::new();
181
182 if self.json {
183 args.push("--json".to_string());
184 }
185
186 if let Some(ref since) = self.since {
187 args.push("--since".to_string());
188 args.push(since.clone());
189 }
190
191 if let Some(ref until) = self.until {
192 args.push("--until".to_string());
193 args.push(until.clone());
194 }
195
196 args.extend(self.services.clone());
198
199 args
200 }
201}
202
203impl ComposeEventsResult {
204 #[must_use]
206 pub fn success(&self) -> bool {
207 self.success
208 }
209
210 #[must_use]
212 pub fn events(&self) -> &[ComposeEvent] {
213 &self.events
214 }
215
216 #[must_use]
218 pub fn services(&self) -> &[String] {
219 &self.services
220 }
221
222 #[must_use]
224 pub fn events_for_service(&self, service: &str) -> Vec<&ComposeEvent> {
225 self.events
226 .iter()
227 .filter(|event| event.service.as_ref().is_some_and(|s| s == service))
228 .collect()
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn test_compose_events_basic() {
238 let cmd = ComposeEventsCommand::new();
239 let args = cmd.build_subcommand_args();
240 assert!(args.is_empty());
241
242 let full_args = ComposeCommand::build_command_args(&cmd);
243 assert_eq!(full_args[0], "compose");
244 assert!(full_args.contains(&"events".to_string()));
245 }
246
247 #[test]
248 fn test_compose_events_with_json() {
249 let cmd = ComposeEventsCommand::new().json();
250 let args = cmd.build_subcommand_args();
251 assert!(args.contains(&"--json".to_string()));
252 }
253
254 #[test]
255 fn test_compose_events_with_timestamps() {
256 let cmd = ComposeEventsCommand::new()
257 .since("2024-01-01T00:00:00")
258 .until("2024-01-02T00:00:00");
259
260 let args = cmd.build_subcommand_args();
261 assert!(args.contains(&"--since".to_string()));
262 assert!(args.contains(&"2024-01-01T00:00:00".to_string()));
263 assert!(args.contains(&"--until".to_string()));
264 assert!(args.contains(&"2024-01-02T00:00:00".to_string()));
265 }
266
267 #[test]
268 fn test_compose_events_with_services() {
269 let cmd = ComposeEventsCommand::new().service("web").service("db");
270
271 let args = cmd.build_subcommand_args();
272 assert!(args.contains(&"web".to_string()));
273 assert!(args.contains(&"db".to_string()));
274 }
275
276 #[test]
277 fn test_compose_events_with_services_method() {
278 let cmd = ComposeEventsCommand::new().services(vec!["cache", "queue"]);
279 let args = cmd.build_subcommand_args();
280 assert!(args.contains(&"cache".to_string()));
281 assert!(args.contains(&"queue".to_string()));
282 }
283
284 #[test]
285 fn test_compose_events_all_options() {
286 let cmd = ComposeEventsCommand::new()
287 .json()
288 .since("2024-01-01")
289 .until("2024-01-02")
290 .services(vec!["web", "worker"]);
291
292 let args = cmd.build_subcommand_args();
293 assert!(args.contains(&"--json".to_string()));
294 assert!(args.contains(&"--since".to_string()));
295 assert!(args.contains(&"2024-01-01".to_string()));
296 assert!(args.contains(&"--until".to_string()));
297 assert!(args.contains(&"2024-01-02".to_string()));
298 assert!(args.contains(&"web".to_string()));
299 assert!(args.contains(&"worker".to_string()));
300 }
301
302 #[test]
303 fn test_compose_config_integration() {
304 let cmd = ComposeEventsCommand::new()
305 .file("docker-compose.yml")
306 .project_name("myapp")
307 .json()
308 .service("api");
309
310 let args = ComposeCommand::build_command_args(&cmd);
311 assert!(args.contains(&"--file".to_string()));
312 assert!(args.contains(&"docker-compose.yml".to_string()));
313 assert!(args.contains(&"--project-name".to_string()));
314 assert!(args.contains(&"myapp".to_string()));
315 assert!(args.contains(&"--json".to_string()));
316 assert!(args.contains(&"api".to_string()));
317 }
318}