use super::jinja_macros::render_template;
use super::{ManifestMap, ManifestValue};
use crate::hex::push_lower_hex_byte;
use crate::localization::{self, keys};
use anyhow::{Context, Result};
use minijinja::{Environment, context, value::Value};
use serde_json::{Number as JsonNumber, map::Entry};
use sha2::{Digest, Sha256};
use tracing::{debug, dispatcher, subscriber::NoSubscriber};
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
pub(crate) struct FilteringStats {
pub filtered_targets: usize,
pub filtered_actions: usize,
}
struct ExpansionContext<'a> {
env: &'a Environment<'a>,
section: &'a str,
}
pub(crate) fn expand_foreach(doc: &mut ManifestValue, env: &Environment) -> Result<FilteringStats> {
let filtered_targets = expand_section(doc, "targets", env)?;
let filtered_actions = expand_section(doc, "actions", env)?;
let stats = FilteringStats {
filtered_targets,
filtered_actions,
};
debug!(
filtered_targets = stats.filtered_targets,
filtered_actions = stats.filtered_actions,
filtered_entry_count = stats.filtered_targets + stats.filtered_actions,
"expanded manifest foreach and when directives"
);
Ok(stats)
}
fn expand_section(doc: &mut ManifestValue, key: &str, env: &Environment) -> Result<usize> {
let Some(entries) = doc.get_mut(key).and_then(|v| v.as_array_mut()) else {
return Ok(0);
};
let mut expanded = Vec::new();
let mut filtered = 0;
let context = ExpansionContext { env, section: key };
for entry in std::mem::take(entries) {
match entry {
ManifestValue::Object(map) => {
expanded.extend(expand_target(map, &context, &mut filtered)?);
}
other => expanded.push(other),
}
}
*entries = expanded;
Ok(filtered)
}
fn expand_target(
mut map: ManifestMap,
context: &ExpansionContext<'_>,
filtered: &mut usize,
) -> Result<Vec<ManifestValue>> {
if let Some(expr_val) = map.get("foreach") {
let values = parse_foreach_values(expr_val, context.env)?;
let mut items = Vec::new();
for (index, item) in values.into_iter().enumerate() {
let mut clone = map.clone();
clone.remove("foreach");
if !when_allows(&mut clone, context, Some((&item, index)))? {
*filtered += 1;
continue;
}
inject_iteration_vars(&mut clone, &item, index)?;
items.push(ManifestValue::Object(clone));
}
Ok(items)
} else {
if !when_allows(&mut map, context, None)? {
*filtered += 1;
return Ok(vec![]);
}
Ok(vec![ManifestValue::Object(map)])
}
}
fn entry_name(map: &ManifestMap) -> &str {
map.get("name")
.and_then(ManifestValue::as_str)
.unwrap_or("<unnamed>")
}
fn entry_name_hash(entry_name: &str) -> String {
let digest = Sha256::digest(entry_name.as_bytes());
digest
.iter()
.take(4)
.fold(String::with_capacity(8), |mut hash, byte| {
push_lower_hex_byte(&mut hash, *byte);
hash
})
}
fn parse_foreach_values(expr_val: &ManifestValue, env: &Environment) -> Result<Vec<Value>> {
if let Some(seq) = expr_val.as_array() {
return Ok(seq.iter().cloned().map(Value::from_serialize).collect());
}
let expr = as_str(expr_val, "foreach")?;
let seq = eval_expression(env, "foreach", expr, context! {})?;
let iter = seq
.try_iter()
.context(localization::message(keys::MANIFEST_FOREACH_NOT_ITERABLE))?;
Ok(iter.collect())
}
fn eval_when(env: &Environment, expr: &str, ctx: Value) -> Result<bool> {
anyhow::ensure!(
!expr.trim().is_empty(),
"{}",
localization::message(keys::MANIFEST_WHEN_EMPTY)
);
if let Ok(compiled) = env.compile_expression(expr) {
let result = compiled.eval(ctx).with_context(|| {
localization::message(keys::MANIFEST_WHEN_EVAL_ERROR).with_arg("expr", expr)
})?;
return Ok(result.is_true());
}
let rendered = render_template(env, expr, &ctx).with_context(|| {
localization::message(keys::MANIFEST_WHEN_TEMPLATE_ERROR).with_arg("expr", expr)
})?;
Ok(matches!(
rendered.trim().to_lowercase().as_str(),
"true" | "1"
))
}
fn when_allows(
map: &mut ManifestMap,
context: &ExpansionContext<'_>,
iteration: Option<(&Value, usize)>,
) -> Result<bool> {
let Some(when_val) = map.remove("when") else {
return Ok(true);
};
let expr = as_str(&when_val, "when")?;
let ctx = when_context(map, iteration)?;
let allowed = eval_when(context.env, expr, ctx)?;
if !allowed && has_subscriber() {
let entry_name = entry_name(map);
let iteration_index = iteration.map(|(_, index)| index);
let entry_name_hash = entry_name_hash(entry_name);
debug!(
section = context.section,
entry_name_hash,
iteration_index,
when_expression_len = expr.len(),
when_result = false,
"filtered manifest entry by when expression"
);
}
Ok(allowed)
}
fn has_subscriber() -> bool {
dispatcher::get_default(|current| !current.is::<NoSubscriber>())
}
fn when_context(map: &ManifestMap, iteration: Option<(&Value, usize)>) -> Result<Value> {
let mut vars = map
.get("vars")
.and_then(ManifestValue::as_object)
.cloned()
.unwrap_or_default();
if let Some((item, index)) = iteration {
vars.insert(
"item".into(),
serde_json::to_value(item)
.context(localization::message(keys::MANIFEST_FOREACH_SERIALISE_ITEM))?,
);
vars.insert(
"index".into(),
ManifestValue::Number(JsonNumber::from(index as u64)),
);
}
Ok(Value::from_serialize(vars))
}
fn inject_iteration_vars(map: &mut ManifestMap, item: &Value, index: usize) -> Result<()> {
let vars_value = match map.entry("vars") {
Entry::Vacant(slot) => slot.insert(ManifestValue::Object(ManifestMap::new())),
Entry::Occupied(slot) => {
let value = slot.into_mut();
match value {
ManifestValue::Object(_) => value,
other => {
return Err(anyhow::anyhow!(
"{}",
localization::message(keys::MANIFEST_TARGET_VARS_NOT_OBJECT)
.with_arg("value", format!("{other:?}"))
));
}
}
}
};
let vars = vars_value.as_object_mut().ok_or_else(|| {
anyhow::anyhow!(
"{}",
localization::message(keys::MANIFEST_VARS_ENTRY_NOT_OBJECT)
)
})?;
vars.insert(
"item".into(),
serde_json::to_value(item)
.context(localization::message(keys::MANIFEST_FOREACH_SERIALISE_ITEM))?,
);
let index_value = ManifestValue::Number(JsonNumber::from(index as u64));
vars.insert("index".into(), index_value);
Ok(())
}
fn as_str<'a>(value: &'a ManifestValue, field: &str) -> Result<&'a str> {
value.as_str().ok_or_else(|| {
anyhow::anyhow!(
"{}",
localization::message(keys::MANIFEST_FIELD_NOT_STRING).with_arg("field", field)
)
})
}
fn eval_expression(env: &Environment, name: &str, expr: &str, ctx: Value) -> Result<Value> {
env.compile_expression(expr)
.with_context(|| {
localization::message(keys::MANIFEST_EXPRESSION_PARSE_ERROR).with_arg("name", name)
})?
.eval(ctx)
.with_context(|| {
localization::message(keys::MANIFEST_EXPRESSION_EVAL_ERROR).with_arg("name", name)
})
}
#[cfg(test)]
#[path = "expand_tests.rs"]
mod tests;