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#[doc(hidden)]
14pub use valence_ident_macros::parse_ident_str;
15
16#[macro_export]
31macro_rules! ident {
32 ($string:literal) => {
33 $crate::Ident::<&'static str>::new_unchecked($crate::parse_ident_str!($string))
34 };
35}
36
37#[derive(Copy, Clone, Eq, Ord, Hash)]
49pub struct Ident<S> {
50 string: S,
51}
52
53#[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 #[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 pub fn namespace(&self) -> &str
104 where
105 S: AsRef<str>,
106 {
107 self.namespace_and_path().0
108 }
109
110 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}