docker_wrapper/command/compose/
port.rs

1//! Docker Compose port command implementation using unified trait pattern.
2
3use crate::command::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Docker Compose port command builder
8#[derive(Debug, Clone)]
9pub struct ComposePortCommand {
10    /// Base command executor
11    pub executor: CommandExecutor,
12    /// Base compose configuration
13    pub config: ComposeConfig,
14    /// Service name
15    pub service: String,
16    /// Private port to query
17    pub private_port: Option<u16>,
18    /// Protocol (tcp/udp)
19    pub protocol: Option<String>,
20    /// Index of container if service has multiple instances
21    pub index: Option<u16>,
22}
23
24/// Result from compose port command
25#[derive(Debug, Clone)]
26pub struct ComposePortResult {
27    /// Raw stdout output
28    pub stdout: String,
29    /// Raw stderr output
30    pub stderr: String,
31    /// Success status
32    pub success: bool,
33    /// Service that was queried
34    pub service: String,
35    /// Port mappings found
36    pub port_mappings: Vec<String>,
37}
38
39impl ComposePortCommand {
40    /// Create a new compose port command
41    #[must_use]
42    pub fn new(service: impl Into<String>) -> Self {
43        Self {
44            executor: CommandExecutor::new(),
45            config: ComposeConfig::new(),
46            service: service.into(),
47            private_port: None,
48            protocol: None,
49            index: None,
50        }
51    }
52
53    /// Set private port to query
54    #[must_use]
55    pub fn private_port(mut self, port: u16) -> Self {
56        self.private_port = Some(port);
57        self
58    }
59
60    /// Set protocol (tcp or udp)
61    #[must_use]
62    pub fn protocol(mut self, protocol: impl Into<String>) -> Self {
63        self.protocol = Some(protocol.into());
64        self
65    }
66
67    /// Set container index if service has multiple instances
68    #[must_use]
69    pub fn index(mut self, index: u16) -> Self {
70        self.index = Some(index);
71        self
72    }
73}
74
75#[async_trait]
76impl DockerCommand for ComposePortCommand {
77    type Output = ComposePortResult;
78
79    fn get_executor(&self) -> &CommandExecutor {
80        &self.executor
81    }
82
83    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
84        &mut self.executor
85    }
86
87    fn build_command_args(&self) -> Vec<String> {
88        <Self as ComposeCommand>::build_command_args(self)
89    }
90
91    async fn execute(&self) -> Result<Self::Output> {
92        let args = <Self as ComposeCommand>::build_command_args(self);
93        let output = self.execute_command(args).await?;
94
95        let port_mappings = output
96            .stdout
97            .lines()
98            .filter(|line| !line.trim().is_empty())
99            .map(|line| line.trim().to_string())
100            .collect();
101
102        Ok(ComposePortResult {
103            stdout: output.stdout,
104            stderr: output.stderr,
105            success: output.success,
106            service: self.service.clone(),
107            port_mappings,
108        })
109    }
110}
111
112impl ComposeCommand for ComposePortCommand {
113    fn get_config(&self) -> &ComposeConfig {
114        &self.config
115    }
116
117    fn get_config_mut(&mut self) -> &mut ComposeConfig {
118        &mut self.config
119    }
120
121    fn subcommand(&self) -> &'static str {
122        "port"
123    }
124
125    fn build_subcommand_args(&self) -> Vec<String> {
126        let mut args = Vec::new();
127
128        if let Some(protocol) = &self.protocol {
129            args.push("--protocol".to_string());
130            args.push(protocol.clone());
131        }
132
133        if let Some(index) = self.index {
134            args.push("--index".to_string());
135            args.push(index.to_string());
136        }
137
138        args.push(self.service.clone());
139
140        if let Some(port) = self.private_port {
141            args.push(port.to_string());
142        }
143
144        args
145    }
146}
147
148impl ComposePortResult {
149    /// Check if the command was successful
150    #[must_use]
151    pub fn success(&self) -> bool {
152        self.success
153    }
154
155    /// Get the service that was queried
156    #[must_use]
157    pub fn service(&self) -> &str {
158        &self.service
159    }
160
161    /// Get port mappings
162    #[must_use]
163    pub fn port_mappings(&self) -> &[String] {
164        &self.port_mappings
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_compose_port_basic() {
174        let cmd = ComposePortCommand::new("web");
175        let args = cmd.build_subcommand_args();
176        assert!(args.contains(&"web".to_string()));
177
178        let full_args = ComposeCommand::build_command_args(&cmd);
179        assert_eq!(full_args[0], "compose");
180        assert!(full_args.contains(&"port".to_string()));
181    }
182
183    #[test]
184    fn test_compose_port_with_options() {
185        let cmd = ComposePortCommand::new("api")
186            .private_port(8080)
187            .protocol("tcp")
188            .index(1);
189
190        let args = cmd.build_subcommand_args();
191        assert!(args.contains(&"--protocol".to_string()));
192        assert!(args.contains(&"tcp".to_string()));
193        assert!(args.contains(&"--index".to_string()));
194        assert!(args.contains(&"1".to_string()));
195        assert!(args.contains(&"api".to_string()));
196        assert!(args.contains(&"8080".to_string()));
197    }
198}