docker_wrapper/compose/
port.rs

1//! Docker Compose port command implementation.
2
3use crate::compose::{ComposeCommandV2 as ComposeCommand, ComposeConfig};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Docker Compose port command
8///
9/// Print the public port for a port binding.
10#[derive(Debug, Clone, Default)]
11pub struct ComposePortCommand {
12    /// Base configuration
13    pub config: ComposeConfig,
14    /// Service name
15    pub service: String,
16    /// Private port number
17    pub private_port: u16,
18    /// Protocol (tcp/udp)
19    pub protocol: Option<String>,
20    /// Index of the container (if service has multiple instances)
21    pub index: Option<u32>,
22}
23
24/// Result from port command
25#[derive(Debug, Clone)]
26pub struct PortResult {
27    /// The public port binding (host:port)
28    pub binding: String,
29    /// Whether the operation succeeded
30    pub success: bool,
31}
32
33impl ComposePortCommand {
34    /// Create a new port command
35    #[must_use]
36    pub fn new(service: impl Into<String>, private_port: u16) -> Self {
37        Self {
38            service: service.into(),
39            private_port,
40            ..Default::default()
41        }
42    }
43
44    /// Add a compose file
45    #[must_use]
46    pub fn file<P: Into<std::path::PathBuf>>(mut self, file: P) -> Self {
47        self.config.files.push(file.into());
48        self
49    }
50
51    /// Set project name
52    #[must_use]
53    pub fn project_name(mut self, name: impl Into<String>) -> Self {
54        self.config.project_name = Some(name.into());
55        self
56    }
57
58    /// Set protocol (tcp/udp)
59    #[must_use]
60    pub fn protocol(mut self, protocol: impl Into<String>) -> Self {
61        self.protocol = Some(protocol.into());
62        self
63    }
64
65    /// Set container index
66    #[must_use]
67    pub fn index(mut self, index: u32) -> Self {
68        self.index = Some(index);
69        self
70    }
71
72    fn build_args(&self) -> Vec<String> {
73        let mut args = vec!["port".to_string()];
74
75        // Add index if specified
76        if let Some(index) = self.index {
77            args.push("--index".to_string());
78            args.push(index.to_string());
79        }
80
81        // Add protocol if specified
82        if let Some(protocol) = &self.protocol {
83            args.push("--protocol".to_string());
84            args.push(protocol.clone());
85        }
86
87        // Add service and port
88        args.push(self.service.clone());
89        args.push(self.private_port.to_string());
90
91        args
92    }
93}
94
95#[async_trait]
96impl ComposeCommand for ComposePortCommand {
97    type Output = PortResult;
98
99    fn get_config(&self) -> &ComposeConfig {
100        &self.config
101    }
102
103    fn get_config_mut(&mut self) -> &mut ComposeConfig {
104        &mut self.config
105    }
106
107    async fn execute_compose(&self, args: Vec<String>) -> Result<Self::Output> {
108        let output = self.execute_compose_command(args).await?;
109
110        Ok(PortResult {
111            binding: output.stdout.trim().to_string(),
112            success: output.success,
113        })
114    }
115
116    async fn execute(&self) -> Result<Self::Output> {
117        let args = self.build_args();
118        self.execute_compose(args).await
119    }
120}
121
122impl PortResult {
123    /// Parse the binding into host and port
124    #[must_use]
125    pub fn parse_binding(&self) -> Option<(String, u16)> {
126        let parts: Vec<&str> = self.binding.split(':').collect();
127        if parts.len() == 2 {
128            if let Ok(port) = parts[1].parse::<u16>() {
129                return Some((parts[0].to_string(), port));
130            }
131        }
132        None
133    }
134
135    /// Get just the port number
136    #[must_use]
137    pub fn port(&self) -> Option<u16> {
138        self.parse_binding().map(|(_, port)| port)
139    }
140
141    /// Get just the host
142    #[must_use]
143    pub fn host(&self) -> Option<String> {
144        self.parse_binding().map(|(host, _)| host)
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_port_command_basic() {
154        let cmd = ComposePortCommand::new("web", 80);
155        let args = cmd.build_args();
156        assert_eq!(args[0], "port");
157        assert!(args.contains(&"web".to_string()));
158        assert!(args.contains(&"80".to_string()));
159    }
160
161    #[test]
162    fn test_port_command_with_protocol() {
163        let cmd = ComposePortCommand::new("web", 53).protocol("udp");
164        let args = cmd.build_args();
165        assert!(args.contains(&"--protocol".to_string()));
166        assert!(args.contains(&"udp".to_string()));
167    }
168
169    #[test]
170    fn test_port_command_with_index() {
171        let cmd = ComposePortCommand::new("web", 8080).index(2);
172        let args = cmd.build_args();
173        assert!(args.contains(&"--index".to_string()));
174        assert!(args.contains(&"2".to_string()));
175    }
176
177    #[test]
178    fn test_port_result_parsing() {
179        let result = PortResult {
180            binding: "0.0.0.0:32768".to_string(),
181            success: true,
182        };
183
184        assert_eq!(result.port(), Some(32768));
185        assert_eq!(result.host(), Some("0.0.0.0".to_string()));
186
187        let (host, port) = result.parse_binding().unwrap();
188        assert_eq!(host, "0.0.0.0");
189        assert_eq!(port, 32768);
190    }
191}