docker_wrapper/command/
compose_logs.rs

1//! Docker Compose logs command implementation using unified trait pattern.
2
3use super::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Docker Compose logs command builder
8#[derive(Debug, Clone)]
9#[allow(clippy::struct_excessive_bools)] // Multiple boolean flags are appropriate for logs command
10pub struct ComposeLogsCommand {
11    /// Base command executor
12    pub executor: CommandExecutor,
13    /// Base compose configuration
14    pub config: ComposeConfig,
15    /// Services to show logs for (empty for all)
16    pub services: Vec<String>,
17    /// Follow log output
18    pub follow: bool,
19    /// Show timestamps
20    pub timestamps: bool,
21    /// Number of lines to show from the end
22    pub tail: Option<String>,
23    /// Show logs since timestamp
24    pub since: Option<String>,
25    /// Show logs until timestamp
26    pub until: Option<String>,
27    /// Don't print prefix
28    pub no_log_prefix: bool,
29    /// Don't use colors
30    pub no_color: bool,
31}
32
33/// Result from compose logs command
34#[derive(Debug, Clone)]
35pub struct ComposeLogsResult {
36    /// Raw stdout output
37    pub stdout: String,
38    /// Raw stderr output
39    pub stderr: String,
40    /// Success status
41    pub success: bool,
42    /// Services logs were fetched for
43    pub services: Vec<String>,
44}
45
46impl ComposeLogsCommand {
47    /// Create a new compose logs command
48    #[must_use]
49    pub fn new() -> Self {
50        Self {
51            executor: CommandExecutor::new(),
52            config: ComposeConfig::new(),
53            services: Vec::new(),
54            follow: false,
55            timestamps: false,
56            tail: None,
57            since: None,
58            until: None,
59            no_log_prefix: false,
60            no_color: false,
61        }
62    }
63
64    /// Add a service to show logs for
65    #[must_use]
66    pub fn service(mut self, service: impl Into<String>) -> Self {
67        self.services.push(service.into());
68        self
69    }
70
71    /// Add multiple services
72    #[must_use]
73    pub fn services<I, S>(mut self, services: I) -> Self
74    where
75        I: IntoIterator<Item = S>,
76        S: Into<String>,
77    {
78        self.services.extend(services.into_iter().map(Into::into));
79        self
80    }
81
82    /// Follow log output
83    #[must_use]
84    pub fn follow(mut self) -> Self {
85        self.follow = true;
86        self
87    }
88
89    /// Show timestamps
90    #[must_use]
91    pub fn timestamps(mut self) -> Self {
92        self.timestamps = true;
93        self
94    }
95
96    /// Number of lines to show from the end
97    #[must_use]
98    pub fn tail(mut self, lines: impl Into<String>) -> Self {
99        self.tail = Some(lines.into());
100        self
101    }
102
103    /// Show logs since timestamp
104    #[must_use]
105    pub fn since(mut self, timestamp: impl Into<String>) -> Self {
106        self.since = Some(timestamp.into());
107        self
108    }
109
110    /// Show logs until timestamp
111    #[must_use]
112    pub fn until(mut self, timestamp: impl Into<String>) -> Self {
113        self.until = Some(timestamp.into());
114        self
115    }
116
117    /// Don't print prefix
118    #[must_use]
119    pub fn no_log_prefix(mut self) -> Self {
120        self.no_log_prefix = true;
121        self
122    }
123
124    /// Don't use colors
125    #[must_use]
126    pub fn no_color(mut self) -> Self {
127        self.no_color = true;
128        self
129    }
130}
131
132impl Default for ComposeLogsCommand {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138#[async_trait]
139impl DockerCommand for ComposeLogsCommand {
140    type Output = ComposeLogsResult;
141
142    fn get_executor(&self) -> &CommandExecutor {
143        &self.executor
144    }
145
146    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
147        &mut self.executor
148    }
149
150    fn build_command_args(&self) -> Vec<String> {
151        // Use the ComposeCommand implementation explicitly
152        <Self as ComposeCommand>::build_command_args(self)
153    }
154
155    async fn execute(&self) -> Result<Self::Output> {
156        let args = <Self as ComposeCommand>::build_command_args(self);
157        let output = self.execute_command(args).await?;
158
159        Ok(ComposeLogsResult {
160            stdout: output.stdout,
161            stderr: output.stderr,
162            success: output.success,
163            services: self.services.clone(),
164        })
165    }
166}
167
168impl ComposeCommand for ComposeLogsCommand {
169    fn get_config(&self) -> &ComposeConfig {
170        &self.config
171    }
172
173    fn get_config_mut(&mut self) -> &mut ComposeConfig {
174        &mut self.config
175    }
176
177    fn subcommand(&self) -> &'static str {
178        "logs"
179    }
180
181    fn build_subcommand_args(&self) -> Vec<String> {
182        let mut args = Vec::new();
183
184        if self.follow {
185            args.push("--follow".to_string());
186        }
187
188        if self.timestamps {
189            args.push("--timestamps".to_string());
190        }
191
192        if let Some(ref tail) = self.tail {
193            args.push("--tail".to_string());
194            args.push(tail.clone());
195        }
196
197        if let Some(ref since) = self.since {
198            args.push("--since".to_string());
199            args.push(since.clone());
200        }
201
202        if let Some(ref until) = self.until {
203            args.push("--until".to_string());
204            args.push(until.clone());
205        }
206
207        if self.no_log_prefix {
208            args.push("--no-log-prefix".to_string());
209        }
210
211        if self.no_color {
212            args.push("--no-color".to_string());
213        }
214
215        // Add service names at the end
216        args.extend(self.services.clone());
217
218        args
219    }
220}
221
222impl ComposeLogsResult {
223    /// Check if the command was successful
224    #[must_use]
225    pub fn success(&self) -> bool {
226        self.success
227    }
228
229    /// Get the services logs were fetched for
230    #[must_use]
231    pub fn services(&self) -> &[String] {
232        &self.services
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_compose_logs_basic() {
242        let cmd = ComposeLogsCommand::new();
243        let args = cmd.build_subcommand_args();
244        assert!(args.is_empty());
245
246        let full_args = ComposeCommand::build_command_args(&cmd);
247        assert_eq!(full_args[0], "compose");
248        assert!(full_args.contains(&"logs".to_string()));
249    }
250
251    #[test]
252    fn test_compose_logs_follow() {
253        let cmd = ComposeLogsCommand::new().follow().timestamps();
254        let args = cmd.build_subcommand_args();
255        assert_eq!(args, vec!["--follow", "--timestamps"]);
256    }
257
258    #[test]
259    fn test_compose_logs_with_tail() {
260        let cmd = ComposeLogsCommand::new().tail("100").service("web");
261        let args = cmd.build_subcommand_args();
262        assert_eq!(args, vec!["--tail", "100", "web"]);
263    }
264
265    #[test]
266    fn test_compose_logs_with_services() {
267        let cmd = ComposeLogsCommand::new()
268            .services(vec!["web", "db"])
269            .follow();
270
271        let args = cmd.build_subcommand_args();
272        assert!(args.contains(&"--follow".to_string()));
273        assert!(args.contains(&"web".to_string()));
274        assert!(args.contains(&"db".to_string()));
275    }
276
277    #[test]
278    fn test_compose_logs_all_options() {
279        let cmd = ComposeLogsCommand::new()
280            .follow()
281            .timestamps()
282            .tail("50")
283            .since("2024-01-01T00:00:00")
284            .until("2024-01-02T00:00:00")
285            .no_color()
286            .no_log_prefix()
287            .service("web")
288            .service("db");
289
290        let args = cmd.build_subcommand_args();
291        assert!(args.contains(&"--follow".to_string()));
292        assert!(args.contains(&"--timestamps".to_string()));
293        assert!(args.contains(&"--tail".to_string()));
294        assert!(args.contains(&"50".to_string()));
295        assert!(args.contains(&"--since".to_string()));
296        assert!(args.contains(&"2024-01-01T00:00:00".to_string()));
297        assert!(args.contains(&"--until".to_string()));
298        assert!(args.contains(&"2024-01-02T00:00:00".to_string()));
299        assert!(args.contains(&"--no-color".to_string()));
300        assert!(args.contains(&"--no-log-prefix".to_string()));
301        assert!(args.contains(&"web".to_string()));
302        assert!(args.contains(&"db".to_string()));
303    }
304
305    #[test]
306    fn test_compose_config_integration() {
307        let cmd = ComposeLogsCommand::new()
308            .file("docker-compose.yml")
309            .project_name("my-project")
310            .follow()
311            .service("api");
312
313        let args = ComposeCommand::build_command_args(&cmd);
314        assert!(args.contains(&"--file".to_string()));
315        assert!(args.contains(&"docker-compose.yml".to_string()));
316        assert!(args.contains(&"--project-name".to_string()));
317        assert!(args.contains(&"my-project".to_string()));
318        assert!(args.contains(&"--follow".to_string()));
319        assert!(args.contains(&"api".to_string()));
320    }
321}