valence_ident/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::borrow::{Borrow, Cow};
4use std::cmp::Ordering;
5use std::fmt;
6use std::fmt::Formatter;
7use std::str::FromStr;
8
9use serde::de::Error as _;
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11use thiserror::Error;
12/// Used internally by the `ident` macro. Not public API.
13#[doc(hidden)]
14pub use valence_ident_macros::parse_ident_str;
15
16/// Creates a new [`Ident`] at compile time from a string literal. A compile
17/// error is raised if the string is not a valid resource identifier.
18///
19/// The type of the expression returned by this macro is `Ident<&'static str>`.
20/// The expression is usable in a `const` context.
21///
22/// # Examples
23///
24/// ```
25/// # use valence_ident::{ident, Ident};
26/// let my_ident: Ident<&'static str> = ident!("apple");
27///
28/// println!("{my_ident}");
29/// ```
30#[macro_export]
31macro_rules! ident {
32    ($string:literal) => {
33        $crate::Ident::<&'static str>::new_unchecked($crate::parse_ident_str!($string))
34    };
35}
36
37/// A wrapper around a string type `S` which guarantees the wrapped string is a
38/// valid resource identifier.
39///
40/// A resource identifier is a string divided into a "namespace" part and a
41/// "path" part. For instance `minecraft:apple` and `valence:frobnicator` are
42/// both valid identifiers. A string must match the regex
43/// `^([a-z0-9_.-]+:)?[a-z0-9_.-\/]+$` to be successfully parsed.
44///
45/// While parsing, if the namespace part is left off (the part before and
46/// including the colon) then "minecraft:" is inserted at the beginning of the
47/// string.
48#[derive(Copy, Clone, Eq, Ord, Hash)]
49pub struct Ident<S> {
50    string: S,
51}
52
53/// The error type created when an [`Ident`] cannot be parsed from a
54/// string. Contains the string that failed to parse.
55#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Error)]
56#[error("invalid resource identifier \"{0}\"")]
57pub struct IdentError(pub String);
58
59impl<'a> Ident<Cow<'a, str>> {
60    pub fn new<S: Into<Cow<'a, str>>>(string: S) -> Result<Self, IdentError> {
61        parse(string.into())
62    }
63}
64
65impl<S> Ident<S> {
66    /// Used internally by the `ident` macro. Not public API.
67    #[doc(hidden)]
68    pub const fn new_unchecked(string: S) -> Self {
69        Self { string }
70    }
71
72    pub fn as_str(&self) -> &str
73    where
74        S: AsRef<str>,
75    {
76        self.string.as_ref()
77    }
78
79    pub fn as_str_ident(&self) -> Ident<&str>
80    where
81        S: AsRef<str>,
82    {
83        Ident {
84            string: self.as_str(),
85        }
86    }
87
88    pub fn to_string_ident(&self) -> Ident<String>
89    where
90        S: AsRef<str>,
91    {
92        Ident {
93            string: self.as_str().to_owned(),
94        }
95    }
96
97    pub fn into_inner(self) -> S {
98        self.string
99    }
100
101    /// Returns the namespace part of this resource identifier (the part before
102    /// the colon).
103    pub fn namespace(&self) -> &str
104    where
105        S: AsRef<str>,
106    {
107        self.namespace_and_path().0
108    }
109
110    /// Returns the path part of this resource identifier (the part after the
111    /// colon).
112    pub fn path(&self) -> &str
113    where
114        S: AsRef<str>,
115    {
116        self.namespace_and_path().1
117    }
118
119    pub fn namespace_and_path(&self) -> (&str, &str)
120    where
121        S: AsRef<str>,
122    {
123        self.as_str()
124            .split_once(':')
125            .expect("invalid resource identifier")
126    }
127}
128
129impl Ident<Cow<'_, str>> {
130    pub fn borrowed(&self) -> Ident<Cow<str>> {
131        Ident::new_unchecked(Cow::Borrowed(self.as_str()))
132    }
133}
134
135fn parse(string: Cow<str>) -> Result<Ident<Cow<str>>, IdentError> {
136    let check_namespace = |s: &str| {
137        !s.is_empty()
138            && s.chars()
139                .all(|c| matches!(c, 'a'..='z' | '0'..='9' | '_' | '.' | '-'))
140    };
141
142    let check_path = |s: &str| {
143        !s.is_empty()
144            && s.chars()
145                .all(|c| matches!(c, 'a'..='z' | '0'..='9' | '_' | '.' | '-' | '/'))
146    };
147
148    match string.split_once(':') {
149        Some((namespace, path)) if check_namespace(namespace) && check_path(path) => {
150            Ok(Ident { string })
151        }
152        None if check_path(&string) => Ok(Ident {
153            string: format!("minecraft:{string}").into(),
154        }),
155        _ => Err(IdentError(string.into())),
156    }
157}
158
159impl<S: AsRef<str>> AsRef<str> for Ident<S> {
160    fn as_ref(&self) -> &str {
161        self.string.as_ref()
162    }
163}
164
165impl<S> AsRef<S> for Ident<S> {
166    fn as_ref(&self) -> &S {
167        &self.string
168    }
169}
170
171impl<S: Borrow<str>> Borrow<str> for Ident<S> {
172    fn borrow(&self) -> &str {
173        self.string.borrow()
174    }
175}
176
177impl From<Ident<&str>> for String {
178    fn from(value: Ident<&str>) -> Self {
179        value.as_str().to_owned()
180    }
181}
182
183impl From<Ident<String>> for String {
184    fn from(value: Ident<String>) -> Self {
185        value.into_inner()
186    }
187}
188
189impl<'a> From<Ident<Cow<'a, str>>> for Cow<'a, str> {
190    fn from(value: Ident<Cow<'a, str>>) -> Self {
191        value.into_inner()
192    }
193}
194
195impl<'a> From<Ident<Cow<'a, str>>> for Ident<String> {
196    fn from(value: Ident<Cow<'a, str>>) -> Self {
197        Self {
198            string: value.string.into(),
199        }
200    }
201}
202
203impl From<Ident<String>> for Ident<Cow<'_, str>> {
204    fn from(value: Ident<String>) -> Self {
205        Self {
206            string: value.string.into(),
207        }
208    }
209}
210
211impl<'a> From<Ident<&'a str>> for Ident<Cow<'a, str>> {
212    fn from(value: Ident<&'a str>) -> Self {
213        Ident {
214            string: value.string.into(),
215        }
216    }
217}
218
219impl<'a> From<Ident<&'a str>> for Ident<String> {
220    fn from(value: Ident<&'a str>) -> Self {
221        Ident {
222            string: value.string.into(),
223        }
224    }
225}
226
227impl FromStr for Ident<String> {
228    type Err = IdentError;
229
230    fn from_str(s: &str) -> Result<Self, Self::Err> {
231        Ok(Ident::new(s)?.into())
232    }
233}
234
235impl FromStr for Ident<Cow<'static, str>> {
236    type Err = IdentError;
237
238    fn from_str(s: &str) -> Result<Self, Self::Err> {
239        Ident::<String>::try_from(s).map(From::from)
240    }
241}
242
243impl<'a> TryFrom<&'a str> for Ident<String> {
244    type Error = IdentError;
245
246    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
247        Ok(Ident::new(value)?.into())
248    }
249}
250
251impl TryFrom<String> for Ident<String> {
252    type Error = IdentError;
253
254    fn try_from(value: String) -> Result<Self, Self::Error> {
255        Ok(Ident::new(value)?.into())
256    }
257}
258
259impl<'a> TryFrom<Cow<'a, str>> for Ident<String> {
260    type Error = IdentError;
261
262    fn try_from(value: Cow<'a, str>) -> Result<Self, Self::Error> {
263        Ok(Ident::new(value)?.into())
264    }
265}
266
267impl<'a> TryFrom<&'a str> for Ident<Cow<'a, str>> {
268    type Error = IdentError;
269
270    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
271        Self::new(value)
272    }
273}
274
275impl TryFrom<String> for Ident<Cow<'_, str>> {
276    type Error = IdentError;
277
278    fn try_from(value: String) -> Result<Self, Self::Error> {
279        Self::new(value)
280    }
281}
282
283impl<'a> TryFrom<Cow<'a, str>> for Ident<Cow<'a, str>> {
284    type Error = IdentError;
285
286    fn try_from(value: Cow<'a, str>) -> Result<Self, Self::Error> {
287        Self::new(value)
288    }
289}
290
291impl<S: fmt::Debug> fmt::Debug for Ident<S> {
292    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
293        self.string.fmt(f)
294    }
295}
296
297impl<S: fmt::Display> fmt::Display for Ident<S> {
298    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
299        self.string.fmt(f)
300    }
301}
302
303impl<S, T> PartialEq<Ident<T>> for Ident<S>
304where
305    S: PartialEq<T>,
306{
307    fn eq(&self, other: &Ident<T>) -> bool {
308        self.string == other.string
309    }
310}
311
312impl<S, T> PartialOrd<Ident<T>> for Ident<S>
313where
314    S: PartialOrd<T>,
315{
316    fn partial_cmp(&self, other: &Ident<T>) -> Option<Ordering> {
317        self.string.partial_cmp(&other.string)
318    }
319}
320
321impl<T: Serialize> Serialize for Ident<T> {
322    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
323    where
324        S: Serializer,
325    {
326        self.string.serialize(serializer)
327    }
328}
329
330impl<'de, S> Deserialize<'de> for Ident<S>
331where
332    S: Deserialize<'de>,
333    Ident<S>: TryFrom<S, Error = IdentError>,
334{
335    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
336    where
337        D: Deserializer<'de>,
338    {
339        Ident::try_from(S::deserialize(deserializer)?).map_err(D::Error::custom)
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346
347    #[test]
348    fn check_namespace_and_path() {
349        let id = ident!("namespace:path");
350        assert_eq!(id.namespace(), "namespace");
351        assert_eq!(id.path(), "path");
352    }
353
354    #[test]
355    fn parse_valid() {
356        ident!("minecraft:whatever");
357        ident!("_what-ever55_:.whatever/whatever123456789_");
358        ident!("valence:frobnicator");
359    }
360
361    #[test]
362    #[should_panic]
363    fn parse_invalid_0() {
364        Ident::new("").unwrap();
365    }
366
367    #[test]
368    #[should_panic]
369    fn parse_invalid_1() {
370        Ident::new(":").unwrap();
371    }
372
373    #[test]
374    #[should_panic]
375    fn parse_invalid_2() {
376        Ident::new("foo:bar:baz").unwrap();
377    }
378
379    #[test]
380    fn equality() {
381        assert_eq!(ident!("minecraft:my.identifier"), ident!("my.identifier"));
382    }
383}