use std::collections::{BTreeMap, HashSet};
use anyhow::Result;
use bytes::Bytes;
use futures::future::try_join_all;
use crate::AssetRef;
use crate::Target;
use crate::assets::AssetProvider;
use crate::images::{collect_images, sanitize_key};
use crate::pages::Page;
type VirtualFileSet = (BTreeMap<String, String>, Vec<(String, Vec<u8>)>);
pub(super) async fn fetch_deduped(
refs: &[AssetRef],
provider: &dyn AssetProvider,
) -> Result<Vec<(String, Option<Bytes>)>> {
let mut seen = HashSet::new();
let keys: Vec<&str> = refs
.iter()
.map(|r| r.key.as_str())
.filter(|key| seen.insert(*key))
.collect();
try_join_all(keys.into_iter().map(|key| async move {
let bytes = provider.get(key).await?;
Ok::<(String, Option<Bytes>), anyhow::Error>((key.to_string(), bytes))
}))
.await
}
fn register_virtual_files(
fetched: Vec<(String, Option<Bytes>)>,
prefix: &str,
warn_missing: bool,
) -> VirtualFileSet {
let mut map = BTreeMap::new();
let mut files = Vec::new();
for (key, bytes) in fetched {
match bytes {
Some(b) => {
let vpath = format!("{prefix}/{}", sanitize_key(&key));
map.insert(key, format!("/{vpath}"));
files.push((vpath, b.to_vec()));
}
None if warn_missing => {
tracing::warn!(key, "layout asset not found in provider; skipping");
}
None => {}
}
}
(map, files)
}
pub(super) async fn collect_images_for_typst(
pages: &[Page],
provider: &dyn AssetProvider,
target: Target,
) -> Result<VirtualFileSet> {
let fetched = collect_images(pages, provider, target).await?;
let fetched = fetched.into_iter().map(|(k, b)| (k, Some(b))).collect();
Ok(register_virtual_files(fetched, "images", false))
}
pub(super) async fn collect_layout_assets(
refs: &[AssetRef],
provider: &dyn AssetProvider,
) -> Result<VirtualFileSet> {
let fetched = fetch_deduped(refs, provider).await?;
Ok(register_virtual_files(fetched, "assets", true))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assets::sync_provider;
#[tokio::test]
async fn collect_layout_assets_registers_found_keys_under_stable_vpath() {
let provider = sync_provider(|key| match key {
"branding/logo.svg" => Ok(Some(Bytes::from_static(b"<svg/>"))),
_ => Ok(None),
});
let refs = vec![AssetRef {
key: "branding/logo.svg".to_string(),
}];
let (map, files) = collect_layout_assets(&refs, &provider).await.unwrap();
assert_eq!(
map.get("branding/logo.svg"),
Some(&"/assets/branding__logo.svg".to_string())
);
assert_eq!(
files,
vec![("assets/branding__logo.svg".to_string(), b"<svg/>".to_vec())]
);
}
#[tokio::test]
async fn collect_layout_assets_skips_missing_keys_without_erroring() {
let provider = sync_provider(|_| Ok(None));
let refs = vec![AssetRef {
key: "does/not-exist.png".to_string(),
}];
let (map, files) = collect_layout_assets(&refs, &provider).await.unwrap();
assert!(map.is_empty());
assert!(files.is_empty());
}
#[tokio::test]
async fn collect_layout_assets_dedups_repeated_keys() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
let calls = Arc::new(AtomicUsize::new(0));
let calls_c = calls.clone();
let provider = sync_provider(move |key| {
calls_c.fetch_add(1, Ordering::SeqCst);
match key {
"logo.svg" => Ok(Some(Bytes::from_static(b"<svg/>"))),
_ => Ok(None),
}
});
let refs = vec![
AssetRef {
key: "logo.svg".to_string(),
},
AssetRef {
key: "logo.svg".to_string(),
},
];
let (map, files) = collect_layout_assets(&refs, &provider).await.unwrap();
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"expected a single fetch for a repeated key"
);
assert_eq!(map.len(), 1);
assert_eq!(files.len(), 1);
}
}