battler_data/common/
id.rs1use alloc::{
2 borrow::{
3 Cow,
4 ToOwned,
5 },
6 boxed::Box,
7 string::String,
8};
9use core::{
10 borrow::Borrow,
11 fmt,
12 fmt::{
13 Debug,
14 Display,
15 },
16 hash::Hash,
17};
18
19use once_cell::race::OnceBox;
20use regex::Regex;
21use serde::{
22 Deserialize,
23 Serialize,
24 de::Visitor,
25};
26
27#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub struct Id(Cow<'static, str>);
34
35impl Id {
36 pub fn from_known(value: &'static str) -> Self {
38 Self(Cow::Borrowed(value))
39 }
40
41 #[allow(dead_code)]
42 fn as_id_ref(&self) -> IdRef<'_> {
43 IdRef(self.0.as_ref())
44 }
45
46 fn chars<'s>(&'s self) -> impl Iterator<Item = char> + 's {
47 self.0.chars()
48 }
49}
50
51#[derive(Clone, Debug, Hash)]
56#[allow(dead_code)]
57struct IdRef<'s>(&'s str);
58
59impl<'s> IdRef<'s> {
60 fn considered_chars(s: &'s str) -> impl Iterator<Item = char> + 's {
61 s.chars().filter_map(|c| match c {
62 '0'..='9' => Some(c),
63 'a'..='z' => Some(c),
64 'A'..='Z' => Some(c.to_ascii_lowercase()),
65 _ => None,
66 })
67 }
68
69 fn chars(&'s self) -> impl Iterator<Item = char> + 's {
70 Self::considered_chars(self.0)
71 }
72}
73
74impl<'s> From<&'s str> for IdRef<'s> {
75 fn from(value: &'s str) -> Self {
76 Self(value)
77 }
78}
79
80impl AsRef<str> for IdRef<'_> {
81 fn as_ref(&self) -> &str {
82 self.0.as_ref()
83 }
84}
85
86impl PartialEq for IdRef<'_> {
87 fn eq(&self, other: &Self) -> bool {
88 self.chars().eq(other.chars())
89 }
90}
91
92impl Eq for IdRef<'_> {}
93
94impl PartialEq<Id> for IdRef<'_> {
95 fn eq(&self, other: &Id) -> bool {
96 self.chars().eq(other.chars())
97 }
98}
99
100impl Borrow<str> for Id {
101 fn borrow(&self) -> &str {
102 &self.0
103 }
104}
105
106impl Display for Id {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 Display::fmt(&self.0, f)
109 }
110}
111
112impl Display for IdRef<'_> {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 Display::fmt(self.0, f)
115 }
116}
117
118impl AsRef<str> for Id {
119 fn as_ref(&self) -> &str {
120 self.0.as_ref()
121 }
122}
123
124impl From<String> for Id {
125 fn from(value: String) -> Self {
126 normalize_id(&value)
127 }
128}
129
130impl From<&str> for Id {
131 fn from(value: &str) -> Self {
132 normalize_id(value)
133 }
134}
135
136impl From<IdRef<'_>> for Id {
137 fn from(value: IdRef) -> Self {
138 Id::from(value.0.to_owned())
139 }
140}
141
142impl PartialEq<str> for Id {
143 fn eq(&self, other: &str) -> bool {
144 self.as_ref().eq(other)
145 }
146}
147
148impl PartialEq<IdRef<'_>> for Id {
149 fn eq(&self, other: &IdRef<'_>) -> bool {
150 self.chars().eq(other.chars())
151 }
152}
153
154impl Serialize for Id {
155 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
156 where
157 S: serde::Serializer,
158 {
159 serializer.serialize_str(self.as_ref())
160 }
161}
162
163struct IdVisitor;
164
165impl<'de> Visitor<'de> for IdVisitor {
166 type Value = Id;
167
168 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
169 write!(formatter, "a string")
170 }
171
172 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
173 where
174 E: serde::de::Error,
175 {
176 Ok(Self::Value::from(v))
177 }
178}
179
180impl<'de> Deserialize<'de> for Id {
181 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
182 where
183 D: serde::Deserializer<'de>,
184 {
185 deserializer.deserialize_str(IdVisitor)
186 }
187}
188
189pub trait Identifiable {
193 fn id(&self) -> &Id;
194}
195
196fn normalize_id(id: &str) -> Id {
200 static PATTERN: OnceBox<Regex> = OnceBox::new();
201
202 match PATTERN
203 .get_or_init(|| Box::new(Regex::new(r"[^a-z0-9]").unwrap()))
204 .replace_all(&id.to_ascii_lowercase(), "")
205 {
206 Cow::Borrowed(str) => Id(Cow::Owned(str.to_owned())),
210 Cow::Owned(str) => Id(Cow::Owned(str)),
211 }
212}
213
214#[cfg(test)]
215mod id_test {
216 use crate::common::Id;
217
218 fn assert_normalize_id(input: &str, output: &str) {
219 assert_eq!(Id::from(input), Id::from(output));
220 }
221
222 #[test]
223 fn removes_non_alphanumeric_characters() {
224 assert_normalize_id("Bulbasaur", "bulbasaur");
225 assert_normalize_id("CHARMANDER", "charmander");
226 assert_normalize_id("Porygon-Z", "porygonz");
227 assert_normalize_id("Flabébé", "flabb");
228 assert_normalize_id("Giratina (Origin)", "giratinaorigin");
229 }
230}