use minijinja::Environment;
use minijinja::syntax::SyntaxConfig;
use serde::Serialize;
use crate::error::{Error, Result};
use crate::spec::InstallSpec;
use crate::target;
mod delimiters {
pub const BLOCK_START: &str = "<%";
pub const BLOCK_END: &str = "%>";
pub const VARIABLE_START: &str = "<{";
pub const VARIABLE_END: &str = "}>";
pub const COMMENT_START: &str = "<#";
pub const COMMENT_END: &str = "#>";
}
#[derive(Debug, Serialize)]
struct Context<'a> {
slug: &'a str,
owner: &'a str,
repo: &'a str,
tag: &'a str,
resolved_tag: &'a str,
binary: &'a str,
shell: &'a str,
proxy: &'a str,
resource_type: &'static str,
resource_ref: &'a str,
install_dir: &'a str,
min_disk_space_mb: u64,
default_target: &'a str,
assets: Vec<&'a crate::spec::AssetEntry>,
targets: Vec<&'a str>,
fallbacks: Vec<Fallback<'a>>,
asset_groups: Vec<AssetGroup<'a>>,
filenames: Vec<&'a str>,
command: String,
eish_version: &'static str,
}
#[derive(Debug, Serialize)]
struct AssetGroup<'a> {
filename: &'a str,
targets: Vec<&'a str>,
}
#[derive(Debug, Serialize)]
struct Fallback<'a> {
target: &'static str,
alternatives: Vec<&'a str>,
}
impl<'a> Context<'a> {
fn new(spec: &'a InstallSpec, slug: &'a str) -> Self {
let mut assets: Vec<_> = spec.assets.iter().collect();
assets.sort_by(|a, b| a.target.cmp(&b.target));
let targets = assets.iter().map(|asset| asset.target.as_str()).collect();
let asset_groups = build_asset_groups(spec);
let filenames = asset_groups.iter().map(|group| group.filename).collect();
Self {
slug,
owner: &spec.owner,
repo: &spec.repo,
tag: &spec.tag,
resolved_tag: spec.resolved_tag.as_deref().unwrap_or(&spec.tag),
binary: spec.binary_name(),
shell: spec.shell.as_str(),
proxy: spec.proxy.as_str(),
resource_type: spec.resource.as_str(),
resource_ref: spec.resource.reference().unwrap_or("main"),
install_dir: &spec.install_dir,
min_disk_space_mb: spec.min_disk_space_mb,
default_target: spec.default_target.as_deref().unwrap_or(""),
fallbacks: build_fallbacks(spec),
asset_groups,
filenames,
command: spec.regenerate_command(),
targets,
assets,
eish_version: crate::VERSION,
}
}
}
fn build_asset_groups(spec: &InstallSpec) -> Vec<AssetGroup<'_>> {
let mut groups: Vec<AssetGroup<'_>> = Vec::new();
for asset in &spec.assets {
match groups.iter_mut().find(|g| g.filename == asset.filename) {
Some(group) => group.targets.push(&asset.target),
None => groups.push(AssetGroup {
filename: &asset.filename,
targets: vec![&asset.target],
}),
}
}
groups.sort_by(|a, b| a.filename.cmp(b.filename));
groups
}
fn build_fallbacks(spec: &InstallSpec) -> Vec<Fallback<'static>> {
let available: Vec<&str> = spec.assets.iter().map(|a| a.target.as_str()).collect();
target::KNOWN_TARGETS
.iter()
.filter_map(|known| {
let alternatives: Vec<&str> = target::compatible_targets(known)
.into_iter()
.filter(|candidate| available.contains(candidate))
.collect();
(!alternatives.is_empty()).then_some(Fallback {
target: known,
alternatives,
})
})
.collect()
}
pub fn render(spec: &InstallSpec) -> Result<String> {
spec.validate()?;
let mut env = Environment::new();
env.set_syntax(
SyntaxConfig::builder()
.block_delimiters(delimiters::BLOCK_START, delimiters::BLOCK_END)
.variable_delimiters(delimiters::VARIABLE_START, delimiters::VARIABLE_END)
.comment_delimiters(delimiters::COMMENT_START, delimiters::COMMENT_END)
.build()
.map_err(|source| Error::Render {
shell: spec.shell,
source,
})?,
);
env.set_keep_trailing_newline(true);
env.add_template(spec.shell.template_id(), spec.shell.template())
.map_err(|source| Error::Render {
shell: spec.shell,
source,
})?;
let slug = spec.slug();
let context = Context::new(spec, &slug);
env.get_template(spec.shell.template_id())
.and_then(|template| template.render(&context))
.map_err(|source| Error::Render {
shell: spec.shell,
source,
})
}