Unify declaration of {Node,Domain,Resource}Part

Jonas Schäfer created

This introduces a str-like type for each of these, which will allow
returning a ref instead of the copied data from various methods in
{Full,Bare}Jid.

The use of a macro ensures that all types are declared consistently.

Change summary

jid/src/lib.rs   |  11 +
jid/src/parts.rs | 288 +++++++++++++++++++++++++++++++++++++++----------
2 files changed, 235 insertions(+), 64 deletions(-)

Detailed changes

jid/src/lib.rs 🔗

@@ -49,7 +49,7 @@ mod inner;
 use inner::InnerJid;
 
 mod parts;
-pub use parts::{DomainPart, NodePart, ResourcePart};
+pub use parts::{DomainPart, DomainRef, NodePart, NodeRef, ResourcePart, ResourceRef};
 
 /// An enum representing a Jabber ID. It can be either a `FullJid` or a `BareJid`.
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -798,7 +798,7 @@ mod tests {
 
         assert_eq!(jid.node_str(), Some("a"),);
 
-        assert_eq!(jid.node(), Some(NodePart::new("a").unwrap()));
+        assert_eq!(jid.node(), Some(NodePart::new("a").unwrap().into_owned()));
     }
 
     #[test]
@@ -807,7 +807,7 @@ mod tests {
 
         assert_eq!(jid.domain_str(), "b.c");
 
-        assert_eq!(jid.domain(), DomainPart::new("b.c").unwrap());
+        assert_eq!(jid.domain(), DomainPart::new("b.c").unwrap().into_owned());
     }
 
     #[test]
@@ -816,7 +816,10 @@ mod tests {
 
         assert_eq!(jid.resource_str(), Some("d"),);
 
-        assert_eq!(jid.resource(), Some(ResourcePart::new("d").unwrap()));
+        assert_eq!(
+            jid.resource(),
+            Some(ResourcePart::new("d").unwrap().into_owned())
+        );
     }
 
     #[test]

jid/src/parts.rs 🔗

@@ -1,6 +1,9 @@
-use stringprep::{nameprep, nodeprep, resourceprep};
-
+use std::borrow::{Borrow, Cow};
 use std::fmt;
+use std::ops::Deref;
+use std::str::FromStr;
+
+use stringprep::{nameprep, nodeprep, resourceprep};
 
 use crate::Error;
 
@@ -14,77 +17,242 @@ fn length_check(len: usize, error_empty: Error, error_too_long: Error) -> Result
     }
 }
 
