docker_wrapper/command/generic.rs
1//! Generic Docker command for executing any Docker CLI command.
2//!
3//! This module provides an escape hatch for running arbitrary Docker commands,
4//! including plugin commands, experimental features, or commands not yet
5//! implemented in this library.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use docker_wrapper::{DockerCommand, GenericCommand};
11//!
12//! # #[tokio::main]
13//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
14//! // Run a plugin command
15//! let output = GenericCommand::new("scan")
16//! .arg("alpine:latest")
17//! .execute()
18//! .await?;
19//!
20//! println!("{}", output.stdout);
21//!
22//! // Run an experimental command
23//! let output = GenericCommand::new("debug")
24//! .args(["container-id", "--shell", "bash"])
25//! .execute()
26//! .await?;
27//! # Ok(())
28//! # }
29//! ```
30
31use crate::command::{CommandExecutor, CommandOutput, DockerCommand};
32use crate::error::Result;
33use async_trait::async_trait;
34
35/// Generic Docker command for executing any Docker CLI command.
36///
37/// This provides an escape hatch for running arbitrary Docker commands that
38/// may not have dedicated command types in this library, such as:
39///
40/// - Plugin commands (e.g., `docker scan`, `docker scout`)
41/// - Experimental features
42/// - Future commands not yet implemented
43/// - Custom or third-party extensions
44///
45/// # Example
46///
47/// ```rust,no_run
48/// use docker_wrapper::{DockerCommand, GenericCommand};
49///
50/// # #[tokio::main]
51/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
52/// // Execute a plugin command
53/// let result = GenericCommand::new("scout")
54/// .arg("cves")
55/// .arg("alpine:latest")
56/// .execute()
57/// .await?;
58///
59/// if result.success {
60/// println!("Scan complete:\n{}", result.stdout);
61/// }
62///
63/// // Execute with multiple arguments
64/// let result = GenericCommand::new("trust")
65/// .args(["inspect", "--pretty", "alpine:latest"])
66/// .execute()
67/// .await?;
68/// # Ok(())
69/// # }
70/// ```
71#[derive(Debug, Clone)]
72pub struct GenericCommand {
73 /// The Docker subcommand to execute
74 command: String,
75 /// Arguments to pass to the command
76 args: Vec<String>,
77 /// Command executor
78 pub executor: CommandExecutor,
79}
80
81impl GenericCommand {
82 /// Create a new generic command.
83 ///
84 /// # Arguments
85 ///
86 /// * `command` - The Docker subcommand to execute (e.g., "scan", "scout", "trust")
87 ///
88 /// # Example
89 ///
90 /// ```rust,no_run
91 /// use docker_wrapper::GenericCommand;
92 ///
93 /// let cmd = GenericCommand::new("scan");
94 /// ```
95 #[must_use]
96 pub fn new(command: impl Into<String>) -> Self {
97 Self {
98 command: command.into(),
99 args: Vec::new(),
100 executor: CommandExecutor::new(),
101 }
102 }
103
104 /// Add a single argument to the command.
105 ///
106 /// # Example
107 ///
108 /// ```rust,no_run
109 /// use docker_wrapper::GenericCommand;
110 ///
111 /// let cmd = GenericCommand::new("scan")
112 /// .arg("--severity")
113 /// .arg("high")
114 /// .arg("alpine:latest");
115 /// ```
116 #[must_use]
117 pub fn arg(mut self, arg: impl Into<String>) -> Self {
118 self.args.push(arg.into());
119 self
120 }
121
122 /// Add multiple arguments to the command.
123 ///
124 /// # Example
125 ///
126 /// ```rust,no_run
127 /// use docker_wrapper::GenericCommand;
128 ///
129 /// let cmd = GenericCommand::new("scout")
130 /// .args(["cves", "--format", "json", "alpine:latest"]);
131 /// ```
132 #[must_use]
133 pub fn args<I, S>(mut self, args: I) -> Self
134 where
135 I: IntoIterator<Item = S>,
136 S: Into<String>,
137 {
138 self.args.extend(args.into_iter().map(Into::into));
139 self
140 }
141
142 /// Build the command arguments.
143 fn build_args(&self) -> Vec<String> {
144 let mut args = vec![self.command.clone()];
145 args.extend(self.args.clone());
146 args.extend(self.executor.raw_args.clone());
147 args
148 }
149}
150
151#[async_trait]
152impl DockerCommand for GenericCommand {
153 type Output = CommandOutput;
154
155 fn get_executor(&self) -> &CommandExecutor {
156 &self.executor
157 }
158
159 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
160 &mut self.executor
161 }
162
163 fn build_command_args(&self) -> Vec<String> {
164 self.build_args()
165 }
166
167 async fn execute(&self) -> Result<Self::Output> {
168 let args = self.build_args();
169 self.execute_command(args).await
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_generic_command_basic() {
179 let cmd = GenericCommand::new("scan");
180 let args = cmd.build_args();
181 assert_eq!(args, vec!["scan"]);
182 }
183
184 #[test]
185 fn test_generic_command_with_arg() {
186 let cmd = GenericCommand::new("scan").arg("alpine:latest");
187 let args = cmd.build_args();
188 assert_eq!(args, vec!["scan", "alpine:latest"]);
189 }
190
191 #[test]
192 fn test_generic_command_with_multiple_args() {
193 let cmd = GenericCommand::new("scan")
194 .arg("--severity")
195 .arg("high")
196 .arg("alpine:latest");
197 let args = cmd.build_args();
198 assert_eq!(args, vec!["scan", "--severity", "high", "alpine:latest"]);
199 }
200
201 #[test]
202 fn test_generic_command_with_args_iterator() {
203 let cmd = GenericCommand::new("scout").args(["cves", "--format", "json", "alpine:latest"]);
204 let args = cmd.build_args();
205 assert_eq!(
206 args,
207 vec!["scout", "cves", "--format", "json", "alpine:latest"]
208 );
209 }
210
211 #[test]
212 fn test_generic_command_complex() {
213 let cmd = GenericCommand::new("trust")
214 .arg("inspect")
215 .args(["--pretty", "alpine:latest"]);
216 let args = cmd.build_args();
217 assert_eq!(args, vec!["trust", "inspect", "--pretty", "alpine:latest"]);
218 }
219
220 #[test]
221 fn test_generic_command_subcommand_with_spaces() {
222 // Test multi-word commands like "container ls"
223 let cmd = GenericCommand::new("container").args(["ls", "-a"]);
224 let args = cmd.build_args();
225 assert_eq!(args, vec!["container", "ls", "-a"]);
226 }
227}