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