use super::ManifestValue;
use crate::ast::MacroDefinition;
use crate::localization::{self, keys};
use anyhow::{Context, Result};
use metrics::{counter, describe_counter, describe_histogram, histogram};
use minijinja::{Environment, Error};
use serde::Serialize;
use std::{sync::Once, time::Instant};
use tracing::field;
const TEMPLATE_RENDERS_TOTAL: &str = "netsuke_manifest_template_renders_total";
const TEMPLATE_RENDER_DURATION: &str = "netsuke_manifest_template_render_duration_seconds";
mod call;
mod invocation;
#[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) 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> {
describe_render_metrics();
let imports = macro_imports(env);
let has_macro_imports = imports.is_some();
let span = tracing::trace_span!(
"manifest.template.render",
outcome = field::Empty,
has_macro_imports,
error_category = field::Empty,
);
let _guard = span.enter();
let started = Instant::now();
let result = imports.map_or_else(
|| env.render_str(template, context),
|import_block| env.render_str(&[import_block.as_str(), template].concat(), context),
);
record_render(&span, &result, has_macro_imports, started);
result
}
fn describe_render_metrics() {
static DESCRIBE: Once = Once::new();
DESCRIBE.call_once(|| {
describe_counter!(
TEMPLATE_RENDERS_TOTAL,
"Counts manifest template renders by bounded outcome and macro-import presence."
);
describe_histogram!(
TEMPLATE_RENDER_DURATION,
"Measures manifest template rendering duration in seconds."
);
});
}
fn record_render(
span: &tracing::Span,
result: &Result<String, Error>,
has_macro_imports: bool,
started: Instant,
) {
let outcome = if result.is_ok() { "success" } else { "error" };
span.record("outcome", outcome);
if let Err(error) = result {
span.record("error_category", format_args!("{:?}", error.kind()));
tracing::debug!(error_category = ?error.kind(), "manifest template render failed");
}
counter!(
TEMPLATE_RENDERS_TOTAL,
"outcome" => outcome,
"has_macro_imports" => if has_macro_imports { "true" } else { "false" },
)
.increment(1);
histogram!(TEMPLATE_RENDER_DURATION).record(started.elapsed());
}
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()
})
}