pub mod animation;
pub mod curve;
pub mod midi_controller;
pub mod output_device;
pub mod palette;
pub mod picture;
pub mod project;
pub mod scene;
use crate::asset_id::AssetId;
use crate::collection::Collections;
use crate::naming::unique_name;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::{fmt::Debug, io::Read, sync::Arc};
pub const ASSET_VERSION: u32 = 2;
pub trait AssetTrait:
Serialize + DeserializeOwned + Debug + Default + Send + Sync + Clone + 'static + PartialEq
{
const DIR_NAME: &'static str;
const NAME: &'static str;
const SHOW_NAME_IF_SELECTED: bool;
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(bound = "T: DeserializeOwned + Serialize")]
struct AssetOnDisk<T: AssetTrait> {
name: Vec<String>,
#[serde(default)]
version: u32,
#[serde(flatten)]
data: T,
}
#[derive(Debug, thiserror::Error)]
pub enum ReadError {
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error("written by a newer gled (asset version {version}, this gled reads up to {supported})")]
Newer { version: u32, supported: u32 },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(bound = "T: DeserializeOwned + Serialize")]
pub struct Asset<T: AssetTrait> {
pub id: AssetId<T>,
pub path: Vec<String>,
pub data: T,
}
impl<T: AssetTrait> Default for Asset<T> {
fn default() -> Self {
Self::new(vec![format!("New {}", T::NAME)])
}
}
impl<T: AssetTrait> Asset<T> {
pub fn new(path: Vec<String>) -> Self {
Self {
id: AssetId::<T>::new(),
path,
data: T::default(),
}
}
pub fn copy(&self, collections: &Collections) -> Self {
let mut path = self.path.clone();
path.pop();
let all = Self::all(collections);
let siblings = all
.iter()
.filter_map(|asset| asset.path.split_last())
.filter(|(_, folder)| *folder == path.as_slice())
.map(|(name, _)| name.as_str());
path.push(unique_name(&format!("{} copy", self.name()), siblings));
Self {
id: AssetId::<T>::new(),
path,
data: self.data.clone(),
}
}
pub fn get(id: AssetId<T>, collections: &Collections) -> Option<Arc<Self>> {
collections.get::<T>()?.get(&id).cloned()
}
pub fn get_asset_from_index(index: usize, collections: &Collections) -> Option<Arc<Self>> {
match index {
0 => None,
index => Self::all(collections).get(index - 1).cloned(),
}
}
pub fn all(collections: &Collections) -> Vec<Arc<Asset<T>>> {
let Some(collections) = collections.get::<T>() else {
return vec![];
};
collections.assets()
}
pub fn read(reader: impl Read, id: AssetId<T>) -> Result<Arc<Self>, ReadError> {
let asset: AssetOnDisk<T> = serde_json::from_reader(reader)?;
if asset.version > ASSET_VERSION {
return Err(ReadError::Newer {
version: asset.version,
supported: ASSET_VERSION,
});
}
let mut path = asset.name;
if path.is_empty() {
tracing::warn!(
"The {} {} has no name; it is named after its id",
T::NAME,
id.id
);
path.push(id.id.to_string());
}
Ok(Arc::new(Self {
id,
path,
data: asset.data,
}))
}
pub fn into_json(self) -> Result<String, serde_json::Error> {
let asset = AssetOnDisk {
name: self.path,
version: ASSET_VERSION,
data: self.data,
};
serde_json::to_string_pretty(&asset)
}
pub fn dir(&self) -> Vec<String> {
self.path
.split_last()
.map(|(_, dir)| dir.to_vec())
.unwrap_or_default()
}
pub fn name(&self) -> &str {
self.path
.last()
.map(|name| name.as_str())
.unwrap_or("No name")
}
pub fn change_dir(&mut self, new_dir: &[String]) {
let name = self.path.pop();
self.path = new_dir.to_owned();
if let Some(name) = name {
self.path.push(name);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::asset::palette::Palette;
fn palette_file(version: Option<u32>) -> String {
let version = version
.map(|v| format!(r#""version": {v},"#))
.unwrap_or_default();
format!(r#"{{"name": ["Warm"], {version} "primary": {{"rgb": [1.0, 0.5, 0.0]}}}}"#)
}
#[test]
fn a_file_from_before_versions_reads_and_saves_with_the_current_version() {
let asset =
Asset::<Palette>::read(palette_file(None).as_bytes(), AssetId::new()).expect("reads");
let saved: serde_json::Value =
serde_json::from_str(&(*asset).clone().into_json().expect("saves")).expect("json");
assert_eq!(saved["version"], ASSET_VERSION);
assert_eq!(saved["name"], serde_json::json!(["Warm"]));
}
#[test]
fn a_file_from_a_newer_gled_is_refused() {
let newer = palette_file(Some(ASSET_VERSION + 1));
assert!(matches!(
Asset::<Palette>::read(newer.as_bytes(), AssetId::new()),
Err(ReadError::Newer { .. })
));
}
#[test]
fn a_file_without_a_name_is_named_after_its_id() {
let file = r#"{"name": [], "primary": {"rgb": [1.0, 0.5, 0.0]}}"#;
let id = AssetId::new();
let asset = Asset::<Palette>::read(file.as_bytes(), id).expect("reads");
assert_eq!(asset.path, [id.id.to_string()]);
assert_eq!(asset.name(), id.id.to_string());
assert!(asset.dir().is_empty());
}
#[test]
fn an_asset_without_a_path_is_in_the_top_folder_and_copies() {
let mut collections = Collections::default();
let nameless = Asset::<Palette>::new(vec![]);
collections.get_mut::<Palette>().set_asset(nameless.clone());
assert!(nameless.dir().is_empty());
assert_eq!(nameless.name(), "No name");
assert_eq!(nameless.copy(&collections).path, ["No name copy"]);
}
fn names(collections: &Collections) -> Vec<Vec<String>> {
let mut names: Vec<_> = Asset::<Palette>::all(collections)
.iter()
.map(|asset| asset.path.clone())
.collect();
names.sort();
names
}
#[test]
fn a_copy_gets_a_name_no_other_asset_in_its_folder_has() {
let mut collections = Collections::default();
let original = Asset::<Palette>::new(vec!["Show".into(), "Warm".into()]);
collections.get_mut::<Palette>().set_asset(original.clone());
let first = original.copy(&collections);
collections.get_mut::<Palette>().set_asset(first.clone());
let second = original.copy(&collections);
collections.get_mut::<Palette>().set_asset(second.clone());
assert_ne!(first.id, original.id);
assert_eq!(first.data, original.data);
assert_eq!(
names(&collections),
[
vec!["Show".to_owned(), "Warm".to_owned()],
vec!["Show".to_owned(), "Warm copy".to_owned()],
vec!["Show".to_owned(), "Warm copy 2".to_owned()],
]
);
}
}