1use std::fmt;
2use std::net::{IpAddr, SocketAddr};
3
4use serde::{Deserialize, Serialize};
5use url::Url;
6
7const DUAL_STACK_IP_V4_MAPPED_V6_PREFIX: &str = "::ffff:";
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
11pub enum Protocol {
12 UDP,
13 HTTP,
14 HTTPS,
15}
16
17impl fmt::Display for Protocol {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 let proto_str = match self {
20 Protocol::UDP => "udp",
21 Protocol::HTTP => "http",
22 Protocol::HTTPS => "https",
23 };
24 write!(f, "{proto_str}")
25 }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
29pub enum IpType {
30 Plain,
32
33 V4MappedV6,
39}
40
41impl fmt::Display for IpType {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 let ip_type_str = match self {
44 Self::Plain => "plain",
45 Self::V4MappedV6 => "v4_mapped_v6",
46 };
47 write!(f, "{ip_type_str}")
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
52pub enum IpFamily {
53 Inet,
55 Inet6,
57}
58
59impl fmt::Display for IpFamily {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 let ip_family_str = match self {
62 Self::Inet => "inet",
63 Self::Inet6 => "inet6",
64 };
65 write!(f, "{ip_family_str}")
66 }
67}
68
69impl From<IpAddr> for IpFamily {
70 fn from(ip: IpAddr) -> Self {
71 if ip.is_ipv4() {
72 return IpFamily::Inet;
73 }
74
75 if ip.is_ipv6() {
76 return IpFamily::Inet6;
77 }
78
79 panic!("Unsupported IP address type: {ip}");
80 }
81}
82
83#[derive(thiserror::Error, Debug, Clone)]
84pub enum Error {
85 #[error("The port number cannot be zero. It must be an assigned valid port.")]
86 PortZeroNotAllowed,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
123pub struct ServiceBinding {
124 protocol: Protocol,
126
127 bind_address: SocketAddr,
129}
130
131impl ServiceBinding {
132 pub fn new(protocol: Protocol, bind_address: SocketAddr) -> Result<Self, Error> {
136 if bind_address.port() == 0 {
137 return Err(Error::PortZeroNotAllowed);
138 }
139
140 Ok(Self { protocol, bind_address })
141 }
142
143 #[must_use]
145 pub fn protocol(&self) -> Protocol {
146 self.protocol.clone()
147 }
148
149 #[must_use]
150 pub fn bind_address(&self) -> SocketAddr {
151 self.bind_address
152 }
153
154 #[must_use]
155 pub fn bind_address_ip_type(&self) -> IpType {
156 if self.is_v4_mapped_v6() {
157 return IpType::V4MappedV6;
158 }
159
160 IpType::Plain
161 }
162
163 #[must_use]
164 pub fn bind_address_ip_family(&self) -> IpFamily {
165 self.bind_address.ip().into()
166 }
167
168 #[must_use]
172 pub fn url(&self) -> Url {
173 Url::parse(&format!("{}://{}", self.protocol, self.bind_address))
174 .expect("Service binding can always be parsed into a URL")
175 }
176
177 fn is_v4_mapped_v6(&self) -> bool {
178 self.bind_address.ip().is_ipv6()
179 && self
180 .bind_address
181 .ip()
182 .to_string()
183 .starts_with(DUAL_STACK_IP_V4_MAPPED_V6_PREFIX)
184 }
185}
186
187impl From<ServiceBinding> for Url {
188 fn from(service_binding: ServiceBinding) -> Self {
189 service_binding.url()
190 }
191}
192
193impl fmt::Display for ServiceBinding {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 write!(f, "{}", self.url())
196 }
197}
198
199#[cfg(test)]
200mod tests {
201
202 mod the_service_binding {
203 use std::net::SocketAddr;
204 use std::str::FromStr;
205
206 use rstest::rstest;
207 use url::Url;
208
209 use crate::service_binding::{Error, IpType, Protocol, ServiceBinding};
210
211 #[rstest]
212 #[case("wildcard_ip", Protocol::UDP, SocketAddr::from_str("0.0.0.0:6969").unwrap())]
213 #[case("udp_service", Protocol::UDP, SocketAddr::from_str("127.0.0.1:6969").unwrap())]
214 #[case("http_service", Protocol::HTTP, SocketAddr::from_str("127.0.0.1:7070").unwrap())]
215 #[case("https_service", Protocol::HTTPS, SocketAddr::from_str("127.0.0.1:7070").unwrap())]
216 fn should_allow_a_subset_of_urls(#[case] case: &str, #[case] protocol: Protocol, #[case] bind_address: SocketAddr) {
217 let service_binding = ServiceBinding::new(protocol.clone(), bind_address);
218
219 assert!(service_binding.is_ok(), "{}", format!("{case} failed: {service_binding:?}"));
220 }
221
222 #[test]
223 fn should_not_allow_undefined_port_zero() {
224 let service_binding = ServiceBinding::new(Protocol::UDP, SocketAddr::from_str("127.0.0.1:0").unwrap());
225
226 assert!(matches!(service_binding, Err(Error::PortZeroNotAllowed)));
227 }
228
229 #[test]
230 fn should_return_the_bind_address() {
231 let service_binding = ServiceBinding::new(Protocol::UDP, SocketAddr::from_str("127.0.0.1:6969").unwrap()).unwrap();
232
233 assert_eq!(
234 service_binding.bind_address(),
235 SocketAddr::from_str("127.0.0.1:6969").unwrap()
236 );
237 }
238
239 #[test]
240 fn should_return_the_bind_address_plain_type_for_ipv4_ips() {
241 let service_binding = ServiceBinding::new(Protocol::UDP, SocketAddr::from_str("127.0.0.1:6969").unwrap()).unwrap();
242
243 assert_eq!(service_binding.bind_address_ip_type(), IpType::Plain);
244 }
245
246 #[test]
247 fn should_return_the_bind_address_plain_type_for_ipv6_ips() {
248 let service_binding =
249 ServiceBinding::new(Protocol::UDP, SocketAddr::from_str("[0:0:0:0:0:0:0:1]:6969").unwrap()).unwrap();
250
251 assert_eq!(service_binding.bind_address_ip_type(), IpType::Plain);
252 }
253
254 #[test]
255 fn should_return_the_bind_address_v4_mapped_v7_type_for_ipv4_ips_mapped_to_ipv6() {
256 let service_binding =
257 ServiceBinding::new(Protocol::UDP, SocketAddr::from_str("[::ffff:192.0.2.33]:6969").unwrap()).unwrap();
258
259 assert_eq!(service_binding.bind_address_ip_type(), IpType::V4MappedV6);
260 }
261
262 #[test]
263 fn should_return_the_corresponding_url() {
264 let service_binding = ServiceBinding::new(Protocol::UDP, SocketAddr::from_str("127.0.0.1:6969").unwrap()).unwrap();
265
266 assert_eq!(service_binding.url(), Url::parse("udp://127.0.0.1:6969").unwrap());
267 }
268
269 #[test]
270 fn should_be_converted_into_an_url() {
271 let service_binding = ServiceBinding::new(Protocol::UDP, SocketAddr::from_str("127.0.0.1:6969").unwrap()).unwrap();
272
273 let url: Url = service_binding.clone().into();
274
275 assert_eq!(url, Url::parse("udp://127.0.0.1:6969").unwrap());
276 }
277
278 #[rstest]
279 #[case("udp_service", Protocol::UDP, SocketAddr::from_str("127.0.0.1:6969").unwrap(), "udp://127.0.0.1:6969")]
280 #[case("http_service", Protocol::HTTP, SocketAddr::from_str("127.0.0.1:7070").unwrap(), "http://127.0.0.1:7070/")]
281 #[case("https_service", Protocol::HTTPS, SocketAddr::from_str("127.0.0.1:7070").unwrap(), "https://127.0.0.1:7070/")]
282 fn should_always_have_a_corresponding_unique_url(
283 #[case] case: &str,
284 #[case] protocol: Protocol,
285 #[case] bind_address: SocketAddr,
286 #[case] expected_url: String,
287 ) {
288 let service_binding = ServiceBinding::new(protocol.clone(), bind_address).unwrap();
289
290 assert_eq!(
291 service_binding.url().to_string(),
292 expected_url,
293 "{case} failed: {service_binding:?}",
294 );
295 }
296 }
297}