docker_wrapper/command/
port.rs

1//! Docker port command implementation.
2//!
3//! This module provides the `docker port` command for listing port mappings.
4
5use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8
9/// Docker port command builder
10///
11/// List port mappings or a specific mapping for a container.
12///
13/// # Example
14///
15/// ```no_run
16/// use docker_wrapper::PortCommand;
17///
18/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
19/// // List all port mappings
20/// let ports = PortCommand::new("my-container")
21///     .run()
22///     .await?;
23///
24/// // Get specific port mapping
25/// let port = PortCommand::new("my-container")
26///     .port(80)
27///     .run()
28///     .await?;
29/// # Ok(())
30/// # }
31/// ```
32#[derive(Debug, Clone)]
33pub struct PortCommand {
34    /// Container name or ID
35    container: String,
36    /// Specific port to query
37    port: Option<u16>,
38    /// Command executor
39    pub executor: CommandExecutor,
40}
41
42impl PortCommand {
43    /// Create a new port command
44    ///
45    /// # Example
46    ///
47    /// ```
48    /// use docker_wrapper::PortCommand;
49    ///
50    /// let cmd = PortCommand::new("my-container");
51    /// ```
52    #[must_use]
53    pub fn new(container: impl Into<String>) -> Self {
54        Self {
55            container: container.into(),
56            port: None,
57            executor: CommandExecutor::new(),
58        }
59    }
60
61    /// Query specific port mapping
62    ///
63    /// # Example
64    ///
65    /// ```
66    /// use docker_wrapper::PortCommand;
67    ///
68    /// let cmd = PortCommand::new("my-container")
69    ///     .port(80);
70    /// ```
71    #[must_use]
72    pub fn port(mut self, port: u16) -> Self {
73        self.port = Some(port);
74        self
75    }
76
77    /// Execute the port command
78    ///
79    /// # Errors
80    /// Returns an error if:
81    /// - The Docker daemon is not running
82    /// - The container doesn't exist
83    ///
84    /// # Example
85    ///
86    /// ```no_run
87    /// use docker_wrapper::PortCommand;
88    ///
89    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
90    /// let result = PortCommand::new("my-container")
91    ///     .run()
92    ///     .await?;
93    ///
94    /// if result.success() {
95    ///     for mapping in result.port_mappings() {
96    ///         println!("{}:{} -> {}", mapping.host_ip, mapping.host_port, mapping.container_port);
97    ///     }
98    /// }
99    /// # Ok(())
100    /// # }
101    /// ```
102    pub async fn run(&self) -> Result<PortResult> {
103        let output = self.execute().await?;
104
105        // Parse port mappings from output
106        let port_mappings = Self::parse_port_mappings(&output.stdout);
107
108        Ok(PortResult {
109            output,
110            container: self.container.clone(),
111            port_mappings,
112        })
113    }
114
115    /// Parse port mappings from command output
116    fn parse_port_mappings(stdout: &str) -> Vec<PortMapping> {
117        let mut mappings = Vec::new();
118
119        for line in stdout.lines() {
120            let line = line.trim();
121            if line.is_empty() {
122                continue;
123            }
124
125            // Format: "80/tcp -> 0.0.0.0:8080"
126            if let Some((container_part, host_part)) = line.split_once(" -> ") {
127                if let Some((port_str, protocol)) = container_part.split_once('/') {
128                    if let Ok(container_port) = port_str.parse::<u16>() {
129                        if let Some((host_ip, host_port_str)) = host_part.rsplit_once(':') {
130                            if let Ok(host_port) = host_port_str.parse::<u16>() {
131                                mappings.push(PortMapping {
132                                    container_port,
133                                    host_ip: host_ip.to_string(),
134                                    host_port,
135                                    protocol: protocol.to_string(),
136                                });
137                            }
138                        }
139                    }
140                }
141            }
142        }
143
144        mappings
145    }
146}
147
148#[async_trait]
149impl DockerCommand for PortCommand {
150    type Output = CommandOutput;
151
152    fn build_command_args(&self) -> Vec<String> {
153        let mut args = vec!["port".to_string(), self.container.clone()];
154
155        if let Some(port) = self.port {
156            args.push(port.to_string());
157        }
158
159        args.extend(self.executor.raw_args.clone());
160        args
161    }
162
163    fn get_executor(&self) -> &CommandExecutor {
164        &self.executor
165    }
166
167    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
168        &mut self.executor
169    }
170
171    async fn execute(&self) -> Result<Self::Output> {
172        let args = self.build_command_args();
173        let command_name = args[0].clone();
174        let command_args = args[1..].to_vec();
175        self.executor
176            .execute_command(&command_name, command_args)
177            .await
178    }
179}
180
181/// Result from the port command
182#[derive(Debug, Clone)]
183pub struct PortResult {
184    /// Raw command output
185    pub output: CommandOutput,
186    /// Container that was queried
187    pub container: String,
188    /// Parsed port mappings
189    pub port_mappings: Vec<PortMapping>,
190}
191
192impl PortResult {
193    /// Check if the port command was successful
194    #[must_use]
195    pub fn success(&self) -> bool {
196        self.output.success
197    }
198
199    /// Get the container name
200    #[must_use]
201    pub fn container(&self) -> &str {
202        &self.container
203    }
204
205    /// Get the port mappings
206    #[must_use]
207    pub fn port_mappings(&self) -> &[PortMapping] {
208        &self.port_mappings
209    }
210
211    /// Get port mapping count
212    #[must_use]
213    pub fn mapping_count(&self) -> usize {
214        self.port_mappings.len()
215    }
216}
217
218/// Port mapping information
219#[derive(Debug, Clone)]
220pub struct PortMapping {
221    /// Container port
222    pub container_port: u16,
223    /// Host IP address
224    pub host_ip: String,
225    /// Host port
226    pub host_port: u16,
227    /// Protocol (tcp/udp)
228    pub protocol: String,
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn test_port_basic() {
237        let cmd = PortCommand::new("test-container");
238        let args = cmd.build_command_args();
239        assert_eq!(args, vec!["port", "test-container"]);
240    }
241
242    #[test]
243    fn test_port_with_specific_port() {
244        let cmd = PortCommand::new("test-container").port(80);
245        let args = cmd.build_command_args();
246        assert_eq!(args, vec!["port", "test-container", "80"]);
247    }
248
249    #[test]
250    fn test_parse_port_mappings() {
251        let output = "80/tcp -> 0.0.0.0:8080\n443/tcp -> 127.0.0.1:8443";
252        let mappings = PortCommand::parse_port_mappings(output);
253
254        assert_eq!(mappings.len(), 2);
255        assert_eq!(mappings[0].container_port, 80);
256        assert_eq!(mappings[0].host_ip, "0.0.0.0");
257        assert_eq!(mappings[0].host_port, 8080);
258        assert_eq!(mappings[0].protocol, "tcp");
259
260        assert_eq!(mappings[1].container_port, 443);
261        assert_eq!(mappings[1].host_ip, "127.0.0.1");
262        assert_eq!(mappings[1].host_port, 8443);
263        assert_eq!(mappings[1].protocol, "tcp");
264    }
265
266    #[test]
267    fn test_parse_port_mappings_empty() {
268        let mappings = PortCommand::parse_port_mappings("");
269        assert!(mappings.is_empty());
270    }
271}