-/// The [`NodePart`] is the optional part before the (optional) `@` in any [`Jid`][crate::Jid],
-/// whether [`BareJid`][crate::BareJid] or [`FullJid`][crate::FullJid].
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub struct NodePart(pub(crate) String);
-
-impl NodePart {
-    /// Build a new [`NodePart`] from a string slice. Will fail in case of stringprep validation
-    /// error.
-    pub fn new(s: &str) -> Result<NodePart, Error> {
-        let node = nodeprep(s).map_err(|_| Error::NodePrep)?;
-        length_check(node.len(), Error::NodeEmpty, Error::NodeTooLong)?;
-        Ok(NodePart(node.to_string()))
-    }
+macro_rules! def_part_parse_doc {
+    ($name:ident, $other:ident, $more:expr) => {
+        concat!(
+            "Parse a [`",
+            stringify!($name),
+            "`] from a `",
+            stringify!($other),
+            "`, copying its contents.\n",
+            "\n",
+            "If the given `",
+            stringify!($other),
+            "` does not conform to the restrictions imposed by `",
+            stringify!($name),
+            "`, an error is returned.\n",
+            $more,
+        )
+    };
+}
 
-    pub(crate) fn new_unchecked(s: &str) -> NodePart {
-        NodePart(s.to_string())
-    }
+macro_rules! def_part_into_inner_doc {
+    ($name:ident, $other:ident, $more:expr) => {
+        concat!(
+            "Consume the `",
+            stringify!($name),
+            "` and return the inner `",
+            stringify!($other),
+            "`.\n",
+            $more,
+        )
+    };
 }
 
-impl fmt::Display for NodePart {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", self.0)
+macro_rules! def_part_types {
+    (
+        $(#[$mainmeta:meta])*
+        pub struct $name:ident(String) use $prepfn:ident(err = $preperr:path, empty = $emptyerr:path, long = $longerr:path);
+
+        $(#[$refmeta:meta])*
+        pub struct ref $borrowed:ident(str);
+    ) => {
+        $(#[$mainmeta])*
+        #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
+        #[repr(transparent)]
+        pub struct $name(pub(crate) String);
+
+        impl $name {
+            #[doc = def_part_parse_doc!($name, str, "Depending on whether the contents are changed by normalisation operations, this function either returns a copy or a reference to the original data.")]
+            pub fn new(s: &str) -> Result<Cow<'_, $borrowed>, Error> {
+                let node = $prepfn(s).map_err(|_| $preperr)?;
+                length_check(node.len(), $emptyerr, $longerr)?;
+                match node {
+                    Cow::Borrowed(v) => Ok(Cow::Borrowed($borrowed::from_str_unchecked(v))),
+                    Cow::Owned(v) => Ok(Cow::Owned(Self(v))),
+                }
+            }
+
+            #[doc = def_part_into_inner_doc!($name, String, "")]
+            pub fn into_inner(self) -> String {
+                self.0
+            }
+
+            pub(crate) fn new_unchecked(s: &str) -> Self {
+                $name(s.to_string())
+            }
+        }
+
+        impl FromStr for $name {
+            type Err = Error;
+
+            fn from_str(s: &str) -> Result<Self, Error> {
+                Ok(Self::new(s)?.into_owned())
+            }
+        }
+
+        impl fmt::Display for $name {
+            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+                <$borrowed as fmt::Display>::fmt(Borrow::<$borrowed>::borrow(self), f)
+            }
+        }
+
+        impl Deref for $name {
+            type Target = $borrowed;
+
+            fn deref(&self) -> &Self::Target {
+                Borrow::<$borrowed>::borrow(self)
+            }
+        }
+
+        impl AsRef<$borrowed> for $name {
+            fn as_ref(&self) -> &$borrowed {
+                Borrow::<$borrowed>::borrow(self)
+            }
+        }
+
+        impl AsRef<String> for $name {
+            fn as_ref(&self) -> &String {
+                &self.0
+            }
+        }
+
+        impl Borrow<$borrowed> for $name {
+            fn borrow(&self) -> &$borrowed {
+                $borrowed::from_str_unchecked(self.0.as_str())
+            }
+        }
+
+        // useful for use in hashmaps
+        impl Borrow<String> for $name {
+            fn borrow(&self) -> &String {
+                &self.0
+            }
+        }
+
+        // useful for use in hashmaps
+        impl Borrow<str> for $name {
+            fn borrow(&self) -> &str {
+                self.0.as_str()
+            }
+        }
+
+        impl<'x> TryFrom<&'x str> for $name {
+            type Error = Error;
+
+            fn try_from(s: &str) -> Result<Self, Error> {
+                Self::from_str(s)
+            }
+        }
+
+        impl From<&$borrowed> for $name {
+            fn from(other: &$borrowed) -> Self {
+                other.to_owned()
+            }
+        }
+
+        impl<'x> From<Cow<'x, $borrowed>> for $name {
+            fn from(other: Cow<'x, $borrowed>) -> Self {
+                other.into_owned()
+            }
+        }
+
+        $(#[$refmeta])*
+        #[repr(transparent)]
+        #[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
+        pub struct $borrowed(pub(crate) str);
+
+        impl $borrowed {
+            fn from_str_unchecked(s: &str) -> &Self {
+                // SAFETY: repr(transparent) thing can be transmuted to/from
+                // its inner.
+                unsafe { std::mem::transmute(s) }
+            }
+
+            /// Access the contents as [`str`] slice.
+            pub fn as_str(&self) -> &str {
+                &self.0
+            }
+        }
+
+        impl Deref for $borrowed {
+            type Target = str;
+
+            fn deref(&self) -> &Self::Target {
+                &self.0
+            }
+        }
+
+        impl ToOwned for $borrowed {
+            type Owned = $name;
+
+            fn to_owned(&self) -> Self::Owned {
+                $name(self.0.to_string())
+            }
+        }
+
+        impl AsRef<str> for $borrowed {
+            fn as_ref(&self) -> &str {
+                &self.0
+            }
+        }
+
+        impl fmt::Display for $borrowed {
+            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+                write!(f, "{}", &self.0)
+            }
+        }
     }
 }
 
-/// The [`DomainPart`] is the part between the (optional) `@` and the (optional) `/` in any
-/// [`Jid`][crate::Jid], whether [`BareJid`][crate::BareJid] or [`FullJid`][crate::FullJid].
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub struct DomainPart(pub(crate) String);
-
-impl DomainPart {
-    /// Build a new [`DomainPart`] from a string slice. Will fail in case of stringprep validation
-    /// error.
-    pub fn new(s: &str) -> Result<DomainPart, Error> {
-        let domain = nameprep(s).map_err(|_| Error::NamePrep)?;
-        length_check(domain.len(), Error::DomainEmpty, Error::DomainTooLong)?;
-        Ok(DomainPart(domain.to_string()))
-    }
+def_part_types! {
+    /// The [`NodePart`] is the optional part before the (optional) `@` in any
+    /// [`Jid`][crate::Jid], whether [`BareJid`][crate::BareJid] or
+    /// [`FullJid`][crate::FullJid].
+    ///
+    /// The corresponding slice type is [`NodeRef`].
+    pub struct NodePart(String) use nodeprep(err = Error::NodePrep, empty = Error::NodeEmpty, long = Error::NodeTooLong);
 
-    pub(crate) fn new_unchecked(s: &str) -> DomainPart {
-        DomainPart(s.to_string())
-    }
+    /// `str`-like type which conforms to the requirements of [`NodePart`].
+    ///
+    /// See [`NodePart`] for details.
+    pub struct ref NodeRef(str);
 }
 
-impl fmt::Display for DomainPart {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", self.0)
-    }
+def_part_types! {
+    /// The [`DomainPart`] is the part between the (optional) `@` and the
+    /// (optional) `/` in any [`Jid`][crate::Jid], whether
+    /// [`BareJid`][crate::BareJid] or [`FullJid`][crate::FullJid].
+    pub struct DomainPart(String) use nameprep(err = Error::NamePrep, empty = Error::DomainEmpty, long = Error::DomainTooLong);
+
+    /// `str`-like type which conforms to the requirements of [`DomainPart`].
+    ///
+    /// See [`DomainPart`] for details.
+    pub struct ref DomainRef(str);
 }
 
-/// The [`ResourcePart`] is the optional part after the `/` in a [`Jid`][crate::Jid]. It is
-/// mandatory in [`FullJid`][crate::FullJid].
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub struct ResourcePart(pub(crate) String);
-
-impl ResourcePart {
-    /// Build a new [`ResourcePart`] from a string slice. Will fail in case of stringprep
-    /// validation error.
-    pub fn new(s: &str) -> Result<ResourcePart, Error> {
-        let resource = resourceprep(s).map_err(|_| Error::ResourcePrep)?;
-        length_check(resource.len(), Error::ResourceEmpty, Error::ResourceTooLong)?;
-        Ok(ResourcePart(resource.to_string()))
-    }
+def_part_types! {
+    /// The [`ResourcePart`] is the optional part after the `/` in a
+    /// [`Jid`][crate::Jid]. It is mandatory in [`FullJid`][crate::FullJid].
+    pub struct ResourcePart(String) use resourceprep(err = Error::ResourcePrep, empty = Error::ResourceEmpty, long = Error::ResourceTooLong);
 
-    pub(crate) fn new_unchecked(s: &str) -> ResourcePart {
-        ResourcePart(s.to_string())
-    }
+    /// `str`-like type which conforms to the requirements of
+    /// [`ResourcePart`].
+    ///
+    /// See [`ResourcePart`] for details.
+    pub struct ref ResourceRef(str);
 }
 
-impl fmt::Display for ResourcePart {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", self.0)
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn nodepart_comparison() {
+        let n1 = NodePart::new("foo").unwrap();
+        let n2 = NodePart::new("bar").unwrap();
+        let n3 = NodePart::new("foo").unwrap();
+        assert_eq!(n1, n3);
+        assert_ne!(n1, n2);
     }
 }