azalea_registry/
identifier.rs

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