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