azalea_registry/
identifier.rs1use 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#[doc(alias = "ResourceLocation")]
22#[derive(Clone, Default, Eq)]
23pub struct Identifier {
24 colon_index: Option<NonZeroUsize>,
26 inner: Box<str>,
27}
28
29static DEFAULT_NAMESPACE: &str = "minecraft";
30impl 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}