use alloc::{
borrow::{
Cow,
ToOwned,
},
boxed::Box,
string::String,
};
use core::{
borrow::Borrow,
fmt,
fmt::{
Debug,
Display,
},
hash::Hash,
};
use once_cell::race::OnceBox;
use regex::Regex;
use serde::{
Deserialize,
Serialize,
de::Visitor,
};
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Id(Cow<'static, str>);
impl Id {
pub fn from_known(value: &'static str) -> Self {
Self(Cow::Borrowed(value))
}
#[allow(dead_code)]
fn as_id_ref(&self) -> IdRef<'_> {
IdRef(self.0.as_ref())
}
fn chars<'s>(&'s self) -> impl Iterator<Item = char> + 's {
self.0.chars()
}
}
#[derive(Clone, Debug, Hash)]
#[allow(dead_code)]
struct IdRef<'s>(&'s str);
impl<'s> IdRef<'s> {
fn considered_chars(s: &'s str) -> impl Iterator<Item = char> + 's {
s.chars().filter_map(|c| match c {
'0'..='9' => Some(c),
'a'..='z' => Some(c),
'A'..='Z' => Some(c.to_ascii_lowercase()),
_ => None,
})
}
fn chars(&'s self) -> impl Iterator<Item = char> + 's {
Self::considered_chars(self.0)
}
}
impl<'s> From<&'s str> for IdRef<'s> {
fn from(value: &'s str) -> Self {
Self(value)
}
}
impl AsRef<str> for IdRef<'_> {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl PartialEq for IdRef<'_> {
fn eq(&self, other: &Self) -> bool {
self.chars().eq(other.chars())
}
}
impl Eq for IdRef<'_> {}
impl PartialEq<Id> for IdRef<'_> {
fn eq(&self, other: &Id) -> bool {
self.chars().eq(other.chars())
}
}
impl Borrow<str> for Id {
fn borrow(&self) -> &str {
&self.0
}
}
impl Display for Id {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl Display for IdRef<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(self.0, f)
}
}
impl AsRef<str> for Id {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl From<String> for Id {
fn from(value: String) -> Self {
normalize_id(&value)
}
}
impl From<&str> for Id {
fn from(value: &str) -> Self {
normalize_id(value)
}
}
impl From<IdRef<'_>> for Id {
fn from(value: IdRef) -> Self {
Id::from(value.0.to_owned())
}
}
impl PartialEq<str> for Id {
fn eq(&self, other: &str) -> bool {
self.as_ref().eq(other)
}
}
impl PartialEq<IdRef<'_>> for Id {
fn eq(&self, other: &IdRef<'_>) -> bool {
self.chars().eq(other.chars())
}
}
impl Serialize for Id {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
struct IdVisitor;
impl<'de> Visitor<'de> for IdVisitor {
type Value = Id;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Self::Value::from(v))
}
}
impl<'de> Deserialize<'de> for Id {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(IdVisitor)
}
}
pub trait Identifiable {
fn id(&self) -> &Id;
}
fn normalize_id(id: &str) -> Id {
static PATTERN: OnceBox<Regex> = OnceBox::new();
match PATTERN
.get_or_init(|| Box::new(Regex::new(r"[^a-z0-9]").unwrap()))
.replace_all(&id.to_ascii_lowercase(), "")
{
Cow::Borrowed(str) => Id(Cow::Owned(str.to_owned())),
Cow::Owned(str) => Id(Cow::Owned(str)),
}
}
#[cfg(test)]
mod id_test {
use crate::common::Id;
fn assert_normalize_id(input: &str, output: &str) {
assert_eq!(Id::from(input), Id::from(output));
}
#[test]
fn removes_non_alphanumeric_characters() {
assert_normalize_id("Bulbasaur", "bulbasaur");
assert_normalize_id("CHARMANDER", "charmander");
assert_normalize_id("Porygon-Z", "porygonz");
assert_normalize_id("Flabébé", "flabb");
assert_normalize_id("Giratina (Origin)", "giratinaorigin");
}
}