use crate::ecs::asset_id::AssetId;
use crate::ecs::{Component, PayloadLocator};
use alloc::string::String;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FileKind {
Obj,
Png,
Jpg,
Jpeg,
Bmp,
Tga,
Gif,
Ttf,
Otf,
Txt,
Md,
Mtl,
}
impl FileKind {
pub fn from_ext(ext: &str) -> Option<Self> {
match ext.to_lowercase().as_str() {
"obj" => Some(Self::Obj),
"png" => Some(Self::Png),
"jpg" => Some(Self::Jpg),
"jpeg" => Some(Self::Jpeg),
"bmp" => Some(Self::Bmp),
"tga" => Some(Self::Tga),
"gif" => Some(Self::Gif),
"ttf" => Some(Self::Ttf),
"otf" => Some(Self::Otf),
"txt" => Some(Self::Txt),
"md" => Some(Self::Md),
"mtl" => Some(Self::Mtl),
_ => None,
}
}
pub fn is_mesh(&self) -> bool {
matches!(self, Self::Obj)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct FileArgs {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<FileKind>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_kind_is_reachable_from_an_extension() {
let all = [
("obj", FileKind::Obj),
("png", FileKind::Png),
("jpg", FileKind::Jpg),
("jpeg", FileKind::Jpeg),
("bmp", FileKind::Bmp),
("tga", FileKind::Tga),
("gif", FileKind::Gif),
("ttf", FileKind::Ttf),
("otf", FileKind::Otf),
("txt", FileKind::Txt),
("md", FileKind::Md),
("mtl", FileKind::Mtl),
];
for (ext, kind) in all {
assert_eq!(FileKind::from_ext(ext).as_ref(), Some(&kind), "{ext}");
assert_eq!(
FileKind::from_ext(&ext.to_uppercase()).as_ref(),
Some(&kind),
"{ext}"
);
}
}
#[test]
fn an_unknown_extension_has_no_kind() {
assert_eq!(FileKind::from_ext("wav"), None);
assert_eq!(FileKind::from_ext(""), None);
}
#[test]
fn only_obj_builds_to_a_mesh_payload() {
assert!(FileKind::Obj.is_mesh());
for kind in [FileKind::Png, FileKind::Ttf, FileKind::Mtl, FileKind::Md] {
assert!(!kind.is_mesh(), "{kind:?}");
}
}
#[test]
fn args_default_to_an_empty_path_and_inferred_kind() {
let args = FileArgs::default();
assert!(args.path.is_empty());
assert_eq!(args.kind, None);
}
#[test]
fn an_absent_kind_is_omitted_from_the_serialized_args() {
let args: FileArgs = serde_json::from_str(r#"{"path":"assets/board.obj"}"#).unwrap();
assert_eq!(args.path, "assets/board.obj");
assert_eq!(args.kind, None);
assert_eq!(
serde_json::to_string(&args).unwrap(),
r#"{"path":"assets/board.obj"}"#
);
}
#[test]
fn an_explicit_kind_round_trips_through_its_lowercase_name() {
let args: FileArgs = serde_json::from_str(r#"{"path":"font.dat","kind":"ttf"}"#).unwrap();
assert_eq!(args.kind, Some(FileKind::Ttf));
assert_eq!(
serde_json::to_string(&args).unwrap(),
r#"{"path":"font.dat","kind":"ttf"}"#
);
let bytes = postcard::to_allocvec(&args).unwrap();
let back: FileArgs = postcard::from_bytes(&bytes).unwrap();
assert_eq!(back.kind, Some(FileKind::Ttf));
assert_eq!(back.path, "font.dat");
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct File {
pub asset_id: AssetId,
pub path: String,
pub kind: Option<FileKind>,
pub locator: Option<PayloadLocator>,
}
impl File {
pub fn bake(args: FileArgs) -> Self {
let kind = args
.kind
.clone()
.or_else(|| super::path_extension(&args.path).and_then(FileKind::from_ext));
Self {
asset_id: AssetId::default(),
path: args.path,
kind,
locator: None,
}
}
}
impl Component for File {
const NAME: &'static str = "File";
fn from_baked(bytes: &[u8]) -> Result<Self, crate::result::CnResult> {
Ok(crate::blob::decode_exact(bytes)?)
}
fn inject_locator(&mut self, locator: PayloadLocator) {
self.locator = Some(locator);
}
fn inject_name(&mut self, id: AssetId) {
self.asset_id = id;
}
}
#[cfg(test)]
mod runtime_tests {
use super::*;
#[test]
fn from_ext_maps_every_known_extension() {
let cases = [
("obj", FileKind::Obj),
("png", FileKind::Png),
("jpg", FileKind::Jpg),
("jpeg", FileKind::Jpeg),
("bmp", FileKind::Bmp),
("tga", FileKind::Tga),
("gif", FileKind::Gif),
("ttf", FileKind::Ttf),
("otf", FileKind::Otf),
("txt", FileKind::Txt),
("md", FileKind::Md),
("mtl", FileKind::Mtl),
];
for (ext, want) in cases {
assert_eq!(FileKind::from_ext(ext), Some(want.clone()));
assert_eq!(FileKind::from_ext(&ext.to_uppercase()), Some(want));
}
assert_eq!(FileKind::from_ext("zzz"), None);
}
#[test]
fn from_args_infers_kind_from_the_extension() {
let f = File::bake(FileArgs {
path: "models/box.obj".into(),
kind: None,
});
assert_eq!(f.kind, Some(FileKind::Obj));
assert_eq!(f.path, "models/box.obj");
let g = File::bake(FileArgs {
path: "data.obj".into(),
kind: Some(FileKind::Txt),
});
assert_eq!(g.kind, Some(FileKind::Txt));
let h = File::bake(FileArgs {
path: "notes.zzz".into(),
kind: None,
});
assert_eq!(h.kind, None);
}
#[test]
fn is_mesh_is_true_only_for_obj() {
assert!(FileKind::Obj.is_mesh());
assert!(!FileKind::Png.is_mesh());
assert!(!FileKind::Ttf.is_mesh());
}
#[test]
fn file_args_and_kind_round_trip_through_json() {
let args = FileArgs {
path: "x.png".into(),
kind: Some(FileKind::Png),
};
let value = serde_json::to_value(&args).unwrap();
let back: FileArgs = serde_json::from_value(value).unwrap();
assert_eq!(back.path, "x.png");
assert_eq!(back.kind, Some(FileKind::Png));
assert_eq!(serde_json::to_string(&FileKind::Jpeg).unwrap(), "\"jpeg\"");
assert_eq!(File::bake(args).kind, Some(FileKind::Png));
}
}