docker_wrapper/compose/
port.rs1use crate::compose::{ComposeCommandV2 as ComposeCommand, ComposeConfig};
4use crate::error::Result;
5use async_trait::async_trait;
6
7#[derive(Debug, Clone, Default)]
11pub struct ComposePortCommand {
12 pub config: ComposeConfig,
14 pub service: String,
16 pub private_port: u16,
18 pub protocol: Option<String>,
20 pub index: Option<u32>,
22}
23
24#[derive(Debug, Clone)]
26pub struct PortResult {
27 pub binding: String,
29 pub success: bool,
31}
32
33impl ComposePortCommand {
34 #[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 #[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 #[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 #[must_use]
60 pub fn protocol(mut self, protocol: impl Into<String>) -> Self {
61 self.protocol = Some(protocol.into());
62 self
63 }
64
65 #[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 if let Some(index) = self.index {
77 args.push("--index".to_string());
78 args.push(index.to_string());
79 }
80
81 if let Some(protocol) = &self.protocol {
83 args.push("--protocol".to_string());
84 args.push(protocol.clone());
85 }
86
87 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 #[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 #[must_use]
137 pub fn port(&self) -> Option<u16> {
138 self.parse_binding().map(|(_, port)| port)
139 }
140
141 #[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}