use std::collections::{BTreeSet, HashMap};
use std::path::PathBuf;
use std::sync::Arc;
use rust_embed::RustEmbed;
use serde::de::DeserializeOwned;
use crate::error::AssetError;
use crate::source::{AssetSource, DirectorySource, EmbeddedSource, MemorySource};
#[derive(Clone, Default)]
pub struct Assets {
layers: Arc<[Arc<dyn AssetSource>]>,
}
impl Assets {
pub fn builder() -> AssetsBuilder {
AssetsBuilder::default()
}
#[must_use]
pub fn open(&self, path: &str) -> Option<Vec<u8>> {
for layer in self.layers.iter().rev() {
if let Some(bytes) = layer.read(path) {
return Some(bytes);
}
}
None
}
pub fn open_text(&self, path: &str) -> Result<String, AssetError> {
let bytes = self.open(path).ok_or_else(|| AssetError::NotFound(path.to_string()))?;
String::from_utf8(bytes).map_err(|_| AssetError::NotUtf8 { path: path.to_string() })
}
#[must_use]
pub fn exists(&self, path: &str) -> bool {
self.layers.iter().any(|l| l.read(path).is_some())
}
#[must_use]
pub fn list_dir(&self, dir: &str) -> Vec<String> {
let mut all = BTreeSet::new();
for layer in self.layers.iter() {
for entry in layer.list(dir) {
all.insert(entry);
}
}
all.into_iter().collect()
}
pub fn load_merged_yaml<T: DeserializeOwned>(&self, path: &str) -> Result<T, AssetError> {
self.load_merged(path, "YAML", |bytes, source_name| {
let s = std::str::from_utf8(bytes).map_err(|e| AssetError::Parse {
path: format!("{path} (layer `{source_name}`)"),
format: "YAML",
message: e.to_string(),
})?;
let yaml_value: serde_yaml::Value =
serde_yaml::from_str(s).map_err(|e| AssetError::Parse {
path: format!("{path} (layer `{source_name}`)"),
format: "YAML",
message: e.to_string(),
})?;
serde_json::to_value(yaml_value).map_err(|e| AssetError::Parse {
path: format!("{path} (layer `{source_name}`)"),
format: "YAML",
message: e.to_string(),
})
})
}
pub fn load_merged_json<T: DeserializeOwned>(&self, path: &str) -> Result<T, AssetError> {
self.load_merged(path, "JSON", |bytes, source_name| {
serde_json::from_slice::<serde_json::Value>(bytes).map_err(|e| AssetError::Parse {
path: format!("{path} (layer `{source_name}`)"),
format: "JSON",
message: e.to_string(),
})
})
}
fn load_merged<T, F>(&self, path: &str, format: &'static str, parse: F) -> Result<T, AssetError>
where
T: DeserializeOwned,
F: Fn(&[u8], &str) -> Result<serde_json::Value, AssetError>,
{
let mut merged: Option<serde_json::Value> = None;
for layer in self.layers.iter() {
let Some(bytes) = layer.read(path) else { continue };
let parsed = parse(&bytes, layer.name())?;
merged = Some(match merged {
None => parsed,
Some(mut acc) => {
json_patch::merge(&mut acc, &parsed);
acc
}
});
}
let merged = merged.ok_or_else(|| AssetError::NotFound(path.to_string()))?;
serde_json::from_value::<T>(merged).map_err(|e| AssetError::Parse {
path: path.to_string(),
format,
message: e.to_string(),
})
}
}
#[derive(Default)]
#[must_use]
pub struct AssetsBuilder {
layers: Vec<Arc<dyn AssetSource>>,
}
impl std::fmt::Debug for AssetsBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let names: Vec<&str> = self.layers.iter().map(|l| l.name()).collect();
f.debug_struct("AssetsBuilder").field("layers", &names).finish()
}
}
impl std::fmt::Debug for Assets {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let names: Vec<&str> = self.layers.iter().map(|l| l.name()).collect();
f.debug_struct("Assets").field("layers", &names).finish()
}
}
impl AssetsBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn embedded<E>(mut self, label: &'static str) -> Self
where
E: RustEmbed + Send + Sync + 'static,
{
self.layers.push(Arc::new(EmbeddedSource::<E>::new(label)));
self
}
pub fn directory(mut self, root: impl Into<PathBuf>, label: impl Into<String>) -> Self {
self.layers.push(Arc::new(DirectorySource::new(root, label)));
self
}
pub fn memory(mut self, label: impl Into<String>, files: HashMap<String, Vec<u8>>) -> Self {
self.layers.push(Arc::new(MemorySource::new(label, files)));
self
}
pub fn source(mut self, source: Arc<dyn AssetSource>) -> Self {
self.layers.push(source);
self
}
#[must_use]
pub fn build(self) -> Assets {
Assets { layers: self.layers.into() }
}
}