use serde::{Deserialize, Serialize};
use super::{FontAsset, ImageAsset};
use crate::DocumentId;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AssetIndex<T> {
pub version: String,
pub count: u32,
pub total_size: u64,
pub assets: Vec<T>,
}
impl<T> AssetIndex<T> {
#[must_use]
pub fn new() -> Self {
Self {
version: crate::SPEC_VERSION.to_string(),
count: 0,
total_size: 0,
assets: Vec::new(),
}
}
pub fn add(&mut self, asset: T, size: u64) {
self.assets.push(asset);
self.count += 1;
self.total_size += size;
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.assets.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.assets.len()
}
}
impl<T> Default for AssetIndex<T> {
fn default() -> Self {
Self::new()
}
}
pub type ImageIndex = AssetIndex<ImageAsset>;
pub type FontIndex = AssetIndex<FontAsset>;
pub type EmbedIndex = AssetIndex<EmbedAsset>;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EmbedAsset {
pub id: String,
pub path: String,
pub hash: DocumentId,
pub size: u64,
pub mime_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub inline: bool,
}
impl EmbedAsset {
#[must_use]
pub fn new(id: impl Into<String>, mime_type: impl Into<String>) -> Self {
let id = id.into();
let path = format!("assets/embeds/{id}");
Self {
id,
path,
hash: DocumentId::pending(),
size: 0,
mime_type: mime_type.into(),
filename: None,
description: None,
inline: false,
}
}
#[must_use]
pub fn with_hash(mut self, hash: DocumentId) -> Self {
self.hash = hash;
self
}
#[must_use]
pub const fn with_size(mut self, size: u64) -> Self {
self.size = size;
self
}
#[must_use]
pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
self.filename = Some(filename.into());
self
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub const fn with_inline(mut self, inline: bool) -> Self {
self.inline = inline;
self
}
#[must_use]
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.path = path.into();
self
}
}
impl super::Asset for EmbedAsset {
fn id(&self) -> &str {
&self.id
}
fn path(&self) -> &str {
&self.path
}
fn hash(&self) -> &DocumentId {
&self.hash
}
fn size(&self) -> u64 {
self.size
}
fn mime_type(&self) -> &str {
&self.mime_type
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum AssetEntry {
Image(ImageAsset),
Font(FontAsset),
Embed(EmbedAsset),
Alias(AssetAlias),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AssetAlias {
pub id: String,
pub alias_of: String,
pub hash: DocumentId,
}
impl AssetEntry {
#[must_use]
pub fn id(&self) -> &str {
match self {
Self::Image(a) => &a.id,
Self::Font(a) => &a.id,
Self::Embed(a) => &a.id,
Self::Alias(a) => &a.id,
}
}
#[must_use]
pub fn path(&self) -> &str {
match self {
Self::Image(a) => &a.path,
Self::Font(a) => &a.path,
Self::Embed(a) => &a.path,
Self::Alias(_) => "",
}
}
#[must_use]
pub fn hash(&self) -> &DocumentId {
match self {
Self::Image(a) => &a.hash,
Self::Font(a) => &a.hash,
Self::Embed(a) => &a.hash,
Self::Alias(a) => &a.hash,
}
}
#[must_use]
pub fn size(&self) -> u64 {
match self {
Self::Image(a) => a.size,
Self::Font(a) => a.size,
Self::Embed(a) => a.size,
Self::Alias(_) => 0,
}
}
#[must_use]
pub fn is_alias(&self) -> bool {
matches!(self, Self::Alias(_))
}
#[must_use]
pub fn alias_of(&self) -> Option<&str> {
match self {
Self::Alias(a) => Some(&a.alias_of),
_ => None,
}
}
}
impl AssetAlias {
#[must_use]
pub fn new(id: impl Into<String>, alias_of: impl Into<String>, hash: DocumentId) -> Self {
Self {
id: id.into(),
alias_of: alias_of.into(),
hash,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::asset::ImageFormat;
#[test]
fn test_asset_index_new() {
let index: ImageIndex = AssetIndex::new();
assert!(index.is_empty());
assert_eq!(index.count, 0);
assert_eq!(index.total_size, 0);
}
#[test]
fn test_asset_index_add() {
let mut index: ImageIndex = AssetIndex::new();
let image = ImageAsset::new("test", ImageFormat::Png).with_size(1024);
index.add(image, 1024);
assert_eq!(index.len(), 1);
assert_eq!(index.count, 1);
assert_eq!(index.total_size, 1024);
}
#[test]
fn test_embed_asset_new() {
let embed = EmbedAsset::new("data", "text/csv");
assert_eq!(embed.id, "data");
assert_eq!(embed.mime_type, "text/csv");
assert_eq!(embed.path, "assets/embeds/data");
}
#[test]
fn test_embed_asset_builder() {
let embed = EmbedAsset::new("spreadsheet", "application/vnd.ms-excel")
.with_filename("sales.xlsx")
.with_description("Quarterly sales data")
.with_size(65536)
.with_inline(false);
assert_eq!(embed.filename, Some("sales.xlsx".to_string()));
assert_eq!(embed.description, Some("Quarterly sales data".to_string()));
assert_eq!(embed.size, 65536);
assert!(!embed.inline);
}
#[test]
fn test_asset_entry_variants() {
let image = AssetEntry::Image(ImageAsset::new("img", ImageFormat::Png));
assert_eq!(image.id(), "img");
let embed = AssetEntry::Embed(EmbedAsset::new("file", "text/plain"));
assert_eq!(embed.id(), "file");
}
#[test]
fn test_asset_index_serialization() {
let mut index: ImageIndex = AssetIndex::new();
let image = ImageAsset::new("test", ImageFormat::Png).with_size(1024);
index.add(image, 1024);
let json = serde_json::to_string_pretty(&index).unwrap();
assert!(json.contains(r#""count": 1"#));
assert!(json.contains(r#""totalSize": 1024"#));
let deserialized: ImageIndex = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.count, 1);
assert_eq!(deserialized.total_size, 1024);
}
#[test]
fn test_asset_alias_creation() {
let hash: DocumentId =
"sha256:abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
.parse()
.unwrap();
let alias = AssetAlias::new("duplicate-logo", "original-logo", hash.clone());
assert_eq!(alias.id, "duplicate-logo");
assert_eq!(alias.alias_of, "original-logo");
assert_eq!(alias.hash, hash);
}
#[test]
fn test_asset_entry_alias() {
let hash: DocumentId =
"sha256:abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
.parse()
.unwrap();
let alias = AssetEntry::Alias(AssetAlias::new("dup", "orig", hash));
assert!(alias.is_alias());
assert_eq!(alias.alias_of(), Some("orig"));
assert_eq!(alias.id(), "dup");
assert_eq!(alias.size(), 0); assert_eq!(alias.path(), ""); }
#[test]
fn test_asset_entry_not_alias() {
let image = AssetEntry::Image(ImageAsset::new("img", ImageFormat::Png));
assert!(!image.is_alias());
assert_eq!(image.alias_of(), None);
}
#[test]
fn test_asset_alias_serialization() {
let hash: DocumentId =
"sha256:abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
.parse()
.unwrap();
let alias = AssetEntry::Alias(AssetAlias::new("dup", "orig", hash));
let json = serde_json::to_string_pretty(&alias).unwrap();
assert!(json.contains(r#""type": "alias""#));
assert!(json.contains(r#""aliasOf": "orig""#));
let deserialized: AssetEntry = serde_json::from_str(&json).unwrap();
assert!(deserialized.is_alias());
assert_eq!(deserialized.alias_of(), Some("orig"));
}
}