1use core::fmt;
17use core::marker::PhantomData;
18
19use alloc::boxed::Box;
20use alloc::string::String;
21use serde::de::{self, Visitor};
22use serde::{Deserialize, Deserializer, Serialize, Serializer};
23
24use crate::ecs::asset_id::AssetId;
25use crate::ecs::resolver::resolve_name;
26
27pub struct AssetRef<T> {
29 name: Box<str>,
32 id: Option<AssetId>,
34 _target: PhantomData<fn() -> T>,
37}
38
39impl<T> AssetRef<T> {
40 pub fn by_name(name: impl Into<Box<str>>) -> Self {
42 Self {
43 name: name.into(),
44 id: None,
45 _target: PhantomData,
46 }
47 }
48
49 pub fn resolved(id: AssetId) -> Self {
51 Self {
52 name: Box::from(""),
53 id: Some(id),
54 _target: PhantomData,
55 }
56 }
57
58 pub fn name(&self) -> &str {
60 &self.name
61 }
62
63 pub fn id(&self) -> Option<AssetId> {
65 self.id
66 }
67
68 pub fn is_resolved(&self) -> bool {
70 self.id.is_some()
71 }
72
73 pub fn resolve(&mut self, id: AssetId) {
75 self.id = Some(id);
76 }
77}
78
79impl<T> Clone for AssetRef<T> {
82 fn clone(&self) -> Self {
83 Self {
84 name: self.name.clone(),
85 id: self.id,
86 _target: PhantomData,
87 }
88 }
89}
90
91impl<T> fmt::Debug for AssetRef<T> {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 f.debug_struct("AssetRef")
94 .field("name", &self.name)
95 .field("id", &self.id)
96 .finish()
97 }
98}
99
100impl<T> PartialEq for AssetRef<T> {
101 fn eq(&self, other: &Self) -> bool {
102 self.name == other.name && self.id == other.id
103 }
104}
105
106impl<T> Eq for AssetRef<T> {}
107
108impl<T> Default for AssetRef<T> {
111 fn default() -> Self {
112 Self {
113 name: Box::from(""),
114 id: None,
115 _target: PhantomData,
116 }
117 }
118}
119
120impl<T> Serialize for AssetRef<T> {
121 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
122 match self.id {
126 Some(id) => s.serialize_u32(id.0),
127 None => s.serialize_str(&self.name),
128 }
129 }
130}
131
132struct AssetRefVisitor<T>(PhantomData<fn() -> T>);
133
134impl<T> AssetRefVisitor<T> {
135 fn from_name(name: &str) -> AssetRef<T> {
136 match resolve_name(name) {
137 Some(id) => AssetRef::resolved(AssetId(id)),
138 None => AssetRef::by_name(name),
139 }
140 }
141}
142
143impl<'de, T> Visitor<'de> for AssetRefVisitor<T> {
144 type Value = AssetRef<T>;
145
146 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 f.write_str("an asset reference name string or a resolved id integer")
148 }
149
150 fn visit_str<E: de::Error>(self, v: &str) -> Result<AssetRef<T>, E> {
151 Ok(Self::from_name(v))
152 }
153 fn visit_string<E: de::Error>(self, v: String) -> Result<AssetRef<T>, E> {
154 Ok(Self::from_name(&v))
155 }
156 fn visit_u64<E: de::Error>(self, v: u64) -> Result<AssetRef<T>, E> {
157 Ok(AssetRef::resolved(AssetId(v as u32)))
158 }
159 fn visit_i64<E: de::Error>(self, v: i64) -> Result<AssetRef<T>, E> {
160 Ok(AssetRef::resolved(AssetId(v as u32)))
161 }
162}
163
164impl<'de, T> Deserialize<'de> for AssetRef<T> {
165 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
166 if d.is_human_readable() {
167 d.deserialize_any(AssetRefVisitor::<T>(PhantomData))
168 } else {
169 Ok(AssetRef::resolved(AssetId(u32::deserialize(d)?)))
170 }
171 }
172}
173
174pub fn de_opt_asset_ref_typed<'de, D, T>(d: D) -> Result<Option<AssetRef<T>>, D::Error>
180where
181 D: Deserializer<'de>,
182{
183 if !d.is_human_readable() {
186 return Option::<AssetRef<T>>::deserialize(d);
187 }
188
189 struct OptVisitor<T>(PhantomData<fn() -> T>);
190
191 impl<'de, T> Visitor<'de> for OptVisitor<T> {
192 type Value = Option<AssetRef<T>>;
193
194 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 f.write_str("an asset reference name string, id integer, or null")
196 }
197
198 fn visit_unit<E: de::Error>(self) -> Result<Option<AssetRef<T>>, E> {
199 Ok(None)
200 }
201 fn visit_none<E: de::Error>(self) -> Result<Option<AssetRef<T>>, E> {
202 Ok(None)
203 }
204 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Option<AssetRef<T>>, E> {
205 Ok(Some(AssetRef::resolved(AssetId(v as u32))))
206 }
207 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Option<AssetRef<T>>, E> {
208 Ok(Some(AssetRef::resolved(AssetId(v as u32))))
209 }
210 fn visit_str<E: de::Error>(self, v: &str) -> Result<Option<AssetRef<T>>, E> {
211 if v.is_empty() {
212 Ok(None)
213 } else {
214 Ok(Some(AssetRefVisitor::<T>::from_name(v)))
215 }
216 }
217 fn visit_string<E: de::Error>(self, v: String) -> Result<Option<AssetRef<T>>, E> {
218 self.visit_str(&v)
219 }
220 }
221
222 d.deserialize_any(OptVisitor(PhantomData))
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
231 use alloc::string::ToString;
232
233 struct Texture;
234
235 #[test]
236 fn deserializes_a_resolved_id_from_compiled_args() {
237 let r: AssetRef<Texture> = serde_json::from_str("5").unwrap();
238 assert_eq!(r.id(), Some(AssetId(5)));
239 assert!(r.is_resolved());
240 }
241
242 #[test]
243 fn resolve_fills_the_id() {
244 let mut r: AssetRef<Texture> = AssetRef::by_name("wall");
245 assert_eq!(r.name(), "wall");
246 r.resolve(AssetId(3));
247 assert_eq!(r.id(), Some(AssetId(3)));
248 }
249
250 #[test]
251 fn serializes_a_resolved_reference_as_an_integer() {
252 let r = AssetRef::<Texture>::resolved(AssetId(7));
254 assert_eq!(serde_json::to_string(&r).unwrap(), "7");
255 }
256
257 #[test]
258 fn serializes_an_unresolved_reference_as_its_name() {
259 let r = AssetRef::<Texture>::by_name("floor");
260 assert_eq!(serde_json::to_string(&r).unwrap(), "\"floor\"");
261 }
262
263 #[derive(Debug, serde::Deserialize)]
264 struct Holder {
265 #[serde(default, deserialize_with = "de_opt_asset_ref_typed")]
266 r: Option<AssetRef<Texture>>,
267 }
268
269 #[test]
270 fn opt_ref_treats_empty_null_and_missing_as_none() {
271 assert!(
272 serde_json::from_str::<Holder>("{\"r\":\"\"}")
273 .unwrap()
274 .r
275 .is_none()
276 );
277 assert!(
278 serde_json::from_str::<Holder>("{\"r\":null}")
279 .unwrap()
280 .r
281 .is_none()
282 );
283 assert!(serde_json::from_str::<Holder>("{}").unwrap().r.is_none());
284 assert_eq!(
285 serde_json::from_str::<Holder>("{\"r\":5}")
286 .unwrap()
287 .r
288 .unwrap()
289 .id(),
290 Some(AssetId(5))
291 );
292 }
293
294 #[test]
295 fn is_send_and_sync_regardless_of_target() {
296 fn assert_send_sync<U: Send + Sync>() {}
297 assert_send_sync::<AssetRef<Texture>>();
298 }
299
300 #[test]
301 fn a_blank_reference_is_unresolved_and_nameless() {
302 let r = AssetRef::<Texture>::default();
303 assert_eq!(r.name(), "");
304 assert_eq!(r.id(), None);
305 assert!(!r.is_resolved());
306 }
307
308 #[test]
309 fn clone_and_equality_compare_name_and_id() {
310 let named = AssetRef::<Texture>::by_name("floor");
313 assert_eq!(named.clone(), named);
314 assert_ne!(named, AssetRef::<Texture>::by_name("wall"));
315 assert_ne!(named, AssetRef::<Texture>::resolved(AssetId(5)));
316 let resolved = AssetRef::<Texture>::resolved(AssetId(5));
317 assert_eq!(resolved.clone(), resolved);
318 }
319
320 #[test]
321 fn debug_shows_the_name_and_id() {
322 let r = AssetRef::<Texture>::resolved(AssetId(5));
323 let shown = alloc::format!("{r:?}");
324 assert!(shown.contains("AssetRef"), "{shown}");
325 assert!(shown.contains("id: Some(AssetId(5))"), "{shown}");
326 }
327
328 #[test]
329 fn deserializes_a_name_through_the_seam() {
330 crate::test_support::install_resolvers();
331 let r: AssetRef<Texture> = serde_json::from_str("\"floor\"").unwrap();
332 assert_eq!(r.id(), Some(AssetId(5)));
333 let r: AssetRef<Texture> = serde_json::from_value(serde_json::json!("wall")).unwrap();
335 assert_eq!(r.id(), Some(AssetId(4)));
336 }
337
338 #[test]
339 fn deserializes_a_signed_integer_narrowed_to_id_width() {
340 let r: AssetRef<Texture> = serde_json::from_str("-1").unwrap();
341 assert_eq!(r.id(), Some(AssetId(u32::MAX)));
342 }
343
344 #[test]
345 fn a_wrong_typed_reference_names_what_it_accepts() {
346 let err = serde_json::from_str::<AssetRef<Texture>>("true")
347 .unwrap_err()
348 .to_string();
349 assert!(
350 err.contains("an asset reference name string or a resolved id integer"),
351 "{err}"
352 );
353 let err = serde_json::from_str::<Holder>("{\"r\":true}")
354 .unwrap_err()
355 .to_string();
356 assert!(
357 err.contains("an asset reference name string, id integer, or null"),
358 "{err}"
359 );
360 }
361
362 #[test]
363 fn round_trips_through_postcard_as_a_resolved_id() {
364 #[derive(serde::Serialize, serde::Deserialize)]
367 struct Baked {
368 plain: AssetRef<Texture>,
369 #[serde(default, deserialize_with = "de_opt_asset_ref_typed")]
370 opt: Option<AssetRef<Texture>>,
371 }
372 let baked = Baked {
373 plain: AssetRef::resolved(AssetId(7)),
374 opt: Some(AssetRef::resolved(AssetId(9))),
375 };
376 let bytes = postcard::to_allocvec(&baked).unwrap();
377 let back: Baked = postcard::from_bytes(&bytes).unwrap();
378 assert_eq!(back.plain.id(), Some(AssetId(7)));
379 assert_eq!(back.opt.unwrap().id(), Some(AssetId(9)));
380 }
381
382 #[test]
383 fn a_truncated_baked_reference_is_an_error_not_a_panic() {
384 assert!(postcard::from_bytes::<AssetRef<Texture>>(&[]).is_err());
387 }
388
389 #[test]
390 fn opt_ref_accepts_names_signed_integers_and_a_reported_none() {
391 crate::test_support::install_resolvers();
392 assert_eq!(
393 serde_json::from_str::<Holder>("{\"r\":\"floor\"}")
394 .unwrap()
395 .r
396 .unwrap()
397 .id(),
398 Some(AssetId(5))
399 );
400 assert_eq!(
401 serde_json::from_str::<Holder>("{\"r\":-1}")
402 .unwrap()
403 .r
404 .unwrap()
405 .id(),
406 Some(AssetId(u32::MAX))
407 );
408 assert_eq!(
410 serde_json::from_value::<Holder>(serde_json::json!({"r": "wall"}))
411 .unwrap()
412 .r
413 .unwrap()
414 .id(),
415 Some(AssetId(4))
416 );
417 assert!(
418 serde_json::from_value::<Holder>(serde_json::json!({"r": ""}))
419 .unwrap()
420 .r
421 .is_none()
422 );
423 assert!(
425 de_opt_asset_ref_typed::<_, Texture>(crate::test_support::NoneDeserializer)
426 .unwrap()
427 .is_none()
428 );
429 }
430}