1use alloc::string::String;
4
5#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum FileKind {
9 Obj,
11 Png,
13 Jpg,
15 Jpeg,
17 Bmp,
19 Tga,
21 Gif,
23 Ttf,
25 Otf,
27 Txt,
29 Md,
31 Mtl,
33}
34
35impl FileKind {
36 pub fn from_ext(ext: &str) -> Option<Self> {
39 match ext.to_lowercase().as_str() {
40 "obj" => Some(Self::Obj),
41 "png" => Some(Self::Png),
42 "jpg" => Some(Self::Jpg),
43 "jpeg" => Some(Self::Jpeg),
44 "bmp" => Some(Self::Bmp),
45 "tga" => Some(Self::Tga),
46 "gif" => Some(Self::Gif),
47 "ttf" => Some(Self::Ttf),
48 "otf" => Some(Self::Otf),
49 "txt" => Some(Self::Txt),
50 "md" => Some(Self::Md),
51 "mtl" => Some(Self::Mtl),
52 _ => None,
53 }
54 }
55
56 pub fn is_mesh(&self) -> bool {
59 matches!(self, Self::Obj)
60 }
61}
62
63#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
65pub struct FileArgs {
66 pub path: String,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub kind: Option<FileKind>,
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn every_kind_is_reachable_from_an_extension() {
79 let all = [
82 ("obj", FileKind::Obj),
83 ("png", FileKind::Png),
84 ("jpg", FileKind::Jpg),
85 ("jpeg", FileKind::Jpeg),
86 ("bmp", FileKind::Bmp),
87 ("tga", FileKind::Tga),
88 ("gif", FileKind::Gif),
89 ("ttf", FileKind::Ttf),
90 ("otf", FileKind::Otf),
91 ("txt", FileKind::Txt),
92 ("md", FileKind::Md),
93 ("mtl", FileKind::Mtl),
94 ];
95 for (ext, kind) in all {
96 assert_eq!(FileKind::from_ext(ext).as_ref(), Some(&kind), "{ext}");
97 assert_eq!(
99 FileKind::from_ext(&ext.to_uppercase()).as_ref(),
100 Some(&kind),
101 "{ext}"
102 );
103 }
104 }
105
106 #[test]
107 fn an_unknown_extension_has_no_kind() {
108 assert_eq!(FileKind::from_ext("wav"), None);
109 assert_eq!(FileKind::from_ext(""), None);
110 }
111
112 #[test]
113 fn only_obj_builds_to_a_mesh_payload() {
114 assert!(FileKind::Obj.is_mesh());
115 for kind in [FileKind::Png, FileKind::Ttf, FileKind::Mtl, FileKind::Md] {
116 assert!(!kind.is_mesh(), "{kind:?}");
117 }
118 }
119
120 #[test]
121 fn args_default_to_an_empty_path_and_inferred_kind() {
122 let args = FileArgs::default();
123 assert!(args.path.is_empty());
124 assert_eq!(args.kind, None);
125 }
126
127 #[test]
128 fn an_absent_kind_is_omitted_from_the_serialized_args() {
129 let args: FileArgs = serde_json::from_str(r#"{"path":"assets/board.obj"}"#).unwrap();
130 assert_eq!(args.path, "assets/board.obj");
131 assert_eq!(args.kind, None);
132 assert_eq!(
135 serde_json::to_string(&args).unwrap(),
136 r#"{"path":"assets/board.obj"}"#
137 );
138 }
139
140 #[test]
141 fn an_explicit_kind_round_trips_through_its_lowercase_name() {
142 let args: FileArgs = serde_json::from_str(r#"{"path":"font.dat","kind":"ttf"}"#).unwrap();
143 assert_eq!(args.kind, Some(FileKind::Ttf));
144 assert_eq!(
145 serde_json::to_string(&args).unwrap(),
146 r#"{"path":"font.dat","kind":"ttf"}"#
147 );
148 let bytes = postcard::to_allocvec(&args).unwrap();
149 let back: FileArgs = postcard::from_bytes(&bytes).unwrap();
150 assert_eq!(back.kind, Some(FileKind::Ttf));
151 assert_eq!(back.path, "font.dat");
152 }
153}