use super::ManifestValue;
use crate::ast::MacroDefinition;
use crate::localization::{self, keys};
use anyhow::{Context, Result};
use minijinja::{Environment, Error, value::Value};
use serde::Serialize;
mod call;
mod invocation;
mod telemetry;
#[cfg(test)]
pub(crate) use call::call_macro_value;
use invocation::{make_macro_fn, validate_macro};
const MACRO_IMPORTS_GLOBAL: &str = "__netsuke_manifest_macro_imports";
pub(crate) enum QueryEvaluation<T> {
Value(T),
QueryDisabled,
}
pub(crate) fn evaluate_when_expression(
env: &Environment,
expression: &str,
context: &Value,
) -> Result<Option<QueryEvaluation<bool>>> {
let Ok(compiled) = env.compile_expression(expression) else {
return Ok(None);
};
classify_query_evaluation(compiled.eval(context))
.map(|evaluation| evaluation.map(|value| value.is_true()))
.with_context(|| {
localization::message(keys::MANIFEST_WHEN_EVAL_ERROR).with_arg("expr", expression)
})
.map(Some)
}
pub(crate) fn render_when_template(
env: &Environment,
template: &str,
context: &Value,
) -> Result<QueryEvaluation<String>> {
classify_query_evaluation(render_template(env, template, context)).with_context(|| {
localization::message(keys::MANIFEST_WHEN_TEMPLATE_ERROR).with_arg("expr", template)
})
}
fn classify_query_evaluation<T>(
evaluation: std::result::Result<T, Error>,
) -> Result<QueryEvaluation<T>> {
match evaluation {
Ok(value) => Ok(QueryEvaluation::Value(value)),
Err(error) if crate::stdlib::is_manifest_query_disabled_error(&error) => {
Ok(QueryEvaluation::QueryDisabled)
}
Err(error) => Err(error.into()),
}
}
impl<T> QueryEvaluation<T> {
fn map<U>(self, transform: impl FnOnce(T) -> U) -> QueryEvaluation<U> {
match self {
Self::Value(value) => QueryEvaluation::Value(transform(value)),
Self::QueryDisabled => QueryEvaluation::QueryDisabled,
}
}
}
pub(crate) fn parse_macro_name(signature: &str) -> Result<String> {
let trimmed = signature.trim();
if trimmed.is_empty() {
return Err(anyhow::anyhow!(
"{}",
localization::message(keys::MANIFEST_MACRO_SIGNATURE_MISSING_IDENTIFIER)
.with_arg("signature", signature)
));
}
let Some((name_segment, _rest)) = trimmed.split_once('(') else {
return Err(anyhow::anyhow!(
"{}",
localization::message(keys::MANIFEST_MACRO_SIGNATURE_MISSING_PARAMS)
.with_arg("signature", signature)
));
};
let identifier = name_segment.trim();
if identifier.is_empty() {
return Err(anyhow::anyhow!(
"{}",
localization::message(keys::MANIFEST_MACRO_SIGNATURE_MISSING_IDENTIFIER)
.with_arg("signature", signature)
));
}
Ok(identifier.to_owned())
}
pub(crate) fn register_macro(
env: &mut Environment<'static>,
macro_def: &MacroDefinition,
index: usize,
) -> Result<()> {
let name = parse_macro_name(¯o_def.signature)?;
let template_name = format!("__manifest_macro_{index}_{name}");
let template_source = format!(
"{{% macro {} %}}{}{{% endmacro %}}",
macro_def.signature, macro_def.body
);
env.add_template_owned(template_name.clone(), template_source)
.with_context(|| {
localization::message(keys::MANIFEST_MACRO_COMPILE_FAILED).with_arg("name", &name)
})?;
validate_macro(env, &template_name, &name)?;
register_macro_import(env, &template_name, &name);
env.add_function(name.clone(), make_macro_fn(template_name, name));
Ok(())
}
pub(crate) fn register_manifest_macros(
doc: &ManifestValue,
env: &mut Environment<'static>,
) -> Result<()> {
let Some(macros) = doc.get("macros").cloned() else {
return Ok(());
};
let defs: Vec<MacroDefinition> = serde_json::from_value(macros)
.context(localization::message(keys::MANIFEST_MACRO_SEQUENCE_INVALID))?;
for (idx, def) in defs.iter().enumerate() {
register_macro(env, def, idx).with_context(|| {
localization::message(keys::MANIFEST_MACRO_REGISTER_FAILED)
.with_arg("signature", &def.signature)
})?;
}
Ok(())
}
pub(crate) fn render_template(
env: &Environment,
template: &str,
context: &impl Serialize,
) -> Result<String, Error> {
let imports = macro_imports(env);
let has_macro_imports = imports.is_some();
telemetry::instrument_template_render(has_macro_imports, || {
imports.map_or_else(
|| env.render_str(template, context),
|import_block| env.render_str(&[import_block.as_str(), template].concat(), context),
)
})
}
fn register_macro_import(env: &mut Environment<'static>, template_name: &str, macro_name: &str) {
let existing = macro_imports(env).unwrap_or_default();
let import = format!("{{% from '{template_name}' import {macro_name} %}}");
env.add_global(MACRO_IMPORTS_GLOBAL, [existing, import].concat());
}
fn macro_imports(env: &Environment) -> Option<String> {
env.globals().find_map(|(name, value)| {
(name == MACRO_IMPORTS_GLOBAL)
.then(|| value.as_str().map(str::to_owned))
.flatten()
})
}