concinnity_core/ecs/
asset_id.rs1use core::fmt;
11
12use alloc::format;
13use serde::de::{self, Visitor};
14use serde::{Deserialize, Deserializer, Serialize, Serializer};
15
16use crate::ecs::resolver::resolve_name;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
21pub struct AssetId(pub u32);
22
23impl AssetId {
24 pub const MINTED_BASE: u32 = u32::MAX - 0xFFFF;
31
32 pub fn is_minted(self) -> bool {
35 self.0 >= Self::MINTED_BASE
36 }
37}
38
39#[derive(Debug, Clone, Default)]
45pub struct MintedIds {
46 next: u32,
47}
48
49impl MintedIds {
50 pub fn next_id(&mut self) -> AssetId {
52 let id = AssetId(AssetId::MINTED_BASE + self.next);
53 self.next += 1;
54 id
55 }
56}
57
58impl fmt::Display for AssetId {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 write!(f, "#{}", self.0)
61 }
62}
63
64impl Serialize for AssetId {
65 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
66 s.serialize_u32(self.0)
67 }
68}
69
70struct AssetIdVisitor;
71
72impl Visitor<'_> for AssetIdVisitor {
73 type Value = AssetId;
74
75 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.write_str("an asset id integer or a name string")
77 }
78
79 fn visit_u64<E: de::Error>(self, v: u64) -> Result<AssetId, E> {
80 Ok(AssetId(v as u32))
81 }
82 fn visit_i64<E: de::Error>(self, v: i64) -> Result<AssetId, E> {
83 Ok(AssetId(v as u32))
84 }
85 fn visit_str<E: de::Error>(self, v: &str) -> Result<AssetId, E> {
86 resolve_name(v).map(AssetId).ok_or_else(|| {
87 E::custom(format!(
88 "no asset-name resolver installed to resolve reference {v:?}"
89 ))
90 })
91 }
92 fn visit_string<E: de::Error>(self, v: alloc::string::String) -> Result<AssetId, E> {
93 self.visit_str(&v)
94 }
95}
96
97impl<'de> Deserialize<'de> for AssetId {
98 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
99 if d.is_human_readable() {
100 d.deserialize_any(AssetIdVisitor)
101 } else {
102 Ok(AssetId(u32::deserialize(d)?))
103 }
104 }
105}
106
107pub fn de_opt_asset_ref<'de, D>(d: D) -> Result<Option<AssetId>, D::Error>
114where
115 D: Deserializer<'de>,
116{
117 if !d.is_human_readable() {
120 return Option::<AssetId>::deserialize(d);
121 }
122
123 struct OptVisitor;
124
125 impl Visitor<'_> for OptVisitor {
126 type Value = Option<AssetId>;
127
128 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 f.write_str("an asset reference name string, id integer, or null")
130 }
131
132 fn visit_unit<E: de::Error>(self) -> Result<Option<AssetId>, E> {
133 Ok(None)
134 }
135 fn visit_none<E: de::Error>(self) -> Result<Option<AssetId>, E> {
136 Ok(None)
137 }
138 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Option<AssetId>, E> {
139 Ok(Some(AssetId(v as u32)))
140 }
141 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Option<AssetId>, E> {
142 Ok(Some(AssetId(v as u32)))
143 }
144 fn visit_str<E: de::Error>(self, v: &str) -> Result<Option<AssetId>, E> {
145 if v.is_empty() {
146 Ok(None)
147 } else {
148 AssetIdVisitor.visit_str(v).map(Some)
149 }
150 }
151 fn visit_string<E: de::Error>(
152 self,
153 v: alloc::string::String,
154 ) -> Result<Option<AssetId>, E> {
155 self.visit_str(&v)
156 }
157 }
158
159 d.deserialize_any(OptVisitor)
160}
161
162pub use super::asset_ref::{AssetRef, de_opt_asset_ref_typed};
163#[cfg(test)]
164mod tests {
165 use alloc::string::ToString;
166
167 use super::*;
168
169 #[test]
170 fn round_trips_through_json_as_a_bare_integer() {
171 let bytes = serde_json::to_vec(&AssetId(7)).unwrap();
172 assert_eq!(bytes, b"7");
173 let back: AssetId = serde_json::from_slice(&bytes).unwrap();
174 assert_eq!(back, AssetId(7));
175 }
176
177 #[test]
178 fn deserializes_from_an_already_resolved_integer() {
179 let id: AssetId = serde_json::from_str("5").unwrap();
181 assert_eq!(id, AssetId(5));
182 }
183
184 #[test]
185 fn round_trips_through_postcard() {
186 let bytes = postcard::to_allocvec(&AssetId(1234)).unwrap();
188 let back: AssetId = postcard::from_bytes(&bytes).unwrap();
189 assert_eq!(back, AssetId(1234));
190 }
191
192 #[test]
193 fn a_truncated_baked_id_is_an_error_not_a_panic() {
194 assert!(postcard::from_bytes::<AssetId>(&[]).is_err());
198 }
199
200 #[test]
201 fn opt_ref_treats_empty_null_and_missing_as_none() {
202 #[derive(serde::Deserialize)]
203 struct Holder {
204 #[serde(default, deserialize_with = "de_opt_asset_ref")]
205 r: Option<AssetId>,
206 }
207 assert!(
208 serde_json::from_str::<Holder>("{\"r\":\"\"}")
209 .unwrap()
210 .r
211 .is_none()
212 );
213 assert!(
214 serde_json::from_str::<Holder>("{\"r\":null}")
215 .unwrap()
216 .r
217 .is_none()
218 );
219 assert!(serde_json::from_str::<Holder>("{}").unwrap().r.is_none());
220 assert_eq!(
221 serde_json::from_str::<Holder>("{\"r\":5}").unwrap().r,
222 Some(AssetId(5))
223 );
224 }
225
226 #[test]
227 fn opt_ref_round_trips_through_postcard() {
228 #[derive(serde::Serialize, serde::Deserialize)]
230 struct Holder {
231 #[serde(default, deserialize_with = "de_opt_asset_ref")]
232 r: Option<AssetId>,
233 #[serde(default, deserialize_with = "de_opt_asset_ref")]
234 none: Option<AssetId>,
235 }
236 let h = Holder {
237 r: Some(AssetId(7)),
238 none: None,
239 };
240 let bytes = postcard::to_allocvec(&h).unwrap();
241 let back: Holder = postcard::from_bytes(&bytes).unwrap();
242 assert_eq!(back.r, Some(AssetId(7)));
243 assert_eq!(back.none, None);
244 }
245
246 #[test]
247 fn display_formats_with_a_hash_prefix() {
248 assert_eq!(AssetId(42).to_string(), "#42");
249 }
250
251 #[test]
252 fn default_is_zero() {
253 assert_eq!(AssetId::default(), AssetId(0));
254 }
255
256 #[test]
257 fn deserializes_a_name_through_the_seam() {
258 crate::test_support::install_resolvers();
259 assert_eq!(
260 serde_json::from_str::<AssetId>("\"floor\"").unwrap(),
261 AssetId(5)
262 );
263 assert_eq!(
265 serde_json::from_value::<AssetId>(serde_json::json!("wall")).unwrap(),
266 AssetId(4)
267 );
268 }
269
270 #[test]
271 fn deserializes_a_signed_integer_narrowed_to_id_width() {
272 assert_eq!(
273 serde_json::from_str::<AssetId>("-1").unwrap(),
274 AssetId(u32::MAX)
275 );
276 }
277
278 #[test]
279 fn a_wrong_typed_id_names_what_it_accepts() {
280 let err = serde_json::from_str::<AssetId>("true")
281 .unwrap_err()
282 .to_string();
283 assert!(
284 err.contains("an asset id integer or a name string"),
285 "{err}"
286 );
287 }
288
289 #[derive(Debug, serde::Deserialize)]
290 struct Holder {
291 #[serde(default, deserialize_with = "de_opt_asset_ref")]
292 r: Option<AssetId>,
293 }
294
295 #[test]
296 fn opt_ref_accepts_names_signed_integers_and_a_reported_none() {
297 crate::test_support::install_resolvers();
298 assert_eq!(
299 serde_json::from_str::<Holder>("{\"r\":\"floor\"}")
300 .unwrap()
301 .r,
302 Some(AssetId(5))
303 );
304 assert_eq!(
305 serde_json::from_str::<Holder>("{\"r\":-1}").unwrap().r,
306 Some(AssetId(u32::MAX))
307 );
308 assert_eq!(
310 serde_json::from_value::<Holder>(serde_json::json!({"r": "wall"}))
311 .unwrap()
312 .r,
313 Some(AssetId(4))
314 );
315 assert_eq!(
316 serde_json::from_value::<Holder>(serde_json::json!({"r": ""}))
317 .unwrap()
318 .r,
319 None
320 );
321 assert_eq!(
323 de_opt_asset_ref(crate::test_support::NoneDeserializer).unwrap(),
324 None
325 );
326 }
327
328 #[test]
329 fn a_wrong_typed_opt_ref_names_what_it_accepts() {
330 let err = serde_json::from_str::<Holder>("{\"r\":true}")
331 .unwrap_err()
332 .to_string();
333 assert!(
334 err.contains("an asset reference name string, id integer, or null"),
335 "{err}"
336 );
337 }
338}