Skip to main content

battler_data/common/
id.rs

1use 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/// An ID for a resource.
28///
29/// Resources of the same type should have a unique ID.
30///
31/// A further optimization would be to allocate strings in an arena for memory proximity.
32#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub struct Id(Cow<'static, str>);
34
35impl Id {
36    /// Creates an ID from a string that is known to already be a valid ID.
37    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/// A reference to an ID for a resource.
52///
53/// This type is primarily for optimization purposes. Some code needs IDs but doesn't necessarily
54/// need to own them. Thus, this type provides ID comparisons for unowned strings.
55#[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
189/// A trait that provides a common way of identifying resources.
190///
191/// Resources of the same type should have a unique ID.
192pub trait Identifiable {
193    fn id(&self) -> &Id;
194}
195
196/// Normalizes the given ID.
197///
198/// IDs must have lowercase alphanumeric characters. Non-alphanumeric characters are removed.
199fn 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        // There is an optimization to be done here. If this is a &'static str, we can save it
207        // without owning it. However, this code is shared for all &str, so we cannot make the
208        // distinction as is.
209        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}