use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::sync::Arc;
use crate::error::{AssetError, AssetResult};
use crate::source::AssetSource;
pub struct LoadContext<'a> {
pub source: &'a AssetSource,
pub bytes: &'a [u8],
pub extension: Option<&'a str>,
}
impl<'a> LoadContext<'a> {
pub fn new(source: &'a AssetSource, bytes: &'a [u8], extension: Option<&'a str>) -> Self {
Self {
source,
bytes,
extension,
}
}
}
pub const DEFAULT_LOADER_PRIORITY: i32 = 0;
pub trait AssetLoader: Send + Sync + 'static {
type Asset: Send + Sync + 'static;
fn extensions(&self) -> &[&str];
fn load(&self, ctx: LoadContext<'_>) -> AssetResult<Self::Asset>;
fn priority(&self) -> i32 {
DEFAULT_LOADER_PRIORITY
}
}
pub trait ErasedAssetLoader: Send + Sync {
fn asset_type_id(&self) -> TypeId;
fn asset_type_name(&self) -> &'static str;
fn extensions(&self) -> &[&str];
fn priority(&self) -> i32;
fn load_erased(&self, ctx: LoadContext<'_>) -> AssetResult<Box<dyn Any + Send + Sync>>;
}
impl<L: AssetLoader> ErasedAssetLoader for L
where
L::Asset: crate::Asset,
{
fn asset_type_id(&self) -> TypeId {
TypeId::of::<L::Asset>()
}
fn asset_type_name(&self) -> &'static str {
<L::Asset as crate::Asset>::type_name()
}
fn extensions(&self) -> &[&str] {
AssetLoader::extensions(self)
}
fn priority(&self) -> i32 {
AssetLoader::priority(self)
}
fn load_erased(&self, ctx: LoadContext<'_>) -> AssetResult<Box<dyn Any + Send + Sync>> {
let asset = self.load(ctx)?;
Ok(Box::new(asset))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct LoaderKey {
type_id: TypeId,
extension: String,
}
struct LoaderEntry {
loader: Arc<dyn ErasedAssetLoader>,
priority: i32,
}
#[derive(Default)]
pub struct LoaderRegistry {
by_type_and_ext: HashMap<LoaderKey, Vec<LoaderEntry>>,
by_type: HashMap<TypeId, Vec<Arc<dyn ErasedAssetLoader>>>,
by_extension: HashMap<String, Vec<LoaderEntry>>,
}
impl LoaderRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register<L: AssetLoader>(&mut self, loader: L)
where
L::Asset: crate::Asset,
{
let loader = Arc::new(loader);
let type_id = loader.asset_type_id();
let priority = loader.priority();
for ext in loader.extensions() {
let ext_lower = ext.to_lowercase();
let key = LoaderKey {
type_id,
extension: ext_lower.clone(),
};
let entries = self.by_type_and_ext.entry(key).or_default();
entries.push(LoaderEntry {
loader: loader.clone(),
priority,
});
entries.sort_by(|a, b| b.priority.cmp(&a.priority));
let ext_entries = self.by_extension.entry(ext_lower).or_default();
ext_entries.push(LoaderEntry {
loader: loader.clone(),
priority,
});
ext_entries.sort_by(|a, b| b.priority.cmp(&a.priority));
}
self.by_type.entry(type_id).or_default().push(loader);
}
pub fn get_for_type_and_extension<T: 'static>(
&self,
extension: &str,
) -> Option<&Arc<dyn ErasedAssetLoader>> {
let key = LoaderKey {
type_id: TypeId::of::<T>(),
extension: extension.to_lowercase(),
};
self.by_type_and_ext
.get(&key)
.and_then(|entries| entries.first())
.map(|entry| &entry.loader)
}
pub fn get_by_type<T: 'static>(&self) -> Option<&[Arc<dyn ErasedAssetLoader>]> {
self.by_type.get(&TypeId::of::<T>()).map(|v| v.as_slice())
}
pub fn get_by_extension(&self, extension: &str) -> Option<&Arc<dyn ErasedAssetLoader>> {
let ext_lower = extension.to_lowercase();
self.by_extension
.get(&ext_lower)
.and_then(|entries| entries.first())
.map(|entry| &entry.loader)
}
pub fn has_loader_for<T: 'static>(&self, extension: &str) -> bool {
let key = LoaderKey {
type_id: TypeId::of::<T>(),
extension: extension.to_lowercase(),
};
self.by_type_and_ext.contains_key(&key)
}
pub fn has_loader_for_type<T: 'static>(&self) -> bool {
self.by_type.contains_key(&TypeId::of::<T>())
}
pub fn has_loader_for_extension(&self, extension: &str) -> bool {
let ext_lower = extension.to_lowercase();
self.by_extension.contains_key(&ext_lower)
}
pub fn load_typed<T: crate::Asset>(
&self,
source: &AssetSource,
bytes: &[u8],
extension: Option<&str>,
) -> AssetResult<T> {
let ext = extension.ok_or_else(|| AssetError::NoLoaderForExtension {
extension: "<none>".to_string(),
})?;
let loader =
self.get_for_type_and_extension::<T>(ext)
.ok_or_else(|| AssetError::NoLoader {
type_id: TypeId::of::<T>(),
type_name: Some(T::type_name()),
})?;
let ctx = LoadContext::new(source, bytes, Some(ext));
let boxed = loader.load_erased(ctx)?;
boxed
.downcast::<T>()
.map(|b| *b)
.map_err(|_| AssetError::TypeMismatch {
expected: T::type_name(),
actual: TypeId::of::<T>(),
})
}
pub fn load(
&self,
source: &AssetSource,
bytes: &[u8],
extension: Option<&str>,
) -> AssetResult<Box<dyn Any + Send + Sync>> {
let ext = extension.ok_or_else(|| AssetError::NoLoaderForExtension {
extension: "<none>".to_string(),
})?;
let loader =
self.get_by_extension(ext)
.ok_or_else(|| AssetError::NoLoaderForExtension {
extension: ext.to_string(),
})?;
let ctx = LoadContext::new(source, bytes, Some(ext));
loader.load_erased(ctx)
}
pub fn extensions_for_type<T: 'static>(&self) -> Vec<&str> {
self.by_type
.get(&TypeId::of::<T>())
.map(|loaders| {
loaders
.iter()
.flat_map(|l| l.extensions().iter().copied())
.collect()
})
.unwrap_or_default()
}
}
pub struct TextLoader;
impl AssetLoader for TextLoader {
type Asset = String;
fn extensions(&self) -> &[&str] {
&["txt", "text", "md", "markdown"]
}
fn load(&self, ctx: LoadContext<'_>) -> AssetResult<Self::Asset> {
String::from_utf8(ctx.bytes.to_vec()).map_err(|e| AssetError::LoaderError {
path: ctx.source.display_path(),
message: format!("Invalid UTF-8: {}", e),
})
}
}
pub struct BytesLoader;
impl AssetLoader for BytesLoader {
type Asset = Vec<u8>;
fn extensions(&self) -> &[&str] {
&["bin", "bytes", "dat"]
}
fn load(&self, ctx: LoadContext<'_>) -> AssetResult<Self::Asset> {
Ok(ctx.bytes.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Asset;
#[derive(Debug, PartialEq)]
struct TestData {
value: i32,
}
impl Asset for TestData {
fn type_name() -> &'static str {
"TestData"
}
}
struct LowPriorityLoader;
impl AssetLoader for LowPriorityLoader {
type Asset = TestData;
fn extensions(&self) -> &[&str] {
&["dat"]
}
fn priority(&self) -> i32 {
-10
}
fn load(&self, _ctx: LoadContext<'_>) -> AssetResult<Self::Asset> {
Ok(TestData { value: 1 })
}
}
struct HighPriorityLoader;
impl AssetLoader for HighPriorityLoader {
type Asset = TestData;
fn extensions(&self) -> &[&str] {
&["dat"]
}
fn priority(&self) -> i32 {
10
}
fn load(&self, _ctx: LoadContext<'_>) -> AssetResult<Self::Asset> {
Ok(TestData { value: 100 })
}
}
#[test]
fn test_text_loader() {
let loader = TextLoader;
let source = AssetSource::memory("test.txt");
let bytes = b"Hello, World!";
let ctx = LoadContext::new(&source, bytes, Some("txt"));
let result = loader.load(ctx).unwrap();
assert_eq!(result, "Hello, World!");
}
#[test]
fn test_bytes_loader() {
let loader = BytesLoader;
let source = AssetSource::memory("test.bin");
let bytes = &[0u8, 1, 2, 3, 4];
let ctx = LoadContext::new(&source, bytes, Some("bin"));
let result = loader.load(ctx).unwrap();
assert_eq!(result, vec![0, 1, 2, 3, 4]);
}
#[test]
fn test_loader_registry_by_type() {
let mut registry = LoaderRegistry::new();
registry.register(TextLoader);
registry.register(BytesLoader);
assert!(registry.has_loader_for::<String>("txt"));
assert!(registry.has_loader_for::<String>("TXT"));
assert!(registry.has_loader_for::<Vec<u8>>("bin"));
assert!(!registry.has_loader_for::<String>("bin"));
assert!(!registry.has_loader_for::<Vec<u8>>("txt"));
assert!(registry.has_loader_for_type::<String>());
assert!(registry.has_loader_for_type::<Vec<u8>>());
}
#[test]
fn test_loader_priority() {
let mut registry = LoaderRegistry::new();
registry.register(LowPriorityLoader);
registry.register(HighPriorityLoader);
let source = AssetSource::memory("test.dat");
let result: TestData = registry.load_typed(&source, b"", Some("dat")).unwrap();
assert_eq!(result.value, 100);
}
#[test]
fn test_loader_priority_reverse_order() {
let mut registry = LoaderRegistry::new();
registry.register(HighPriorityLoader);
registry.register(LowPriorityLoader);
let source = AssetSource::memory("test.dat");
let result: TestData = registry.load_typed(&source, b"", Some("dat")).unwrap();
assert_eq!(result.value, 100);
}
#[test]
fn test_typed_load() {
let mut registry = LoaderRegistry::new();
registry.register(TextLoader);
let source = AssetSource::memory("test.txt");
let result: String = registry
.load_typed(&source, b"Hello!", Some("txt"))
.unwrap();
assert_eq!(result, "Hello!");
}
#[test]
fn test_no_loader_for_type_extension_combo() {
let mut registry = LoaderRegistry::new();
registry.register(TextLoader);
let source = AssetSource::memory("test.txt");
let result: AssetResult<TestData> = registry.load_typed(&source, b"data", Some("txt"));
assert!(result.is_err());
}
}