stringy 0.2.2

A tiny Rust crate for generating byte-sized enums that represent a fixed, ordered set of &str data.
Documentation
///! This library holds macros which can be used to generate enums as labels
///! for fixed instances of lazily allocated literal data.
///!

/// Generates byte-sized enums corresponding to a set of string literals.
///
/// The variants are all fieldless variants, but they each can generate *one*
/// of the aforementioned set of string literals -- in particular, each variant
/// dereferences to an instance of the string literal provided when calling
/// this macro, thus each enum generated by this macro implements
/// `std::ops::Deref<&str>`.
///
/// Additionally, every generated enum implements `Ord`, with the total order
/// defined in terms of the order in which each variant was defined, i.e.,
/// strictly based on the order they appear in the macro call.
///
/// This effectively allows for unification of `&str` values with
/// `usize` values via a single `1` byte long interface, as the inherent method
/// `as_usize` returns the `usize` value corresponding to the position of a
/// given variant within this total order.
///
/// The provided literal values are additionally automatically recorded in doc
/// comments for each enum variant, as well as within relevant methods.
///
/// # Example
/// ```
/// // Let's define some names for colors
/// stringy! { Color =
///     Red     "red"
///     Green   "green"
///     Blue    "blue"
/// }
///
/// // Now we can test any strings to see if they match a color name
/// let not_a_color_name = "boop";
/// let red = "red";
/// assert_eq!(Color::test_str(not_a_color_name), false);
/// assert_eq!(Color::from_str(not_a_color_name), None);
/// assert_eq!(Color::test_str(red), true);
/// assert_eq!(Color::from_str(red), Some(Color::Red));
///
/// // Each variant is associated with a `usize` value indicating its order
/// // relative to the other variants
/// let idx_red = Color::Red.as_usize();
/// let idx_blue = Color::Blue.as_usize();
/// assert_eq!(idx_red, 0);
/// assert_eq!(idx_blue, 2);
///
/// // We can also generate a fixed-size array of all of the possibiities,
/// // ordered as defined.
/// let rgb = Color::VARIANTS;
/// assert_eq!(rgb, [Color::Red, Color::Green, Color::Blue]);
/// ```
///
#[macro_export]
macro_rules! stringy {
    (
        $($(#[$top:meta])+)?
        $name:ident
        =
        $(
            $($(#[$com:meta])+)?
            $label:ident $lit:literal $(| $alt:literal)*
        )+
    ) => {
        $(
            $(#[$top])+
            ///
            /// ---
            ///
        )?
        /// *This enum and documentation snipper was generated by the*
        /// `stringy` *macro.*
        ///
        /// The variants of this enum are each associated with a fixed `&str`
        /// value provided at the macro invocation site. Those `&str` values are
        /// serialized in the following order:
        ///
        $(#[doc = "1. `"]
        #[doc = $lit]
        #[doc = "`"])+
        ///
        #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
        pub enum $name {
            $(
                $(
                    $(#[$com])+
                    ///
                    /// ---
                    ///
                )?
                #[doc = "Corresponds to the symbol `"]
                #[doc = $lit]
                #[doc = "`."]
                #[allow(dead_code)]
                $label,
            )+
        }

        #[allow(unused)]
        impl $name {
            /// An array containing an instance of every variant in the same
            /// order declared. This array effectively loosely portrays the
            /// compiler-generated implementations of `PartialOrd` etc..
            pub const VARIANTS: [$name; $crate::stringy!(#$($label)+)] = [$($name::$label,)+];

            /// Given a string slice, check the literals corresponding to each
            /// of this enum's variants, returning the variant (wrapped in a
            /// `Some` variant) if a strict match is found, otherwise returning
            /// `None`
            ///
            /// This method will return a value wrapped in `Some` if the
            /// provided string literal is any of:
            ///
            $(
                #[doc = "1. `"]
                #[doc = $lit]
                #[doc = "`"]
                #[doc = " "]
            )+
            #[inline]
            pub fn from_str(s: &str) -> Option<Self> {
                match s {
                    $($lit $(| $alt)* => { Some($name::$label) })+
                    _ => None
                }
            }

            /// Returns the literal string provided with a given variant from
            /// where it was defined. Alternates are *not* reachable this way.
            #[inline]
            pub fn as_str(&self) -> &str {
                match self {
                    $($name::$label => { $lit })+
                }
            }

            /// With all variants enumerated based on definition order, this
            /// returns the `usize` value corresponding to where this
            /// instance's variant lies along the sequence of variants.
            // TODO: this needs a better name
            #[inline]
            pub fn as_usize(&self) -> usize {
                let mut i = 0;
                $(
                    if self == &Self::$label { return i; } else { i += 1; };
                )+
                i
            }

            #[inline]
            pub fn as_bytes(&self) -> &[u8] {
                (match self {
                    $($name::$label => { $lit })+
                }).as_bytes()
            }

            /// Identifies whether a given instance has the same variant as any
            /// from a given slice of variant instances.
            #[inline]
            pub fn is_any_of(&self, others: &[$name]) -> bool {
                others.contains(self)
            }

            /// Given a string slice, identifies whether it is equal to the
            /// string literal corresponding to any of this enum's variants.
            ///
            /// In other words, this will return whether a given string matches
            /// any of the following:
            $(
                #[doc = "`"]
                #[doc = $lit]
                #[doc = "`"]
            )+
            #[inline]
            pub fn test_str(text: &str) -> bool {
                match text {
                    $($lit => { true })+
                    _ => false
                }
            }

            /// Given a slice of bytes, identifies a match against the bytes of
            /// any of this enum's variants' string literal data
            #[inline]
            pub fn same_bytes(&self, bytes: &[u8]) -> bool {
                self.as_bytes() == bytes
            }
        }


        /// The `Display` trait just writes the same string literal each
        /// variant was defined with,
        impl std::fmt::Display for $name {
             fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                let s = match self {
                    $($name::$label => { $lit })+
                };
                write!(f, "{}", s)
            }
        }

        /// Simple implementation allowing for direct conversion to the standard
        /// library's `borrow::Cow<'t, str>` type for some lifetime `'t`.
        impl<'t> From<$name> for std::borrow::Cow<'t, str> {
            fn from(label: $name) -> std::borrow::Cow<'t, str> {
                match label {
                    $($name::$label => {
                        std::borrow::Cow::from($lit)
                    })+
                }
            }
        }

        /// Every `stringy` generated enum variant dereferences to a `&str`
        impl std::ops::Deref for $name {
            type Target = str;
            fn deref(&self) -> &Self::Target {
                match self {
                    $($name::$label => { $lit })+
                }
            }
        }

        /// Cheaply convert a `stringy`-generated enum to a string slice.
        impl AsRef<str> for $name {
            fn as_ref(&self) -> &str {
                match self {
                    $($name::$label => { $lit })+
                }
            }
        }

        /// Tries to convert a string slice into a variant of this enum.
        /// On failure, the provided string slice is returned.
        impl<'t> std::convert::TryFrom<&'t str> for $name {
            type Error = &'t str;

            fn try_from(value: &'t str) -> Result<Self, Self::Error> {
                match value {
                    $($lit $(| $alt)* => { Ok($name::$label) })+
                    _ => { Err(value) }
                }
            }
        }

        /// Tries to convert a string into a variant of this enum.
        /// On failure, the provided string is returned in case it is to be
        /// reused.
        impl std::convert::TryFrom<String> for $name {
            type Error = String;

            fn try_from(value: String) -> Result<Self, Self::Error> {
                match value.as_str() {
                    $($lit $(| $alt)* => { Ok($name::$label) })+
                    _ => { Err(value) }
                }
            }
        }
    };

    // internal rule/hack to satisfy integer portion for constant expressions by
    // exploiting the fact the compiler can accept a binary expression such as
    // `1 + 1 + 1` in place of `3`.
    //
    // __NOTE:__ The above is likely to cap out based on whether a recursion
    // limit has been set, implying that for enums with large enough variants,
    // this macro *may* cause the compiler to complain..
    (#$t:tt) => { 1 };
    (#$a:tt $($bs:tt)+) => {{
        1 $(+ $crate::stringy!(# $bs))+
    }
    };
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::mem;

    #[test]
    fn it_works() {
        stringy! {
            Color =
                Red "red" | "rojo"
                Blue "blue" | "azul"
                Green "green" | "verde"
        }

        assert_eq!(mem::size_of::<Color>(), 1);
        assert_eq!(mem::size_of_val(&Color::Red), 1);
        assert_eq!(mem::size_of_val(&Color::VARIANTS), 3);
        assert_eq!(mem::size_of_val(&Color::Red.as_str()), 16);
        assert_eq!(Color::from_str("red"), Some(Color::Red));
        assert_eq!(Color::from_str("rojo"), Color::from_str("red"));
    }

    #[test]
    fn test_array() {
        stringy! {
            Color = Red "red" Green "green" Blue "blue"
        }
        let [r, g, b] = Color::VARIANTS;
        assert_eq!(r, Color::Red);
        assert_eq!(g, Color::Green);
        assert_eq!(b, Color::Blue);
    }

    #[test]
    fn test_from_str() {
        stringy! { Operator = Add "+" Sub "-" Mul "*" Div "/" }
        assert_eq!(Operator::from_str("+"), Some(Operator::Add));
        assert_eq!(Operator::from_str("-"), Some(Operator::Sub));
        assert_eq!(Operator::from_str("*"), Some(Operator::Mul));
        assert_eq!(Operator::from_str("/"), Some(Operator::Div));
    }

    #[test]
    fn test_is_even() {
        stringy! {
            /// This is a doc comment about `Digit`
            Digit =
            /// Doc comment for `Digit::One`
            One "1" | "one"
            Two "2"
            Three "3"
            Four "4"
            /// Five
            Five "5"
            Six "6"
            Seven "7"
            Eight "8"
            Nine "9"
        }

        assert_eq!(Digit::from_str("one"), Some(Digit::One));

        let evens = Digit::VARIANTS
            .iter()
            .map(|d| d.parse::<usize>().unwrap())
            .filter(|n| n % 2 == 0)
            .collect::<Vec<usize>>();

        assert_eq!(evens, vec![2, 4, 6, 8])
    }
}