minecraft_command_types/
resource_location.rs1use 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}