Repository: plabayo/rama (language: rust)

Issue:
Add SocketAddress to rama-net
It's pretty much like Authority from the same rama-net package (https://ramaproxy.org/docs/rama/net/address/index.html)

But it is a IpAddr Port (u16) pair, so IpAddr instead of Host.

We can also make it convertible to and from std's SocketAddr.

This type is convenient if you want to enforce it to be a socket compatible addr, which Authority can't do for you.


Discussion / hints:
Hello,

I’m the one who reached out about the other issue (#357) and I appreciate you suggesting this one as a better starting point. I’d love to work on this issue as my first contribution to the project. As I mentioned before, I’m new to Rust and open-source contributions, so I might need some guidance along the way, but I’m eager to learn and give it my best effort.

Thank you for offering to mentor me, and I look forward to working on this!
Hi the issue is assigned to you and all yours. Feel free to open a PR as soon as you are stuck or need guidance, if not ready of course.

Is the issue description clear to you? You can essentially start by copying the authority code as mentioned in the description and replace host with ip_addr. Following that path should get you mostly there.

Do let me know if you are stuck anywhere or need to talk some things through or are in need of guidance :) 

Fix patch:
diff --git a/rama-net/src/address/authority.rs b/rama-net/src/address/authority.rs
index 805970a9..c7b68b17 100644
--- a/rama-net/src/address/authority.rs
+++ b/rama-net/src/address/authority.rs
@@ -1,5 +1,5 @@
-use super::{Domain, Host};
-use rama_core::error::{ErrorContext, ErrorExt, OpaqueError};
+use super::{parse_utils, Domain, Host};
+use rama_core::error::{ErrorContext, OpaqueError};
 use std::net::{Ipv4Addr, Ipv6Addr};
 use std::{
     fmt,
@@ -116,12 +116,12 @@ impl From<&SocketAddr> for Authority {
 }
 
 impl fmt::Display for Authority {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match &self.host {
             Host::Name(domain) => write!(f, "{}:{}", domain, self.port),
             Host::Address(ip) => match ip {
-                std::net::IpAddr::V4(ip) => write!(f, "{}:{}", ip, self.port),
-                std::net::IpAddr::V6(ip) => write!(f, "[{}]:{}", ip, self.port),
+                IpAddr::V4(ip) => write!(f, "{}:{}", ip, self.port),
+                IpAddr::V6(ip) => write!(f, "[{}]:{}", ip, self.port),
             },
         }
     }
@@ -147,7 +147,7 @@ impl TryFrom<&str> for Authority {
     type Error = OpaqueError;
 
     fn try_from(s: &str) -> Result<Self, Self::Error> {
-        let (host, port) = split_port_from_str(s)?;
+        let (host, port) = parse_utils::split_port_from_str(s)?;
         let host = Host::try_from(host).context("parse host from authority")?;
         match host {
             Host::Address(IpAddr::V6(_)) if !s.starts_with('[') => Err(OpaqueError::from_display(
@@ -194,17 +194,6 @@ impl TryFrom<&[u8]> for Authority {
     }
 }
 
-fn split_port_from_str(s: &str) -> Result<(&str, u16), OpaqueError> {
-    if let Some(colon) = s.as_bytes().iter().rposition(|c| *c == b':') {
-        match s[colon + 1..].parse() {
-            Ok(port) => Ok((&s[..colon], port)),
-            Err(err) => Err(err.context("parse port as u16")),
-        }
-    } else {
-        Err(OpaqueError::from_display("missing port"))
-    }
-}
-
 impl serde::Serialize for Authority {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
diff --git a/rama-net/src/address/host.rs b/rama-net/src/address/host.rs
index e68c1819..ee85ca1c 100644
--- a/rama-net/src/address/host.rs
+++ b/rama-net/src/address/host.rs
@@ -1,4 +1,4 @@
-use super::Domain;
+use super::{parse_utils, Domain};
 use rama_core::error::{ErrorContext, OpaqueError};
 use std::{
     fmt,
@@ -153,7 +153,7 @@ impl TryFrom<String> for Host {
     type Error = OpaqueError;
 
     fn try_from(name: String) -> Result<Self, Self::Error> {
-        try_to_parse_str_to_ip(name.as_str())
+        parse_utils::try_to_parse_str_to_ip(name.as_str())
             .map(Host::Address)
             .or_else(|| Domain::try_from(name).ok().map(Host::Name))
             .context("parse host from string")
@@ -164,7 +164,7 @@ impl TryFrom<&str> for Host {
     type Error = OpaqueError;
 
     fn try_from(name: &str) -> Result<Self, Self::Error> {
-        try_to_parse_str_to_ip(name)
+        parse_utils::try_to_parse_str_to_ip(name)
             .map(Host::Address)
             .or_else(|| Domain::try_from(name.to_owned()).ok().map(Host::Name))
             .context("parse host from string")
@@ -295,21 +295,10 @@ impl<'de> serde::Deserialize<'de> for Host {
     }
 }
 
-fn try_to_parse_str_to_ip(value: &str) -> Option<IpAddr> {
-    if value.starts_with('[') || value.ends_with(']') {
-        let value = value
-            .strip_prefix('[')
-            .and_then(|value| value.strip_suffix(']'))?;
-        Some(IpAddr::V6(value.parse::<Ipv6Addr>().ok()?))
-    } else {
-        value.parse::<IpAddr>().ok()
-    }
-}
-
 fn try_to_parse_bytes_to_ip(value: &[u8]) -> Option<IpAddr> {
     if let Some(ip) = std::str::from_utf8(value)
         .ok()
-        .and_then(try_to_parse_str_to_ip)
+        .and_then(parse_utils::try_to_parse_str_to_ip)
     {
         return Some(ip);
     }
diff --git a/rama-net/src/address/mod.rs b/rama-net/src/address/mod.rs
index af57dba1..9dc22651 100644
--- a/rama-net/src/address/mod.rs
+++ b/rama-net/src/address/mod.rs
@@ -19,6 +19,13 @@ mod authority;
 #[doc(inline)]
 pub use authority::Authority;
 
+mod socket_address;
+#[doc(inline)]
+pub use socket_address::SocketAddress;
+
 mod proxy;
+
+mod parse_utils;
+
 #[doc(inline)]
 pub use proxy::ProxyAddress;
diff --git a/rama-net/src/address/parse_utils.rs b/rama-net/src/address/parse_utils.rs
new file mode 100644
index 00000000..b37d6de7
--- /dev/null
+++ b/rama-net/src/address/parse_utils.rs
@@ -0,0 +1,24 @@
+use rama_core::error::{ErrorExt, OpaqueError};
+use std::net::{IpAddr, Ipv6Addr};
+
+pub(super) fn split_port_from_str(s: &str) -> Result<(&str, u16), OpaqueError> {
+    if let Some(colon) = s.as_bytes().iter().rposition(|c| *c == b':') {
+        match s[colon + 1..].parse() {
+            Ok(port) => Ok((&s[..colon], port)),
+            Err(err) => Err(err.context("parse port as u16")),
+        }
+    } else {
+        Err(OpaqueError::from_display("missing port"))
+    }
+}
+
+pub(super) fn try_to_parse_str_to_ip(value: &str) -> Option<IpAddr> {
+    if value.starts_with('[') || value.ends_with(']') {
+        let value = value
+            .strip_prefix('[')
+            .and_then(|value| value.strip_suffix(']'))?;
+        Some(IpAddr::V6(value.parse::<Ipv6Addr>().ok()?))
+    } else {
+        value.parse::<IpAddr>().ok()
+    }
+}
diff --git a/rama-net/src/address/socket_address.rs b/rama-net/src/address/socket_address.rs
new file mode 100644
index 00000000..75b1c748
--- /dev/null
+++ b/rama-net/src/address/socket_address.rs
@@ -0,0 +1,298 @@
+use crate::address::parse_utils::try_to_parse_str_to_ip;
+use rama_core::error::{ErrorContext, OpaqueError};
+#[cfg(feature = "http")]
+use rama_http_types::HeaderValue;
+use std::fmt;
+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
+use std::str::FromStr;
+
+/// An [`IpAddr`] with an associated port
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct SocketAddress {
+    ip_addr: IpAddr,
+    port: u16,
+}
+
+impl SocketAddress {
+    /// creates a new [`SocketAddress`]
+    pub const fn new(ip_addr: IpAddr, port: u16) -> Self {
+        SocketAddress { ip_addr, port }
+    }
+    /// Gets the [`IpAddr`] reference.
+    pub fn ip_addr(&self) -> &IpAddr {
+        &self.ip_addr
+    }
+
+    /// Consumes the [`SocketAddress`] and returns the [`IpAddr`].
+    pub fn into_ip_addr(self) -> IpAddr {
+        self.ip_addr
+    }
+
+    /// Gets the port
+    pub fn port(&self) -> u16 {
+        self.port
+    }
+
+    /// Consume self into its parts: `(ip_addr, port)`
+    pub fn into_parts(self) -> (IpAddr, u16) {
+        (self.ip_addr, self.port)
+    }
+}
+
+impl From<SocketAddr> for SocketAddress {
+    fn from(addr: SocketAddr) -> Self {
+        SocketAddress {
+            ip_addr: addr.ip(),
+            port: addr.port(),
+        }
+    }
+}
+
+impl From<&SocketAddr> for SocketAddress {
+    fn from(addr: &SocketAddr) -> Self {
+        SocketAddress {
+            ip_addr: addr.ip(),
+            port: addr.port(),
+        }
+    }
+}
+
+impl From<SocketAddress> for SocketAddr {
+    fn from(addr: SocketAddress) -> Self {
+        SocketAddr::new(addr.ip_addr, addr.port)
+    }
+}
+
+impl From<(IpAddr, u16)> for SocketAddress {
+    #[inline]
+    fn from((ip, port): (IpAddr, u16)) -> Self {
+        (ip, port).into()
+    }
+}
+
+impl From<(Ipv4Addr, u16)> for SocketAddress {
+    #[inline]
+    fn from((ip, port): (Ipv4Addr, u16)) -> Self {
+        (ip, port).into()
+    }
+}
+
+impl From<([u8; 4], u16)> for SocketAddress {
+    #[inline]
+    fn from((ip, port): ([u8; 4], u16)) -> Self {
+        let ip: IpAddr = ip.into();
+        (ip, port).into()
+    }
+}
+
+impl From<(Ipv6Addr, u16)> for SocketAddress {
+    #[inline]
+    fn from((ip, port): (Ipv6Addr, u16)) -> Self {
+        (ip, port).into()
+    }
+}
+
+impl From<([u8; 16], u16)> for SocketAddress {
+    #[inline]
+    fn from((ip, port): ([u8; 16], u16)) -> Self {
+        let ip: IpAddr = ip.into();
+        (ip, port).into()
+    }
+}
+
+impl fmt::Display for SocketAddress {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match &self.ip_addr {
+            IpAddr::V4(ip) => write!(f, "{}:{}", ip, self.port),
+            IpAddr::V6(ip) => write!(f, "[{}]:{}", ip, self.port),
+        }
+    }
+}
+
+impl FromStr for SocketAddress {
+    type Err = OpaqueError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        SocketAddress::try_from(s)
+    }
+}
+
+impl TryFrom<String> for SocketAddress {
+    type Error = OpaqueError;
+
+    fn try_from(s: String) -> Result<Self, Self::Error> {
+        s.as_str().try_into()
+    }
+}
+
+impl TryFrom<&str> for SocketAddress {
+    type Error = OpaqueError;
+
+    fn try_from(s: &str) -> Result<Self, Self::Error> {
+        let (ip_addr, port) = crate::address::parse_utils::split_port_from_str(s)?;
+        let ip_addr =
+            try_to_parse_str_to_ip(ip_addr).context("parse ip address from socket address")?;
+        match ip_addr {
+            IpAddr::V6(_) if !s.starts_with('[') => Err(OpaqueError::from_display(
+                "missing brackets for IPv6 address with port",
+            )),
+            _ => Ok(SocketAddress { ip_addr, port }),
+        }
+    }
+}
+
+#[cfg(feature = "http")]
+impl TryFrom<HeaderValue> for SocketAddress {
+    type Error = OpaqueError;
+
+    fn try_from(header: HeaderValue) -> Result<Self, Self::Error> {
+        Self::try_from(&header)
+    }
+}
+
+#[cfg(feature = "http")]
+impl TryFrom<&HeaderValue> for SocketAddress {
+    type Error = OpaqueError;
+
+    fn try_from(header: &HeaderValue) -> Result<Self, Self::Error> {
+        header.to_str().context("convert header to str")?.try_into()
+    }
+}
+
+impl TryFrom<Vec<u8>> for SocketAddress {
+    type Error = OpaqueError;
+
+    fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
+        Self::try_from(bytes.as_slice())
+    }
+}
+
+impl TryFrom<&[u8]> for SocketAddress {
+    type Error = OpaqueError;
+
+    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
+        let s = std::str::from_utf8(bytes).context("parse sock address from bytes")?;
+        s.try_into()
+    }
+}
+
+impl serde::Serialize for SocketAddress {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        let address = self.to_string();
+        address.serialize(serializer)
+    }
+}
+
+impl<'de> serde::Deserialize<'de> for SocketAddress {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        s.try_into().map_err(serde::de::Error::custom)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn assert_eq(s: &str, sock_address: SocketAddress, ip_addr: &str, port: u16) {
+        assert_eq!(
+            sock_address.ip_addr().to_string(),
+            ip_addr,
+            "parsing: {}",
+            s
+        );
+        assert_eq!(sock_address.port(), port, "parsing: {}", s);
+    }
+
+    #[test]
+    fn test_parse_valid() {
+        for (s, (expected_ip_addr, expected_port)) in [
+            ("[::1]:80", ("::1", 80)),
+            ("127.0.0.1:80", ("127.0.0.1", 80)),
+            (
+                "[2001:db8:3333:4444:5555:6666:7777:8888]:80",
+                ("2001:db8:3333:4444:5555:6666:7777:8888", 80),
+            ),
+        ] {
+            let msg = format!("parsing '{}'", s);
+
+            assert_eq(s, s.parse().expect(&msg), expected_ip_addr, expected_port);
+            assert_eq(
+                s,
+                s.try_into().expect(&msg),
+                expected_ip_addr,
+                expected_port,
+            );
+            assert_eq(
+                s,
+                s.to_owned().try_into().expect(&msg),
+                expected_ip_addr,
+                expected_port,
+            );
+            assert_eq(
+                s,
+                s.as_bytes().try_into().expect(&msg),
+                expected_ip_addr,
+                expected_port,
+            );
+            assert_eq(
+                s,
+                s.as_bytes().to_vec().try_into().expect(&msg),
+                expected_ip_addr,
+                expected_port,
+            );
+        }
+    }
+
+    #[test]
+    fn test_parse_invalid() {
+        for s in [
+            "",
+            "-",
+            ".",
+            ":",
+            ":80",
+            "-.",
+            ".-",
+            "::1",
+            "127.0.0.1",
+            "[::1]",
+            "2001:db8:3333:4444:5555:6666:7777:8888",
+            "[2001:db8:3333:4444:5555:6666:7777:8888]",
+            "example.com",
+            "example.com:",
+            "example.com:-1",
+            "example.com:999999",
+            "example.com:80",
+            "example:com",
+            "[127.0.0.1]:80",
+            "2001:db8:3333:4444:5555:6666:7777:8888:80",
+        ] {
+            let msg = format!("parsing '{}'", s);
+            assert!(s.parse::<SocketAddress>().is_err(), "{}", msg);
+            assert!(SocketAddress::try_from(s).is_err(), "{}", msg);
+            assert!(SocketAddress::try_from(s.to_owned()).is_err(), "{}", msg);
+            assert!(SocketAddress::try_from(s.as_bytes()).is_err(), "{}", msg);
+            assert!(
+                SocketAddress::try_from(s.as_bytes().to_vec()).is_err(),
+                "{}",
+                msg
+            );
+        }
+    }
+
+    #[test]
+    fn test_parse_display() {
+        for (s, expected) in [("[::1]:80", "[::1]:80"), ("127.0.0.1:80", "127.0.0.1:80")] {
+            let msg = format!("parsing '{}'", s);
+            let socket_address: SocketAddress = s.parse().expect(&msg);
+            assert_eq!(socket_address.to_string(), expected, "{}", msg);
+        }
+    }
+}


Test patch: