Skip to main content

astrelis_assets/
loader.rs

1//! Asset loader traits and infrastructure.
2
3use std::any::{Any, TypeId};
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use crate::error::{AssetError, AssetResult};
8use crate::source::AssetSource;
9
10/// Context provided to asset loaders during loading.
11pub struct LoadContext<'a> {
12    /// The source of the asset being loaded.
13    pub source: &'a AssetSource,
14    /// The raw bytes of the asset.
15    pub bytes: &'a [u8],
16    /// File extension (without the dot), if available.
17    pub extension: Option<&'a str>,
18}
19
20impl<'a> LoadContext<'a> {
21    /// Create a new load context.
22    pub fn new(source: &'a AssetSource, bytes: &'a [u8], extension: Option<&'a str>) -> Self {
23        Self {
24            source,
25            bytes,
26            extension,
27        }
28    }
29}
30
31/// Default priority for loaders.
32pub const DEFAULT_LOADER_PRIORITY: i32 = 0;
33
34/// Trait for loading assets from bytes.
35///
36/// Implement this trait to add support for loading a specific asset type.
37///
38/// # Example
39///
40/// ```ignore
41/// struct PngLoader;
42///
43/// impl AssetLoader for PngLoader {
44///     type Asset = Texture;
45///
46///     fn extensions(&self) -> &[&str] {
47///         &["png"]
48///     }
49///
50///     fn load(&self, ctx: LoadContext<'_>) -> AssetResult<Self::Asset> {
51///         // Decode PNG bytes into Texture...
52///     }
53/// }
54/// ```
55pub trait AssetLoader: Send + Sync + 'static {
56    /// The asset type this loader produces.
57    type Asset: Send + Sync + 'static;
58
59    /// The file extensions this loader handles (without dots).
60    ///
61    /// Example: `&["png", "jpg", "jpeg"]`
62    fn extensions(&self) -> &[&str];
63
64    /// Load an asset from the provided context.
65    fn load(&self, ctx: LoadContext<'_>) -> AssetResult<Self::Asset>;
66
67    /// Priority for this loader. Higher priority loaders are tried first
68    /// when multiple loaders handle the same extension for the same type.
69    ///
70    /// Default is 0. Use positive values to override default loaders.
71    fn priority(&self) -> i32 {
72        DEFAULT_LOADER_PRIORITY
73    }
74}
75
76/// Type-erased asset loader for dynamic dispatch.
77pub trait ErasedAssetLoader: Send + Sync {
78    /// Get the type ID of the asset this loader produces.
79    fn asset_type_id(&self) -> TypeId;
80
81    /// Get a human-readable name for the asset type.
82    fn asset_type_name(&self) -> &'static str;
83
84    /// Get the file extensions this loader handles.
85    fn extensions(&self) -> &[&str];
86
87    /// Get the priority of this loader.
88    fn priority(&self) -> i32;
89
90    /// Load an asset and return it as a boxed Any.
91    fn load_erased(&self, ctx: LoadContext<'_>) -> AssetResult<Box<dyn Any + Send + Sync>>;
92}
93
94impl<L: AssetLoader> ErasedAssetLoader for L
95where
96    L::Asset: crate::Asset,
97{
98    fn asset_type_id(&self) -> TypeId {
99        TypeId::of::<L::Asset>()
100    }
101
102    fn asset_type_name(&self) -> &'static str {
103        <L::Asset as crate::Asset>::type_name()
104    }
105
106    fn extensions(&self) -> &[&str] {
107        AssetLoader::extensions(self)
108    }
109
110    fn priority(&self) -> i32 {
111        AssetLoader::priority(self)
112    }
113
114    fn load_erased(&self, ctx: LoadContext<'_>) -> AssetResult<Box<dyn Any + Send + Sync>> {
115        let asset = self.load(ctx)?;
116        Ok(Box::new(asset))
117    }
118}
119
120/// Key for indexing loaders by type and extension.
121#[derive(Debug, Clone, PartialEq, Eq, Hash)]
122struct LoaderKey {
123    type_id: TypeId,
124    extension: String,
125}
126
127/// Entry in the loader registry with priority.
128struct LoaderEntry {
129    loader: Arc<dyn ErasedAssetLoader>,
130    priority: i32,
131}
132
133/// Registry of asset loaders, indexed by asset type and extension.
134///
135/// When loading an asset of type `T` with extension `.ext`, the registry
136/// finds all loaders that:
137/// 1. Produce type `T` (matching `TypeId`)
138/// 2. Handle extension `ext`
139///
140/// If multiple loaders match, the one with highest priority is used.
141#[derive(Default)]
142pub struct LoaderRegistry {
143    /// Loaders indexed by (TypeId, extension) -> sorted by priority (highest first).
144    by_type_and_ext: HashMap<LoaderKey, Vec<LoaderEntry>>,
145    /// All loaders for a given type (for listing).
146    by_type: HashMap<TypeId, Vec<Arc<dyn ErasedAssetLoader>>>,
147    /// All loaders for a given extension (for fallback/listing).
148    by_extension: HashMap<String, Vec<LoaderEntry>>,
149}
150
151impl LoaderRegistry {
152    /// Create a new empty loader registry.
153    pub fn new() -> Self {
154        Self::default()
155    }
156
157    /// Register a loader for its declared extensions.
158    ///
159    /// The loader will be used when loading assets of type `L::Asset`
160    /// with any of the extensions returned by `extensions()`.
161    pub fn register<L: AssetLoader>(&mut self, loader: L)
162    where
163        L::Asset: crate::Asset,
164    {
165        let loader = Arc::new(loader);
166        let type_id = loader.asset_type_id();
167        let priority = loader.priority();
168
169        // Register for each extension
170        for ext in loader.extensions() {
171            let ext_lower = ext.to_lowercase();
172
173            // Index by (type, extension)
174            let key = LoaderKey {
175                type_id,
176                extension: ext_lower.clone(),
177            };
178
179            let entries = self.by_type_and_ext.entry(key).or_default();
180            entries.push(LoaderEntry {
181                loader: loader.clone(),
182                priority,
183            });
184            // Sort by priority (highest first)
185            entries.sort_by(|a, b| b.priority.cmp(&a.priority));
186
187            // Index by extension only (for fallback)
188            let ext_entries = self.by_extension.entry(ext_lower).or_default();
189            ext_entries.push(LoaderEntry {
190                loader: loader.clone(),
191                priority,
192            });
193            ext_entries.sort_by(|a, b| b.priority.cmp(&a.priority));
194        }
195
196        // Index by type
197        self.by_type.entry(type_id).or_default().push(loader);
198    }
199
200    /// Get the best loader for a specific type and extension.
201    ///
202    /// Returns the highest-priority loader that produces type `T` and handles `extension`.
203    pub fn get_for_type_and_extension<T: 'static>(
204        &self,
205        extension: &str,
206    ) -> Option<&Arc<dyn ErasedAssetLoader>> {
207        let key = LoaderKey {
208            type_id: TypeId::of::<T>(),
209            extension: extension.to_lowercase(),
210        };
211
212        self.by_type_and_ext
213            .get(&key)
214            .and_then(|entries| entries.first())
215            .map(|entry| &entry.loader)
216    }
217
218    /// Get all loaders for a specific asset type.
219    pub fn get_by_type<T: 'static>(&self) -> Option<&[Arc<dyn ErasedAssetLoader>]> {
220        self.by_type.get(&TypeId::of::<T>()).map(|v| v.as_slice())
221    }
222
223    /// Get all loaders for an extension (any type), sorted by priority.
224    pub fn get_by_extension(&self, extension: &str) -> Option<&Arc<dyn ErasedAssetLoader>> {
225        let ext_lower = extension.to_lowercase();
226        self.by_extension
227            .get(&ext_lower)
228            .and_then(|entries| entries.first())
229            .map(|entry| &entry.loader)
230    }
231
232    /// Check if a loader is registered for an extension and type.
233    pub fn has_loader_for<T: 'static>(&self, extension: &str) -> bool {
234        let key = LoaderKey {
235            type_id: TypeId::of::<T>(),
236            extension: extension.to_lowercase(),
237        };
238        self.by_type_and_ext.contains_key(&key)
239    }
240
241    /// Check if a loader is registered for a type.
242    pub fn has_loader_for_type<T: 'static>(&self) -> bool {
243        self.by_type.contains_key(&TypeId::of::<T>())
244    }
245
246    /// Check if any loader is registered for an extension.
247    pub fn has_loader_for_extension(&self, extension: &str) -> bool {
248        let ext_lower = extension.to_lowercase();
249        self.by_extension.contains_key(&ext_lower)
250    }
251
252    /// Load an asset of type `T` using the appropriate loader.
253    ///
254    /// Finds a loader that:
255    /// 1. Produces type `T`
256    /// 2. Handles the given extension
257    ///
258    /// Returns an error if no such loader exists.
259    pub fn load_typed<T: crate::Asset>(
260        &self,
261        source: &AssetSource,
262        bytes: &[u8],
263        extension: Option<&str>,
264    ) -> AssetResult<T> {
265        let ext = extension.ok_or_else(|| AssetError::NoLoaderForExtension {
266            extension: "<none>".to_string(),
267        })?;
268
269        let loader =
270            self.get_for_type_and_extension::<T>(ext)
271                .ok_or_else(|| AssetError::NoLoader {
272                    type_id: TypeId::of::<T>(),
273                    type_name: Some(T::type_name()),
274                })?;
275
276        let ctx = LoadContext::new(source, bytes, Some(ext));
277        let boxed = loader.load_erased(ctx)?;
278
279        // This should always succeed since we looked up by TypeId
280        boxed
281            .downcast::<T>()
282            .map(|b| *b)
283            .map_err(|_| AssetError::TypeMismatch {
284                expected: T::type_name(),
285                actual: TypeId::of::<T>(),
286            })
287    }
288
289    /// Load an asset using extension-based lookup (type-erased).
290    ///
291    /// Uses the highest-priority loader for the extension regardless of type.
292    /// Primarily for backwards compatibility or when type is not known at compile time.
293    pub fn load(
294        &self,
295        source: &AssetSource,
296        bytes: &[u8],
297        extension: Option<&str>,
298    ) -> AssetResult<Box<dyn Any + Send + Sync>> {
299        let ext = extension.ok_or_else(|| AssetError::NoLoaderForExtension {
300            extension: "<none>".to_string(),
301        })?;
302
303        let loader =
304            self.get_by_extension(ext)
305                .ok_or_else(|| AssetError::NoLoaderForExtension {
306                    extension: ext.to_string(),
307                })?;
308
309        let ctx = LoadContext::new(source, bytes, Some(ext));
310        loader.load_erased(ctx)
311    }
312
313    /// List all registered extensions for a type.
314    pub fn extensions_for_type<T: 'static>(&self) -> Vec<&str> {
315        self.by_type
316            .get(&TypeId::of::<T>())
317            .map(|loaders| {
318                loaders
319                    .iter()
320                    .flat_map(|l| l.extensions().iter().copied())
321                    .collect()
322            })
323            .unwrap_or_default()
324    }
325}
326
327/// A simple text loader that loads UTF-8 strings.
328pub struct TextLoader;
329
330impl AssetLoader for TextLoader {
331    type Asset = String;
332
333    fn extensions(&self) -> &[&str] {
334        &["txt", "text", "md", "markdown"]
335    }
336
337    fn load(&self, ctx: LoadContext<'_>) -> AssetResult<Self::Asset> {
338        String::from_utf8(ctx.bytes.to_vec()).map_err(|e| AssetError::LoaderError {
339            path: ctx.source.display_path(),
340            message: format!("Invalid UTF-8: {}", e),
341        })
342    }
343}
344
345/// A simple binary loader that loads raw bytes.
346pub struct BytesLoader;
347
348impl AssetLoader for BytesLoader {
349    type Asset = Vec<u8>;
350
351    fn extensions(&self) -> &[&str] {
352        &["bin", "bytes", "dat"]
353    }
354
355    fn load(&self, ctx: LoadContext<'_>) -> AssetResult<Self::Asset> {
356        Ok(ctx.bytes.to_vec())
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363    use crate::Asset;
364
365    // Test asset type
366    #[derive(Debug, PartialEq)]
367    struct TestData {
368        value: i32,
369    }
370
371    impl Asset for TestData {
372        fn type_name() -> &'static str {
373            "TestData"
374        }
375    }
376
377    // Low priority loader
378    struct LowPriorityLoader;
379
380    impl AssetLoader for LowPriorityLoader {
381        type Asset = TestData;
382
383        fn extensions(&self) -> &[&str] {
384            &["dat"]
385        }
386
387        fn priority(&self) -> i32 {
388            -10
389        }
390
391        fn load(&self, _ctx: LoadContext<'_>) -> AssetResult<Self::Asset> {
392            Ok(TestData { value: 1 })
393        }
394    }
395
396    // High priority loader
397    struct HighPriorityLoader;
398
399    impl AssetLoader for HighPriorityLoader {
400        type Asset = TestData;
401
402        fn extensions(&self) -> &[&str] {
403            &["dat"]
404        }
405
406        fn priority(&self) -> i32 {
407            10
408        }
409
410        fn load(&self, _ctx: LoadContext<'_>) -> AssetResult<Self::Asset> {
411            Ok(TestData { value: 100 })
412        }
413    }
414
415    #[test]
416    fn test_text_loader() {
417        let loader = TextLoader;
418        let source = AssetSource::memory("test.txt");
419        let bytes = b"Hello, World!";
420        let ctx = LoadContext::new(&source, bytes, Some("txt"));
421
422        let result = loader.load(ctx).unwrap();
423        assert_eq!(result, "Hello, World!");
424    }
425
426    #[test]
427    fn test_bytes_loader() {
428        let loader = BytesLoader;
429        let source = AssetSource::memory("test.bin");
430        let bytes = &[0u8, 1, 2, 3, 4];
431        let ctx = LoadContext::new(&source, bytes, Some("bin"));
432
433        let result = loader.load(ctx).unwrap();
434        assert_eq!(result, vec![0, 1, 2, 3, 4]);
435    }
436
437    #[test]
438    fn test_loader_registry_by_type() {
439        let mut registry = LoaderRegistry::new();
440        registry.register(TextLoader);
441        registry.register(BytesLoader);
442
443        // Should find loader for String + txt
444        assert!(registry.has_loader_for::<String>("txt"));
445        assert!(registry.has_loader_for::<String>("TXT")); // Case insensitive
446
447        // Should find loader for Vec<u8> + bin
448        assert!(registry.has_loader_for::<Vec<u8>>("bin"));
449
450        // Should NOT find String loader for bin (wrong type)
451        assert!(!registry.has_loader_for::<String>("bin"));
452
453        // Should NOT find Vec<u8> loader for txt (wrong type)
454        assert!(!registry.has_loader_for::<Vec<u8>>("txt"));
455
456        assert!(registry.has_loader_for_type::<String>());
457        assert!(registry.has_loader_for_type::<Vec<u8>>());
458    }
459
460    #[test]
461    fn test_loader_priority() {
462        let mut registry = LoaderRegistry::new();
463
464        // Register low priority first, then high priority
465        registry.register(LowPriorityLoader);
466        registry.register(HighPriorityLoader);
467
468        // Should use high priority loader
469        let source = AssetSource::memory("test.dat");
470        let result: TestData = registry.load_typed(&source, b"", Some("dat")).unwrap();
471        assert_eq!(result.value, 100);
472    }
473
474    #[test]
475    fn test_loader_priority_reverse_order() {
476        let mut registry = LoaderRegistry::new();
477
478        // Register high priority first, then low priority
479        registry.register(HighPriorityLoader);
480        registry.register(LowPriorityLoader);
481
482        // Should still use high priority loader
483        let source = AssetSource::memory("test.dat");
484        let result: TestData = registry.load_typed(&source, b"", Some("dat")).unwrap();
485        assert_eq!(result.value, 100);
486    }
487
488    #[test]
489    fn test_typed_load() {
490        let mut registry = LoaderRegistry::new();
491        registry.register(TextLoader);
492
493        let source = AssetSource::memory("test.txt");
494        let result: String = registry
495            .load_typed(&source, b"Hello!", Some("txt"))
496            .unwrap();
497        assert_eq!(result, "Hello!");
498    }
499
500    #[test]
501    fn test_no_loader_for_type_extension_combo() {
502        let mut registry = LoaderRegistry::new();
503        registry.register(TextLoader); // Only handles String
504
505        // Try to load TestData from .txt - should fail
506        let source = AssetSource::memory("test.txt");
507        let result: AssetResult<TestData> = registry.load_typed(&source, b"data", Some("txt"));
508        assert!(result.is_err());
509    }
510}