use crate::core::types::*;
use std::collections::HashMap;
type BuiltinImpl = fn(&[String]) -> Result<String, String>;
const BUILTINS: &[(&str, usize, BuiltinImpl)] = &[
("upper", 1, |args| Ok(args[0].to_uppercase())),
("lower", 1, |args| Ok(args[0].to_lowercase())),
("trim", 1, |args| Ok(args[0].trim().to_string())),
("default", 2, |args| {
Ok(if args[0].is_empty() {
args[1].clone()
} else {
args[0].clone()
})
}),
("replace", 3, |args| {
Ok(args[0].replace(args[1].as_str(), args[2].as_str()))
}),
("env", 1, |args| {
std::env::var(&args[0]).map_err(|_| format!("env var '{}' not set", args[0]))
}),
("b3sum", 1, |args| {
Ok(blake3::hash(args[0].as_bytes()).to_hex().to_string())
}),
("join", 2, |args| {
let parts: Vec<&str> = args[0].split(',').map(|s| s.trim()).collect();
Ok(parts.join(&args[1]))
}),
("split", 2, |args| {
let parts: Vec<&str> = args[0].split(args[1].as_str()).collect();
Ok(parts.join(","))
}),
];
pub(crate) fn resolve_function(
expr: &str,
params: &HashMap<String, serde_yaml_ng::Value>,
machines: &indexmap::IndexMap<String, Machine>,
) -> Result<String, String> {
let (func_name, args_str) = split_call(expr)?;
let args = parse_func_args(args_str, params, machines)?;
let (_, arity, apply) = BUILTINS
.iter()
.find(|(name, _, _)| *name == func_name)
.ok_or_else(|| format!("unknown template function: {func_name}"))?;
check_arg_count(func_name, &args, *arity)?;
apply(&args)
}
fn split_call(expr: &str) -> Result<(&str, &str), String> {
let open_paren = expr
.find('(')
.ok_or_else(|| format!("malformed function: {expr}"))?;
if !expr.ends_with(')') {
return Err(format!("unclosed parenthesis in function: {expr}"));
}
Ok((
expr[..open_paren].trim(),
&expr[open_paren + 1..expr.len() - 1],
))
}
fn parse_func_args(
args_str: &str,
params: &HashMap<String, serde_yaml_ng::Value>,
machines: &indexmap::IndexMap<String, Machine>,
) -> Result<Vec<String>, String> {
split_top_level_args(args_str)
.into_iter()
.map(|arg| resolve_func_arg(&arg, params, machines))
.collect()
}
fn split_top_level_args(args_str: &str) -> Vec<String> {
let mut args = Vec::new();
let mut current = String::new();
let mut in_quote = false;
let mut quote_char = '"';
let mut depth = 0;
for ch in args_str.chars() {
if in_quote {
if ch == quote_char {
in_quote = false;
} else {
current.push(ch);
}
continue;
}
match ch {
'"' | '\'' => {
in_quote = true;
quote_char = ch;
}
'(' => {
depth += 1;
current.push(ch);
}
')' if depth > 0 => {
depth -= 1;
current.push(ch);
}
',' if depth == 0 => {
args.push(current.trim().to_string());
current.clear();
}
_ => current.push(ch),
}
}
let trimmed = current.trim();
if !trimmed.is_empty() {
args.push(trimmed.to_string());
}
args
}
fn resolve_func_arg(
arg: &str,
params: &HashMap<String, serde_yaml_ng::Value>,
machines: &indexmap::IndexMap<String, Machine>,
) -> Result<String, String> {
if (arg.starts_with('"') && arg.ends_with('"'))
|| (arg.starts_with('\'') && arg.ends_with('\''))
{
return Ok(arg[1..arg.len() - 1].to_string());
}
if let Some(param_key) = arg.strip_prefix("params.") {
return params
.get(param_key)
.map(yaml_value_to_string)
.ok_or_else(|| format!("unknown param in function arg: {param_key}"));
}
if arg.starts_with("machine.") {
let parts: Vec<&str> = arg.splitn(3, '.').collect();
if parts.len() == 3 {
let machine = machines
.get(parts[1])
.ok_or_else(|| format!("unknown machine: {}", parts[1]))?;
return Ok(match parts[2] {
"addr" => machine.addr.clone(),
"hostname" => machine.hostname.clone(),
"user" => machine.user.clone(),
"arch" => machine.arch.clone(),
_ => return Err(format!("unknown machine field: {}", parts[2])),
});
}
}
if arg.contains('(') {
return resolve_function(arg, params, machines);
}
Ok(arg.to_string())
}
fn check_arg_count(func: &str, args: &[String], expected: usize) -> Result<(), String> {
if args.len() != expected {
return Err(format!(
"{}() requires {} argument(s), got {}",
func,
expected,
args.len()
));
}
Ok(())
}