docker_wrapper/command/
compose_logs.rs1use super::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6
7#[derive(Debug, Clone)]
9#[allow(clippy::struct_excessive_bools)] pub struct ComposeLogsCommand {
11 pub executor: CommandExecutor,
13 pub config: ComposeConfig,
15 pub services: Vec<String>,
17 pub follow: bool,
19 pub timestamps: bool,
21 pub tail: Option<String>,
23 pub since: Option<String>,
25 pub until: Option<String>,
27 pub no_log_prefix: bool,
29 pub no_color: bool,
31}
32
33#[derive(Debug, Clone)]
35pub struct ComposeLogsResult {
36 pub stdout: String,
38 pub stderr: String,
40 pub success: bool,
42 pub services: Vec<String>,
44}
45
46impl ComposeLogsCommand {
47 #[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 #[must_use]
66 pub fn service(mut self, service: impl Into<String>) -> Self {
67 self.services.push(service.into());
68 self
69 }
70
71 #[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 #[must_use]
84 pub fn follow(mut self) -> Self {
85 self.follow = true;
86 self
87 }
88
89 #[must_use]
91 pub fn timestamps(mut self) -> Self {
92 self.timestamps = true;
93 self
94 }
95
96 #[must_use]
98 pub fn tail(mut self, lines: impl Into<String>) -> Self {
99 self.tail = Some(lines.into());
100 self
101 }
102
103 #[must_use]
105 pub fn since(mut self, timestamp: impl Into<String>) -> Self {
106 self.since = Some(timestamp.into());
107 self
108 }
109
110 #[must_use]
112 pub fn until(mut self, timestamp: impl Into<String>) -> Self {
113 self.until = Some(timestamp.into());
114 self
115 }
116
117 #[must_use]
119 pub fn no_log_prefix(mut self) -> Self {
120 self.no_log_prefix = true;
121 self
122 }
123
124 #[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 <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 args.extend(self.services.clone());
217
218 args
219 }
220}
221
222impl ComposeLogsResult {
223 #[must_use]
225 pub fn success(&self) -> bool {
226 self.success
227 }
228
229 #[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}