use ahash::AHashMap;
use bytes::Bytes;
use std::path::Path;
use tokio::io::AsyncReadExt;
use reinda_core::{
AssetDef, AssetId,
template::{Fragment, Template},
};
use crate::{
Config, Error, Setup,
hash,
dep_graph::DepGraph,
};
#[derive(Debug)]
pub(crate) struct Resolver {
resolved: AHashMap<AssetId, Bytes>,
unresolved: AHashMap<AssetId, Template>,
graph: DepGraph,
}
impl Resolver {
pub(crate) fn new() -> Self {
Self {
resolved: AHashMap::new(),
unresolved: AHashMap::new(),
graph: DepGraph::new(),
}
}
#[cfg(any(not(debug_assertions), feature = "debug-is-prod"))]
pub(crate) async fn for_all_assets(setup: &Setup, config: &Config) -> Result<Resolver, Error> {
let mut resolver = Resolver::new();
for (id, asset_def) in setup.assets.iter().enumerate() {
let id = AssetId(id as u32);
resolver.graph.add_asset(id);
let raw_bytes = if asset_def.dynamic {
load_raw_from_fs(id, setup, config).await?
} else {
load_from_static(asset_def.content)
};
resolver.add_raw(raw_bytes, id, setup)?;
}
Ok(resolver)
}
#[cfg(all(debug_assertions, not(feature = "debug-is-prod")))]
pub(crate) async fn for_single_asset_from_fs(
asset_id: AssetId,
setup: &Setup,
config: &Config,
) -> Result<Resolver, Error> {
let mut resolver = Resolver::new();
resolver.graph.add_asset(asset_id);
let mut stack = vec![asset_id];
while let Some(id) = stack.pop() {
if resolver.resolved.contains_key(&id) || resolver.unresolved.contains_key(&id) {
continue;
}
let raw_bytes = load_raw_from_fs(id, setup, config).await?;
resolver.add_raw(raw_bytes, id, &setup)?;
stack.extend(resolver.graph.dependencies_of(id));
}
Ok(resolver)
}
pub(crate) fn resolve<'a>(
self,
setup: &Setup,
config: &Config,
) -> Result<ResolveResult, Error> {
let Resolver { mut resolved, mut unresolved, graph } = self;
let mut public_paths: AHashMap<AssetId, String> = AHashMap::new();
let assets = graph.topological_sort().map_err(|cycle| {
let cycle = cycle.into_iter().map(|id| setup.def(id).path.to_string()).collect();
Error::CyclicInclude(cycle)
})?;
for id in assets {
let def = setup.def(id);
if let Some(template) = unresolved.remove(&id) {
let path = def.path;
let rendered = template.render(|fragment, mut appender| -> Result<_, Error> {
match fragment {
Fragment::Include(p) => {
let id = setup.path_to_id(&p).unwrap();
let data = resolved.get(&id)
.expect("missing include in `Assets::resolve`");
appender.append(&data);
}
Fragment::Path(p) => {
let reference_id = setup.path_to_id(&p)
.expect("missing path reference in `Assets::resolve`");
let public_path = public_paths.get(&reference_id)
.map(|s| &**s)
.unwrap_or(setup.def(reference_id).path);
appender.append(public_path.as_bytes());
}
Fragment::Var(key) => {
let value = config.variables.get(&key)
.ok_or_else(|| Error::MissingVariable {
key: key.into(),
file: path.into(),
})?;
appender.append(value.as_bytes());
}
}
Ok(())
})?;
resolved.insert(id, rendered);
}
if def.hashed_filename() {
let hashed = hash::hashed_path_of(def, &resolved[&id]);
public_paths.insert(id, hashed);
}
}
Ok(ResolveResult {
assets: resolved,
public_paths,
})
}
fn add_raw(
&mut self,
raw_bytes: Bytes,
asset_id: AssetId,
setup: &Setup,
) -> Result<(), Error> {
let raw_asset = RawAsset::new(raw_bytes, &setup.def(asset_id))?;
match raw_asset.into_already_rendered() {
Ok(resolved) => {
self.resolved.insert(asset_id, resolved);
}
Err(template) => {
for fragment in template.fragments() {
let dep = dependency_in_fragment(fragment, asset_id, setup)?;
if let Some(dep) = dep {
self.graph.add_dependency(asset_id, dep);
}
}
self.unresolved.insert(asset_id, template);
}
}
Ok(())
}
}
pub(crate) struct ResolveResult {
pub(crate) assets: AHashMap<AssetId, Bytes>,
#[cfg_attr(debug_assertions, allow(dead_code))]
pub(crate) public_paths: AHashMap<AssetId, String>,
}
fn dependency_in_fragment(
fragment: &Fragment,
parent: AssetId,
setup: &Setup,
) -> Result<Option<AssetId>, Error> {
match fragment {
Fragment::Include(p) => {
setup.path_to_id(&p)
.ok_or_else(|| Error::UnresolvedInclude {
in_file: setup.def(parent).path.into(),
included: p.into(),
})
.map(Some)
}
Fragment::Path(p) => {
let referee_id = setup.path_to_id(p)
.ok_or_else(|| Error::UnresolvedPath {
in_file: setup.def(parent).path.into(),
referenced: p.into(),
})?;
if setup.def(referee_id).hashed_filename() {
Ok(Some(referee_id))
} else {
Ok(None)
}
}
_ => Ok(None),
}
}
async fn load_raw_from_fs(
id: AssetId,
setup: &Setup,
config: &Config,
) -> Result<Bytes, Error> {
let def = setup.def(id);
let path = match config.path_overrides.get(def.path) {
Some(p) => p.clone(),
None => {
config.base_path.as_deref()
.unwrap_or(Path::new(setup.base_path))
.join(def.path)
}
};
let mut out = Vec::new();
if let Some(prepend) = def.prepend {
out.extend_from_slice(prepend);
}
let mut file = tokio::fs::File::open(&path).await
.map_err(|err| Error::Io { err, path: path.clone() })?;
file.read_to_end(&mut out).await
.map_err(|err| Error::Io { err, path: path.clone() })?;
if let Some(append) = def.append {
out.extend_from_slice(append);
}
Ok(Bytes::from(out))
}
#[cfg(any(not(debug_assertions), feature = "debug-is-prod"))]
fn load_from_static(raw: &'static [u8]) -> Bytes {
#[cfg(feature = "compress")]
{
use flate2::bufread::DeflateDecoder;
use std::io::Read;
let mut decompressed = Vec::new();
let mut decoder = DeflateDecoder::new(raw);
decoder.read_to_end(&mut decompressed).expect("failed to decompress static data");
Bytes::from(decompressed)
}
#[cfg(not(feature = "compress"))]
Bytes::from_static(raw)
}
#[derive(Debug)]
enum RawAsset {
Template(Template),
Literal(Bytes),
}
impl RawAsset {
fn new(raw: Bytes, def: &AssetDef) -> Result<Self, Error> {
if def.template {
let t = Template::parse(raw)
.map_err(|err| Error::Template { err, file: def.path.into() })?;
Ok(Self::Template(t))
} else {
Ok(Self::Literal(raw))
}
}
fn into_already_rendered(self) -> Result<Bytes, Template> {
match self {
Self::Template(t) => t.into_already_rendered(),
Self::Literal(b) => Ok(b),
}
}
}