1pub mod animation;
2pub mod curve;
3pub mod midi_controller;
4pub mod output_device;
5pub mod palette;
6pub mod picture;
7pub mod project;
8pub mod scene;
9
10use crate::asset_id::AssetId;
11use crate::collection::Collections;
12use crate::naming::unique_name;
13use serde::{Deserialize, Serialize, de::DeserializeOwned};
14use std::{fmt::Debug, io::Read, sync::Arc};
15
16pub const ASSET_VERSION: u32 = 2;
26
27pub trait AssetTrait:
28 Serialize + DeserializeOwned + Debug + Default + Send + Sync + Clone + 'static + PartialEq
29{
30 const DIR_NAME: &'static str;
31 const NAME: &'static str;
32 const SHOW_NAME_IF_SELECTED: bool;
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(bound = "T: DeserializeOwned + Serialize")]
37struct AssetOnDisk<T: AssetTrait> {
38 name: Vec<String>,
39 #[serde(default)]
42 version: u32,
43 #[serde(flatten)]
44 data: T,
45}
46
47#[derive(Debug, thiserror::Error)]
49pub enum ReadError {
50 #[error(transparent)]
51 Json(#[from] serde_json::Error),
52 #[error("written by a newer gled (asset version {version}, this gled reads up to {supported})")]
53 Newer { version: u32, supported: u32 },
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(bound = "T: DeserializeOwned + Serialize")]
60pub struct Asset<T: AssetTrait> {
61 pub id: AssetId<T>,
62 pub path: Vec<String>,
63 pub data: T,
64}
65
66impl<T: AssetTrait> Default for Asset<T> {
67 fn default() -> Self {
68 Self::new(vec![format!("New {}", T::NAME)])
69 }
70}
71
72impl<T: AssetTrait> Asset<T> {
73 pub fn new(path: Vec<String>) -> Self {
74 Self {
75 id: AssetId::<T>::new(),
76 path,
77 data: T::default(),
78 }
79 }
80
81 pub fn copy(&self, collections: &Collections) -> Self {
85 let mut path = self.path.clone();
86 path.pop();
87 let all = Self::all(collections);
88 let siblings = all
89 .iter()
90 .filter_map(|asset| asset.path.split_last())
91 .filter(|(_, folder)| *folder == path.as_slice())
92 .map(|(name, _)| name.as_str());
93 path.push(unique_name(&format!("{} copy", self.name()), siblings));
94
95 Self {
96 id: AssetId::<T>::new(),
97 path,
98 data: self.data.clone(),
99 }
100 }
101
102 pub fn get(id: AssetId<T>, collections: &Collections) -> Option<Arc<Self>> {
103 collections.get::<T>()?.get(&id).cloned()
104 }
105
106 pub fn get_asset_from_index(index: usize, collections: &Collections) -> Option<Arc<Self>> {
107 match index {
108 0 => None,
109 index => Self::all(collections).get(index - 1).cloned(),
110 }
111 }
112
113 pub fn all(collections: &Collections) -> Vec<Arc<Asset<T>>> {
114 let Some(collections) = collections.get::<T>() else {
115 return vec![];
116 };
117
118 collections.assets()
119 }
120
121 pub fn read(reader: impl Read, id: AssetId<T>) -> Result<Arc<Self>, ReadError> {
129 let asset: AssetOnDisk<T> = serde_json::from_reader(reader)?;
130 if asset.version > ASSET_VERSION {
131 return Err(ReadError::Newer {
132 version: asset.version,
133 supported: ASSET_VERSION,
134 });
135 }
136
137 let mut path = asset.name;
138 if path.is_empty() {
139 tracing::warn!(
140 "The {} {} has no name; it is named after its id",
141 T::NAME,
142 id.id
143 );
144 path.push(id.id.to_string());
145 }
146
147 Ok(Arc::new(Self {
148 id,
149 path,
150 data: asset.data,
151 }))
152 }
153
154 pub fn into_json(self) -> Result<String, serde_json::Error> {
155 let asset = AssetOnDisk {
156 name: self.path,
157 version: ASSET_VERSION,
158 data: self.data,
159 };
160
161 serde_json::to_string_pretty(&asset)
162 }
163
164 pub fn dir(&self) -> Vec<String> {
168 self.path
169 .split_last()
170 .map(|(_, dir)| dir.to_vec())
171 .unwrap_or_default()
172 }
173
174 pub fn name(&self) -> &str {
175 self.path
176 .last()
177 .map(|name| name.as_str())
178 .unwrap_or("No name")
179 }
180
181 pub fn change_dir(&mut self, new_dir: &[String]) {
182 let name = self.path.pop();
183 self.path = new_dir.to_owned();
184 if let Some(name) = name {
185 self.path.push(name);
186 }
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::asset::palette::Palette;
194
195 fn palette_file(version: Option<u32>) -> String {
196 let version = version
197 .map(|v| format!(r#""version": {v},"#))
198 .unwrap_or_default();
199 format!(r#"{{"name": ["Warm"], {version} "primary": {{"rgb": [1.0, 0.5, 0.0]}}}}"#)
200 }
201
202 #[test]
203 fn a_file_from_before_versions_reads_and_saves_with_the_current_version() {
204 let asset =
205 Asset::<Palette>::read(palette_file(None).as_bytes(), AssetId::new()).expect("reads");
206 let saved: serde_json::Value =
207 serde_json::from_str(&(*asset).clone().into_json().expect("saves")).expect("json");
208 assert_eq!(saved["version"], ASSET_VERSION);
209 assert_eq!(saved["name"], serde_json::json!(["Warm"]));
210 }
211
212 #[test]
213 fn a_file_from_a_newer_gled_is_refused() {
214 let newer = palette_file(Some(ASSET_VERSION + 1));
215 assert!(matches!(
216 Asset::<Palette>::read(newer.as_bytes(), AssetId::new()),
217 Err(ReadError::Newer { .. })
218 ));
219 }
220
221 #[test]
222 fn a_file_without_a_name_is_named_after_its_id() {
223 let file = r#"{"name": [], "primary": {"rgb": [1.0, 0.5, 0.0]}}"#;
224 let id = AssetId::new();
225 let asset = Asset::<Palette>::read(file.as_bytes(), id).expect("reads");
226
227 assert_eq!(asset.path, [id.id.to_string()]);
228 assert_eq!(asset.name(), id.id.to_string());
229 assert!(asset.dir().is_empty());
230 }
231
232 #[test]
233 fn an_asset_without_a_path_is_in_the_top_folder_and_copies() {
234 let mut collections = Collections::default();
235 let nameless = Asset::<Palette>::new(vec![]);
236 collections.get_mut::<Palette>().set_asset(nameless.clone());
237
238 assert!(nameless.dir().is_empty());
239 assert_eq!(nameless.name(), "No name");
240 assert_eq!(nameless.copy(&collections).path, ["No name copy"]);
241 }
242
243 fn names(collections: &Collections) -> Vec<Vec<String>> {
244 let mut names: Vec<_> = Asset::<Palette>::all(collections)
245 .iter()
246 .map(|asset| asset.path.clone())
247 .collect();
248 names.sort();
249 names
250 }
251
252 #[test]
253 fn a_copy_gets_a_name_no_other_asset_in_its_folder_has() {
254 let mut collections = Collections::default();
255 let original = Asset::<Palette>::new(vec!["Show".into(), "Warm".into()]);
256 collections.get_mut::<Palette>().set_asset(original.clone());
257
258 let first = original.copy(&collections);
259 collections.get_mut::<Palette>().set_asset(first.clone());
260 let second = original.copy(&collections);
261 collections.get_mut::<Palette>().set_asset(second.clone());
262
263 assert_ne!(first.id, original.id);
264 assert_eq!(first.data, original.data);
265 assert_eq!(
266 names(&collections),
267 [
268 vec!["Show".to_owned(), "Warm".to_owned()],
269 vec!["Show".to_owned(), "Warm copy".to_owned()],
270 vec!["Show".to_owned(), "Warm copy 2".to_owned()],
271 ]
272 );
273 }
274}