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}