docker_wrapper/command/compose/
events.rs

1//! Docker Compose events command implementation using unified trait pattern.
2
3use crate::command::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::Deserialize;
7
8/// Docker Compose events command builder
9#[derive(Debug, Clone)]
10pub struct ComposeEventsCommand {
11    /// Base command executor
12    pub executor: CommandExecutor,
13    /// Base compose configuration
14    pub config: ComposeConfig,
15    /// Output format as JSON
16    pub json: bool,
17    /// Start timestamp
18    pub since: Option<String>,
19    /// End timestamp
20    pub until: Option<String>,
21    /// Services to get events for (empty for all)
22    pub services: Vec<String>,
23}
24
25/// Event from Docker Compose
26#[derive(Debug, Clone, Deserialize)]
27pub struct ComposeEvent {
28    /// Time of the event
29    pub time: String,
30    /// Type of the event
31    #[serde(rename = "type")]
32    pub event_type: String,
33    /// Action that occurred
34    pub action: String,
35    /// Service name
36    pub service: Option<String>,
37    /// Container ID
38    pub container: Option<String>,
39    /// Additional attributes
40    pub attributes: Option<serde_json::Value>,
41}
42
43/// Result from compose events command
44#[derive(Debug, Clone)]
45pub struct ComposeEventsResult {
46    /// Raw stdout output
47    pub stdout: String,
48    /// Raw stderr output
49    pub stderr: String,
50    /// Success status
51    pub success: bool,
52    /// Parsed events (if JSON format was used)
53    pub events: Vec<ComposeEvent>,
54    /// Services that were monitored
55    pub services: Vec<String>,
56}
57
58impl ComposeEventsCommand {
59    /// Create a new compose events command
60    #[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    /// Output events in JSON format
73    #[must_use]
74    pub fn json(mut self) -> Self {
75        self.json = true;
76        self
77    }
78
79    /// Set start timestamp for events
80    #[must_use]
81    pub fn since(mut self, timestamp: impl Into<String>) -> Self {
82        self.since = Some(timestamp.into());
83        self
84    }
85
86    /// Set end timestamp for events
87    #[must_use]
88    pub fn until(mut self, timestamp: impl Into<String>) -> Self {
89        self.until = Some(timestamp.into());
90        self
91    }
92
93    /// Add a service to monitor events for
94    #[must_use]
95    pub fn service(mut self, service: impl Into<String>) -> Self {
96        self.services.push(service.into());
97        self
98    }
99
100    /// Add multiple services to monitor events for
101    #[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        // Use the ComposeCommand implementation explicitly
132        <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        // Parse JSON events if JSON format was requested
140        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        // Add service names at the end
197        args.extend(self.services.clone());
198
199        args
200    }
201}
202
203impl ComposeEventsResult {
204    /// Check if the command was successful
205    #[must_use]
206    pub fn success(&self) -> bool {
207        self.success
208    }
209
210    /// Get parsed events (if JSON format was used)
211    #[must_use]
212    pub fn events(&self) -> &[ComposeEvent] {
213        &self.events
214    }
215
216    /// Get the services that were monitored
217    #[must_use]
218    pub fn services(&self) -> &[String] {
219        &self.services
220    }
221
222    /// Get events for a specific service
223    #[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}