use std::borrow::Cow;
use std::ops::Deref;
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum InvalidClassName {
#[error("Class name is empty or whitespace-only: '{name}'")]
Empty {
name: Cow<'static, str>,
},
#[error("Class names must not be whitespace-separated. Got: '{name}'.")]
ContainsWhitespace {
name: Cow<'static, str>,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClassName(Cow<'static, str>);
impl ClassName {
pub fn try_new(name: impl Into<Cow<'static, str>>) -> Result<Self, InvalidClassName> {
let name = name.into();
if name.trim().is_empty() {
return Err(InvalidClassName::Empty { name });
}
if name.chars().any(char::is_whitespace) {
return Err(InvalidClassName::ContainsWhitespace { name });
}
Ok(Self(name))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Deref for ClassName {
type Target = str;
fn deref(&self) -> &str {
self.as_str()
}
}
impl AsRef<str> for ClassName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl From<&'static str> for ClassName {
fn from(s: &'static str) -> Self {
Self::try_new(s).unwrap_or_else(|err| panic!("{err}"))
}
}
impl From<String> for ClassName {
fn from(s: String) -> Self {
Self::try_new(s).unwrap_or_else(|err| panic!("{err}"))
}
}
impl From<Cow<'static, str>> for ClassName {
fn from(s: Cow<'static, str>) -> Self {
Self::try_new(s).unwrap_or_else(|err| panic!("{err}"))
}
}
#[cfg(test)]
mod try_new {
use assertr::prelude::*;
use super::{ClassName, InvalidClassName};
#[test]
fn accepts_plain_ascii_token() {
let name = ClassName::try_new("btn-primary").unwrap();
assert_that!(name.as_str()).is_equal_to("btn-primary");
}
#[test]
fn accepts_non_whitespace_unicode_token() {
let name = ClassName::try_new("h\u{00E9}ros").unwrap();
assert_that!(name.as_str()).is_equal_to("h\u{00E9}ros");
}
#[test]
fn rejects_empty_input() {
assert_that!(ClassName::try_new("")).is_err();
}
#[test]
fn rejects_ascii_whitespace_only_input() {
let result = ClassName::try_new(" \t\n");
assert_that!(result)
.is_err()
.is_equal_to(InvalidClassName::Empty {
name: " \t\n".into(),
});
}
#[test]
fn rejects_unicode_whitespace_only_input() {
let result = ClassName::try_new("\u{00A0}\u{00A0}");
assert_that!(result)
.is_err()
.is_equal_to(InvalidClassName::Empty {
name: "\u{00A0}\u{00A0}".into(),
});
}
#[test]
fn rejects_token_with_ascii_whitespace_in_middle() {
let result = ClassName::try_new("foo bar");
assert_that!(result)
.is_err()
.is_equal_to(InvalidClassName::ContainsWhitespace {
name: "foo bar".into(),
});
}
#[test]
fn rejects_token_with_non_breaking_space_in_middle() {
let result = ClassName::try_new("foo\u{00A0}bar");
assert_that!(result)
.is_err()
.is_equal_to(InvalidClassName::ContainsWhitespace {
name: "foo\u{00A0}bar".into(),
});
}
#[test]
fn rejects_token_with_line_separator_in_middle() {
let result = ClassName::try_new("foo\u{2028}bar");
assert_that!(result)
.is_err()
.is_equal_to(InvalidClassName::ContainsWhitespace {
name: "foo\u{2028}bar".into(),
});
}
}