Skip to main content

azalea_registry/
identifier.rs

1//! An arbitrary identifier or resource location.
2
3use std::{
4    fmt::{self, Debug, Display},
5    hash::{Hash, Hasher},
6    io::{self, Cursor, Write},
7    num::NonZeroUsize,
8    str::FromStr,
9};
10
11use azalea_buf::{AzBuf, BufReadError};
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
14use simdnbt::{FromNbtTag, ToNbtTag, owned::NbtTag};
15
16/// An identifier, like `minecraft:stone` or `brigadier:number`.
17///
18/// All registry variants can be converted to an identifier.
19///
20/// This was formerly called a `ResourceLocation`.
21#[doc(alias = "ResourceLocation")]
22#[derive(Clone, Default, Eq)]
23pub struct Identifier {
24    // empty namespaces aren't allowed so NonZero is fine.
25    colon_index: Option<NonZeroUsize>,
26    inner: Box<str>,
27}
28
29static DEFAULT_NAMESPACE: &str = "minecraft";
30// static REALMS_NAMESPACE: &str = "realms";
31
32impl Identifier {
33    pub fn new(resource_string: impl Into<String>) -> Identifier {
34        let mut resource_string = resource_string.into();
35
36        let colon_index = resource_string.find(':');
37        let colon_index = if let Some(colon_index) = colon_index {
38            if colon_index == 0 {
39                resource_string = resource_string.split_off(1);
40            }
41            NonZeroUsize::new(colon_index)
42        } else {
43            None
44        };
45
46        Self {
47            colon_index,
48            inner: resource_string.into(),
49        }
50    }
51
52    pub fn namespace(&self) -> &str {
53        if let Some(colon_index) = self.colon_index {
54            &self.inner[0..colon_index.get()]
55        } else {
56            DEFAULT_NAMESPACE
57        }
58    }
59    pub fn path(&self) -> &str {
60        if let Some(colon_index) = self.colon_index {
61            &self.inner[(colon_index.get() + 1)..]
62        } else {
63            &self.inner
64        }
65    }
66}
67impl PartialEq for Identifier {
68    fn eq(&self, other: &Self) -> bool {
69        self.namespace() == other.namespace() && self.path() == other.path()
70    }
71}
72impl Hash for Identifier {
73    fn hash<H: Hasher>(&self, state: &mut H) {
74        let namespace = self.namespace();
75        if namespace != DEFAULT_NAMESPACE {
76            namespace.hash(state);
77        }
78        self.path().hash(state);
79    }
80}
81
82impl Display for Identifier {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        if self.colon_index.is_some() {
85            write!(f, "{}", self.inner)
86        } else {
87            write!(f, "{DEFAULT_NAMESPACE}:{}", self.inner)
88        }
89    }
90}
91impl Debug for Identifier {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        write!(f, "{self}")
94    }
95}
96impl FromStr for Identifier {
97    type Err = &'static str;
98
99    fn from_str(s: &str) -> Result<Self, Self::Err> {
100        Ok(Identifier::new(s))
101    }
102}
103impl From<&str> for Identifier {
104    fn from(s: &str) -> Self {
105        Identifier::new(s)
106    }
107}
108
109impl AzBuf for Identifier {
110    fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
111        let location_string = String::azalea_read(buf)?;
112        Ok(Identifier::new(&location_string))
113    }
114    fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
115        self.to_string().azalea_write(buf)
116    }
117}
118
119#[cfg(feature = "serde")]
120impl Serialize for Identifier {
121    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
122    where
123        S: Serializer,
124    {
125        serializer.serialize_str(&self.to_string())
126    }
127}
128
129#[cfg(feature = "serde")]
130impl<'de> Deserialize<'de> for Identifier {
131    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
132    where
133        D: Deserializer<'de>,
134    {
135        let s = String::deserialize(deserializer)?;
136        if s.contains(':') {
137            Ok(Identifier::new(&s))
138        } else {
139            Err(de::Error::invalid_value(
140                de::Unexpected::Str(&s),
141                &"a valid Identifier",
142            ))
143        }
144    }
145}
146
147impl FromNbtTag for Identifier {
148    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
149        tag.string().and_then(|s| s.to_str().parse().ok())
150    }
151}
152
153impl ToNbtTag for Identifier {
154    fn to_nbt_tag(self) -> NbtTag {
155        NbtTag::String(self.to_string().into())
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn basic_identifier() {
165        let r = Identifier::new("abcdef:ghijkl");
166        assert_eq!(r.namespace(), "abcdef");
167        assert_eq!(r.path(), "ghijkl");
168    }
169    #[test]
170    fn no_namespace() {
171        let r = Identifier::new("azalea");
172        assert_eq!(r.namespace(), "minecraft");
173        assert_eq!(r.path(), "azalea");
174    }
175    #[test]
176    fn colon_start() {
177        let r = Identifier::new(":azalea");
178        assert_eq!(r.namespace(), "minecraft");
179        assert_eq!(r.path(), "azalea");
180    }
181    #[test]
182    fn colon_end() {
183        let r = Identifier::new("azalea:");
184        assert_eq!(r.namespace(), "azalea");
185        assert_eq!(r.path(), "");
186    }
187
188    #[test]
189    fn azbuf_identifier() {
190        let mut buf = Vec::new();
191        Identifier::new("minecraft:dirt")
192            .azalea_write(&mut buf)
193            .unwrap();
194
195        let mut buf = Cursor::new(&buf[..]);
196
197        assert_eq!(
198            Identifier::azalea_read(&mut buf).unwrap(),
199            Identifier::new("minecraft:dirt")
200        );
201    }
202}