docker_wrapper/compose/
events.rs

1//! Docker Compose events command implementation.
2
3use crate::compose::{ComposeCommandV2 as ComposeCommand, ComposeConfig};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::Deserialize;
7
8/// Docker Compose events command
9///
10/// Stream container events for services.
11#[derive(Debug, Clone, Default)]
12pub struct ComposeEventsCommand {
13    /// Base 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
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 events command
44#[derive(Debug, Clone)]
45pub struct EventsResult {
46    /// Raw output from the command
47    pub output: String,
48    /// Parsed events (if JSON format)
49    pub events: Vec<ComposeEvent>,
50}
51
52impl ComposeEventsCommand {
53    /// Create a new events command
54    #[must_use]
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    /// Add a compose file
60    #[must_use]
61    pub fn file<P: Into<std::path::PathBuf>>(mut self, file: P) -> Self {
62        self.config.files.push(file.into());
63        self
64    }
65
66    /// Set project name
67    #[must_use]
68    pub fn project_name(mut self, name: impl Into<String>) -> Self {
69        self.config.project_name = Some(name.into());
70        self
71    }
72
73    /// Output as JSON
74    #[must_use]
75    pub fn json(mut self) -> Self {
76        self.json = true;
77        self
78    }
79
80    /// Set start timestamp
81    #[must_use]
82    pub fn since(mut self, timestamp: impl Into<String>) -> Self {
83        self.since = Some(timestamp.into());
84        self
85    }
86
87    /// Set end timestamp
88    #[must_use]
89    pub fn until(mut self, timestamp: impl Into<String>) -> Self {
90        self.until = Some(timestamp.into());
91        self
92    }
93
94    /// Add a service to get events for
95    #[must_use]
96    pub fn service(mut self, service: impl Into<String>) -> Self {
97        self.services.push(service.into());
98        self
99    }
100
101    /// Add multiple services
102    #[must_use]
103    pub fn services<I, S>(mut self, services: I) -> Self
104    where
105        I: IntoIterator<Item = S>,
106        S: Into<String>,
107    {
108        self.services.extend(services.into_iter().map(Into::into));
109        self
110    }
111
112    fn build_args(&self) -> Vec<String> {
113        let mut args = vec!["events".to_string()];
114
115        // Add flags
116        if self.json {
117            args.push("--json".to_string());
118        }
119
120        // Add timestamps
121        if let Some(since) = &self.since {
122            args.push("--since".to_string());
123            args.push(since.clone());
124        }
125        if let Some(until) = &self.until {
126            args.push("--until".to_string());
127            args.push(until.clone());
128        }
129
130        // Add services
131        args.extend(self.services.clone());
132
133        args
134    }
135}
136
137#[async_trait]
138impl ComposeCommand for ComposeEventsCommand {
139    type Output = EventsResult;
140
141    fn get_config(&self) -> &ComposeConfig {
142        &self.config
143    }
144
145    fn get_config_mut(&mut self) -> &mut ComposeConfig {
146        &mut self.config
147    }
148
149    async fn execute_compose(&self, args: Vec<String>) -> Result<Self::Output> {
150        let output = self.execute_compose_command(args).await?;
151
152        // Parse events if JSON format
153        let events = if self.json {
154            output
155                .stdout
156                .lines()
157                .filter_map(|line| serde_json::from_str(line).ok())
158                .collect()
159        } else {
160            Vec::new()
161        };
162
163        Ok(EventsResult {
164            output: output.stdout,
165            events,
166        })
167    }
168
169    async fn execute(&self) -> Result<Self::Output> {
170        let args = self.build_args();
171        self.execute_compose(args).await
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_events_command_basic() {
181        let cmd = ComposeEventsCommand::new();
182        let args = cmd.build_args();
183        assert_eq!(args[0], "events");
184    }
185
186    #[test]
187    fn test_events_command_with_json() {
188        let cmd = ComposeEventsCommand::new().json();
189        let args = cmd.build_args();
190        assert!(args.contains(&"--json".to_string()));
191    }
192
193    #[test]
194    fn test_events_command_with_timestamps() {
195        let cmd = ComposeEventsCommand::new()
196            .since("2025-08-23T00:00:00")
197            .until("2025-08-23T23:59:59");
198        let args = cmd.build_args();
199        assert!(args.contains(&"--since".to_string()));
200        assert!(args.contains(&"2025-08-23T00:00:00".to_string()));
201        assert!(args.contains(&"--until".to_string()));
202        assert!(args.contains(&"2025-08-23T23:59:59".to_string()));
203    }
204
205    #[test]
206    fn test_events_command_with_services() {
207        let cmd = ComposeEventsCommand::new().service("web").service("db");
208        let args = cmd.build_args();
209        assert!(args.contains(&"web".to_string()));
210        assert!(args.contains(&"db".to_string()));
211    }
212}