classes 1.0.0

Dependency-free macro that simplifies the process of building class strings for DOM elements
Documentation
pub struct Classes {
    them: Vec<Class>,
}

impl Classes {
    pub fn new() -> Self {
        Self { them: Vec::new() }
    }

    pub fn add(&mut self, class: Class) -> &mut Self {
        self.them.push(class);
        self
    }

    pub fn collect(&self) -> String {
        self.them
            .iter()
            .filter_map(|class| class.get())
            .collect::<Vec<String>>()
            .join(" ")
    }
}

impl Default for Classes {
    fn default() -> Self {
        Classes::new()
    }
}

pub struct Class(Option<String>);

impl Class {
    pub fn get(&self) -> Option<String> {
        self.0.clone()
    }

    pub fn new(value: Option<String>) -> Self {
        Self(value.and_then(|it| if it.is_empty() { None } else { Some(it) }))
    }
}

impl From<String> for Class {
    fn from(value: String) -> Self {
        Class::new(Some(value))
    }
}

impl From<&str> for Class {
    fn from(value: &str) -> Self {
        Class::new(Some(value.into()))
    }
}

impl From<Option<String>> for Class {
    fn from(value: Option<String>) -> Self {
        Class::new(value)
    }
}

impl From<Option<&str>> for Class {
    fn from(value: Option<&str>) -> Self {
        Class::new(value.map(|it| it.into()))
    }
}

#[macro_export]
macro_rules! classes {
    ($($token:expr$(=> $bool:expr)?),*) => [
        {
            let mut classes = $crate::core::Classes::new();

            $(
                if true $(&& $bool)? {
                    classes.add($token.into());
                }
            )*

            classes.collect()
        }
    ];
}

#[cfg(test)]
mod tests {
    macro_rules! tests {
        [$([$test_name:ident, $actual:expr, $expected:literal]),+$(,)?] => {
            $(
                #[test]
                fn $test_name() {
                    assert_eq!($actual, $expected);
                }
            )+
        }
    }

    const DISABLED: bool = true;

    tests![
        [should_accept_str_and_strings, classes!["button".to_string(), "button--disabled"], "button button--disabled"],
        [should_accept_optionals, classes![Some("button--active"), None::<String>, Some("button--disabled".to_string())], "button--active button--disabled"],
        [should_accept_expressions, classes!["concatenated".to_string() + "-class", Some("batman").map(|_| "bruce-wayne")], "concatenated-class bruce-wayne"],
        [should_apply_classes_evaluating_to_true, classes!["button" => true, "button--disabled" => DISABLED, "button--active" => false, "all-the-buttons" => 42 > 3 ], "button button--disabled all-the-buttons"],
        [should_accept_various_types_at_the_same_time, classes!["button" => true, Some("button--disabled"), None::<String>, "button--primary"], "button button--disabled button--primary"],
        [should_remove_empty_str, classes!["button", "", "button--active"], "button button--active"],
        [should_remove_empty_string, classes!["button", "".to_string(), "button--active"], "button button--active"],
        [should_remove_empty_strings_passed_as_options, classes!["button", Some(""), "button--active"], "button button--active"],
    ];
}