minecraft_command_types/
resource_location.rs

1use itertools::Itertools;
2use minecraft_command_types_derive::HasMacro;
3use nonempty::{NonEmpty, nonempty};
4use serde::de::Visitor;
5use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
6use std::fmt::{Display, Formatter};
7use std::str::FromStr;
8
9#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, HasMacro)]
10pub struct ResourceLocation {
11    pub is_tag: bool,
12    pub namespace: Option<String>,
13    pub paths: NonEmpty<String>,
14}
15
16impl ResourceLocation {
17    #[inline]
18    #[must_use]
19    pub fn new<T: ToString>(is_tag: bool, namespace: Option<T>, paths: NonEmpty<T>) -> Self {
20        Self {
21            is_tag,
22            namespace: namespace.map(|namespace| namespace.to_string()),
23            paths: paths.map(|path| path.to_string()),
24        }
25    }
26
27    #[inline]
28    #[must_use]
29    pub fn new_namespace_paths<T: ToString>(namespace: T, paths: NonEmpty<T>) -> Self {
30        Self::new(false, Some(namespace), paths)
31    }
32
33    #[inline]
34    #[must_use]
35    pub fn new_namespace_path<T: ToString>(namespace: T, path: T) -> Self {
36        Self::new_namespace_paths(namespace, nonempty![path])
37    }
38
39    #[inline]
40    #[must_use]
41    pub fn new_paths<T: ToString>(paths: NonEmpty<T>) -> Self {
42        Self::new(false, None, paths)
43    }
44
45    #[inline]
46    #[must_use]
47    pub fn new_path<T: ToString>(path: T) -> Self {
48        Self::new_paths(nonempty![path])
49    }
50
51    pub fn paths_string(&self) -> String {
52        self.paths.iter().join("/")
53    }
54}
55
56impl Display for ResourceLocation {
57    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
58        if self.is_tag {
59            f.write_str("#")?;
60        }
61
62        if let Some(namespace) = &self.namespace
63            && *namespace != "minecraft"
64        {
65            write!(f, "{}:", namespace)?;
66        }
67
68        self.paths.iter().join("/").fmt(f)
69    }
70}
71
72#[derive(Debug)]
73pub enum ResourceLocationParseError {
74    EmptyString,
75    InvalidFormat(String),
76}
77
78impl Display for ResourceLocationParseError {
79    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
80        match self {
81            ResourceLocationParseError::EmptyString => {
82                f.write_str("Resource location string cannot be empty")
83            }
84            ResourceLocationParseError::InvalidFormat(msg) => {
85                write!(f, "Invalid resource location format: {}", msg)
86            }
87        }
88    }
89}
90
91impl std::error::Error for ResourceLocationParseError {}
92
93impl FromStr for ResourceLocation {
94    type Err = ResourceLocationParseError;
95
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        if s.is_empty() {
98            return Err(ResourceLocationParseError::EmptyString);
99        }
100
101        let mut remaining = s;
102        let mut is_tag = false;
103
104        if remaining.starts_with('#') {
105            is_tag = true;
106            remaining = &remaining[1..];
107        }
108
109        let parts: Vec<&str> = remaining.split(':').collect();
110
111        let (namespace_raw, path_raw) = match parts.len() {
112            1 => (None, parts[0]),
113            2 => {
114                if parts[0].is_empty() {
115                    return Err(ResourceLocationParseError::InvalidFormat(
116                        "Namespace component cannot be empty".to_string(),
117                    ));
118                }
119                (Some(parts[0]), parts[1])
120            }
121            _ => {
122                return Err(ResourceLocationParseError::InvalidFormat(
123                    "Too many ':' separators".to_string(),
124                ));
125            }
126        };
127
128        if path_raw.is_empty() {
129            return Err(ResourceLocationParseError::InvalidFormat(
130                "Path component cannot be empty".to_string(),
131            ));
132        }
133
134        let path_components: Vec<String> = path_raw.split('/').map(|s| s.to_owned()).collect();
135
136        let paths = NonEmpty::from_vec(path_components)
137            .expect("Path component check guarantees paths are not empty");
138
139        let namespace = namespace_raw.map(|s| s.to_string());
140
141        Ok(ResourceLocation {
142            is_tag,
143            namespace,
144            paths,
145        })
146    }
147}
148
149impl Serialize for ResourceLocation {
150    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
151    where
152        S: Serializer,
153    {
154        serializer.collect_str(self)
155    }
156}
157
158struct ResourceLocationVisitor;
159
160impl<'de> Visitor<'de> for ResourceLocationVisitor {
161    type Value = ResourceLocation;
162
163    fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
164        formatter.write_str("a string representing a Minecraft resource location (e.g., 'minecraft:stone', 'stone', or '#forge:ingots/iron')")
165    }
166
167    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
168    where
169        E: de::Error,
170    {
171        v.parse()
172            .map_err(|e| E::custom(format!("failed to parse resource location: {}", e)))
173    }
174
175    fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
176    where
177        E: de::Error,
178    {
179        self.visit_str(&v)
180    }
181}
182
183impl<'de> Deserialize<'de> for ResourceLocation {
184    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185    where
186        D: Deserializer<'de>,
187    {
188        deserializer.deserialize_string(ResourceLocationVisitor)
189    }
190}