use std::any::Any;
use std::fmt;
use crate::asset::handle::AssetPath;
#[derive(Debug, Clone)]
pub enum AssetLoadError {
Io { path: String, message: String },
Parse { path: String, message: String },
UnsupportedFormat { extension: String },
InvalidData { message: String },
DependencyFailed { dependency: String, message: String },
NoLoader,
Other(String),
}
impl fmt::Display for AssetLoadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AssetLoadError::Io { path, message } => {
write!(f, "IO error loading '{path}': {message}")
}
AssetLoadError::Parse { path, message } => {
write!(f, "Parse error in '{path}': {message}")
}
AssetLoadError::UnsupportedFormat { extension } => {
write!(f, "Unsupported file extension: '.{extension}'")
}
AssetLoadError::InvalidData { message } => {
write!(f, "Invalid asset data: {message}")
}
AssetLoadError::DependencyFailed { dependency, message } => {
write!(f, "Dependency '{dependency}' failed: {message}")
}
AssetLoadError::NoLoader => write!(f, "No loader registered for this asset type"),
AssetLoadError::Other(s) => write!(f, "Asset load error: {s}"),
}
}
}
impl std::error::Error for AssetLoadError {}
pub struct LoadedAsset<T> {
pub asset: T,
pub dependencies: Vec<AssetPath>,
}
impl<T> LoadedAsset<T> {
pub fn new(asset: T) -> Self {
Self { asset, dependencies: Vec::new() }
}
pub fn with_dependencies(asset: T, dependencies: Vec<AssetPath>) -> Self {
Self { asset, dependencies }
}
pub fn add_dependency(&mut self, path: impl Into<AssetPath>) {
self.dependencies.push(path.into());
}
}
pub struct LoadContext<'a> {
pub path: &'a str,
labeled: Vec<(String, Box<dyn Any + Send + Sync>)>,
dependencies: Vec<AssetPath>,
}
impl<'a> LoadContext<'a> {
pub fn new(path: &'a str) -> Self {
Self { path, labeled: Vec::new(), dependencies: Vec::new() }
}
pub fn add_labeled_asset<T: Any + Send + Sync>(&mut self, label: impl Into<String>, asset: T) {
self.labeled.push((label.into(), Box::new(asset)));
}
pub fn add_dependency(&mut self, path: impl Into<AssetPath>) {
self.dependencies.push(path.into());
}
pub fn take_labeled(&mut self) -> Vec<(String, Box<dyn Any + Send + Sync>)> {
std::mem::take(&mut self.labeled)
}
pub fn take_dependencies(&mut self) -> Vec<AssetPath> {
std::mem::take(&mut self.dependencies)
}
pub fn extension(&self) -> Option<&str> {
self.path.rsplit('.').next().filter(|e| !e.contains('/'))
}
}
pub trait AssetLoader: Send + Sync + 'static {
fn extensions(&self) -> &[&str];
fn load_bytes(
&self,
bytes: &[u8],
path: &str,
ctx: &mut LoadContext<'_>,
) -> Result<Box<dyn Any + Send + Sync>, AssetLoadError>;
fn name(&self) -> &str {
std::any::type_name::<Self>()
}
}
pub trait Asset: Any + Send + Sync + 'static {
type Loader: AssetLoader + Default;
}
pub trait AssetProcessor: Send + Sync + 'static {
type Asset: Asset;
fn process(&self, asset: &mut Self::Asset) -> Result<(), AssetLoadError>;
fn name(&self) -> &str {
std::any::type_name::<Self>()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextAsset {
pub text: String,
pub source_path: String,
}
impl TextAsset {
pub fn new(text: impl Into<String>) -> Self {
Self { text: text.into(), source_path: String::new() }
}
pub fn line_count(&self) -> usize {
self.text.lines().count()
}
pub fn line(&self, index: usize) -> Option<&str> {
self.text.lines().nth(index)
}
}
#[derive(Default)]
pub struct TextAssetLoader;
impl AssetLoader for TextAssetLoader {
fn extensions(&self) -> &[&str] {
&["txt", "text", "md", "log"]
}
fn load_bytes(
&self,
bytes: &[u8],
path: &str,
_ctx: &mut LoadContext<'_>,
) -> Result<Box<dyn Any + Send + Sync>, AssetLoadError> {
let text = std::str::from_utf8(bytes).map_err(|e| AssetLoadError::Parse {
path: path.to_string(),
message: format!("invalid UTF-8: {e}"),
})?;
Ok(Box::new(TextAsset {
text: text.to_string(),
source_path: path.to_string(),
}))
}
}
impl Asset for TextAsset {
type Loader = TextAssetLoader;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BytesAsset {
pub bytes: Vec<u8>,
pub source_path: String,
}
impl BytesAsset {
pub fn new(bytes: Vec<u8>) -> Self {
Self { bytes, source_path: String::new() }
}
pub fn len(&self) -> usize {
self.bytes.len()
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
pub fn as_str(&self) -> Option<&str> {
std::str::from_utf8(&self.bytes).ok()
}
}
#[derive(Default)]
pub struct BytesAssetLoader;
impl AssetLoader for BytesAssetLoader {
fn extensions(&self) -> &[&str] {
&["bin", "dat", "raw", "bytes"]
}
fn load_bytes(
&self,
bytes: &[u8],
path: &str,
_ctx: &mut LoadContext<'_>,
) -> Result<Box<dyn Any + Send + Sync>, AssetLoadError> {
Ok(Box::new(BytesAsset {
bytes: bytes.to_vec(),
source_path: path.to_string(),
}))
}
}
impl Asset for BytesAsset {
type Loader = BytesAssetLoader;
}
#[derive(Debug, Clone)]
pub struct TomlAsset {
pub value: toml::Value,
pub source_path: String,
}
impl TomlAsset {
pub fn new(value: toml::Value) -> Self {
Self { value, source_path: String::new() }
}
pub fn get(&self, key: &str) -> Option<&toml::Value> {
let mut current = &self.value;
for part in key.split('.') {
current = current.get(part)?;
}
Some(current)
}
pub fn get_str(&self, key: &str) -> Option<&str> {
self.get(key)?.as_str()
}
pub fn get_int(&self, key: &str) -> Option<i64> {
self.get(key)?.as_integer()
}
pub fn get_float(&self, key: &str) -> Option<f64> {
self.get(key)?.as_float()
}
pub fn get_bool(&self, key: &str) -> Option<bool> {
self.get(key)?.as_bool()
}
}
#[derive(Default)]
pub struct TomlAssetLoader;
impl AssetLoader for TomlAssetLoader {
fn extensions(&self) -> &[&str] {
&["toml"]
}
fn load_bytes(
&self,
bytes: &[u8],
path: &str,
_ctx: &mut LoadContext<'_>,
) -> Result<Box<dyn Any + Send + Sync>, AssetLoadError> {
let text = std::str::from_utf8(bytes).map_err(|e| AssetLoadError::Parse {
path: path.to_string(),
message: format!("invalid UTF-8: {e}"),
})?;
let value = text.parse::<toml::Value>().map_err(|e| AssetLoadError::Parse {
path: path.to_string(),
message: format!("TOML parse error: {e}"),
})?;
Ok(Box::new(TomlAsset { value, source_path: path.to_string() }))
}
}
impl Asset for TomlAsset {
type Loader = TomlAssetLoader;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScriptAsset {
pub source: String,
pub language: ScriptLanguage,
pub source_path: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScriptLanguage {
Lua,
Custom(String),
Unknown,
}
impl ScriptAsset {
pub fn new(source: impl Into<String>, language: ScriptLanguage) -> Self {
Self { source: source.into(), language, source_path: String::new() }
}
pub fn line_count(&self) -> usize {
self.source.lines().count()
}
pub fn header_line(&self) -> Option<&str> {
self.source.lines().next()
}
}
#[derive(Default)]
pub struct ScriptAssetLoader;
impl AssetLoader for ScriptAssetLoader {
fn extensions(&self) -> &[&str] {
&["lua", "script", "scr", "gs"]
}
fn load_bytes(
&self,
bytes: &[u8],
path: &str,
_ctx: &mut LoadContext<'_>,
) -> Result<Box<dyn Any + Send + Sync>, AssetLoadError> {
let source = std::str::from_utf8(bytes).map_err(|e| AssetLoadError::Parse {
path: path.to_string(),
message: format!("invalid UTF-8 in script: {e}"),
})?;
let language = if path.ends_with(".lua") {
ScriptLanguage::Lua
} else {
ScriptLanguage::Unknown
};
Ok(Box::new(ScriptAsset {
source: source.to_string(),
language,
source_path: path.to_string(),
}))
}
}
impl Asset for ScriptAsset {
type Loader = ScriptAssetLoader;
}
#[cfg(test)]
mod tests {
use super::*;
fn make_ctx(path: &str) -> LoadContext<'_> {
LoadContext::new(path)
}
#[test]
fn text_asset_loader_valid_utf8() {
let loader = TextAssetLoader;
let bytes = b"Hello, world!";
let mut ctx = make_ctx("test.txt");
let result = loader.load_bytes(bytes, "test.txt", &mut ctx);
assert!(result.is_ok());
let boxed = result.unwrap();
let asset = boxed.downcast_ref::<TextAsset>().unwrap();
assert_eq!(asset.text, "Hello, world!");
}
#[test]
fn text_asset_loader_invalid_utf8() {
let loader = TextAssetLoader;
let bytes = &[0xFF, 0xFE, 0x00];
let mut ctx = make_ctx("bad.txt");
let result = loader.load_bytes(bytes, "bad.txt", &mut ctx);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), AssetLoadError::Parse { .. }));
}
#[test]
fn bytes_asset_loader_raw() {
let loader = BytesAssetLoader;
let bytes = &[1u8, 2, 3, 4, 5];
let mut ctx = make_ctx("data.bin");
let result = loader.load_bytes(bytes, "data.bin", &mut ctx);
assert!(result.is_ok());
let boxed = result.unwrap();
let asset = boxed.downcast_ref::<BytesAsset>().unwrap();
assert_eq!(asset.bytes, &[1, 2, 3, 4, 5]);
}
#[test]
fn toml_asset_loader_parse() {
let loader = TomlAssetLoader;
let toml_src = b"[window]\nwidth = 1920\nheight = 1080\n";
let mut ctx = make_ctx("config.toml");
let result = loader.load_bytes(toml_src, "config.toml", &mut ctx);
assert!(result.is_ok());
let boxed = result.unwrap();
let asset = boxed.downcast_ref::<TomlAsset>().unwrap();
assert_eq!(asset.get_int("window.width"), Some(1920));
assert_eq!(asset.get_int("window.height"), Some(1080));
}
#[test]
fn script_asset_loader_lua() {
let loader = ScriptAssetLoader;
let src = b"-- hello from lua\nprint('hi')";
let mut ctx = make_ctx("init.lua");
let result = loader.load_bytes(src, "init.lua", &mut ctx);
assert!(result.is_ok());
let boxed = result.unwrap();
let asset = boxed.downcast_ref::<ScriptAsset>().unwrap();
assert_eq!(asset.language, ScriptLanguage::Lua);
assert_eq!(asset.line_count(), 2);
}
#[test]
fn load_context_sub_assets() {
let mut ctx = LoadContext::new("atlas.png");
ctx.add_labeled_asset("frame_0", TextAsset::new("frame data"));
ctx.add_dependency(AssetPath::new("palette.toml"));
let labeled = ctx.take_labeled();
let deps = ctx.take_dependencies();
assert_eq!(labeled.len(), 1);
assert_eq!(labeled[0].0, "frame_0");
assert_eq!(deps.len(), 1);
}
#[test]
fn text_asset_line_helpers() {
let asset = TextAsset::new("line one\nline two\nline three");
assert_eq!(asset.line_count(), 3);
assert_eq!(asset.line(1), Some("line two"));
assert_eq!(asset.line(99), None);
}
#[test]
fn asset_loader_extensions() {
assert!(TextAssetLoader.extensions().contains(&"txt"));
assert!(TomlAssetLoader.extensions().contains(&"toml"));
assert!(ScriptAssetLoader.extensions().contains(&"lua"));
assert!(BytesAssetLoader.extensions().contains(&"bin"));
}
}