use crate::cook::environment::EnvValue;
use crate::cook::execution::interpolation::{InterpolationContext, InterpolationEngine};
use anyhow::{Context, Result};
use serde_json::Value;
use std::collections::HashMap;
pub fn build_positional_args_context(args: &[String]) -> InterpolationContext {
let mut variables = HashMap::new();
for (index, arg) in args.iter().enumerate() {
let position = index + 1;
variables.insert(position.to_string(), Value::String(arg.clone()));
variables.insert(format!("ARG_{}", position), Value::String(arg.clone()));
}
InterpolationContext {
variables,
parent: None,
}
}
pub fn interpolate_env_value(
value: &str,
context: &InterpolationContext,
engine: &mut InterpolationEngine,
) -> Result<String> {
engine
.interpolate(value, context)
.map_err(|e| anyhow::anyhow!("Interpolation failed: {}", e))
}
pub fn interpolate_workflow_env_with_positional_args(
workflow_env: Option<&HashMap<String, String>>,
positional_args: &[String],
) -> Result<HashMap<String, EnvValue>> {
let Some(env_map) = workflow_env else {
return Ok(HashMap::new());
};
let context = build_positional_args_context(positional_args);
let mut engine = InterpolationEngine::new(false);
let mut result = HashMap::new();
for (key, value) in env_map {
let interpolated_value =
interpolate_env_value(value, &context, &mut engine).with_context(|| {
format!(
"Failed to interpolate workflow env variable '{}' with value '{}'",
key, value
)
})?;
result.insert(key.clone(), EnvValue::Static(interpolated_value));
}
Ok(result)
}
pub fn positional_args_as_env_vars(positional_args: &[String]) -> HashMap<String, EnvValue> {
positional_args
.iter()
.enumerate()
.map(|(index, arg)| {
let arg_name = format!("ARG_{}", index + 1);
(arg_name, EnvValue::Static(arg.clone()))
})
.collect()
}
pub fn env_values_to_plain_map(env_values: &HashMap<String, EnvValue>) -> HashMap<String, String> {
env_values
.iter()
.filter_map(|(k, v)| match v {
EnvValue::Static(val) => Some((k.clone(), val.clone())),
_ => None, })
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_positional_args_context_empty() {
let args: Vec<String> = vec![];
let context = build_positional_args_context(&args);
assert_eq!(context.variables.len(), 0);
}
#[test]
fn test_build_positional_args_context_single_arg() {
let args = vec!["file.txt".to_string()];
let context = build_positional_args_context(&args);
assert_eq!(context.variables.len(), 2); assert_eq!(
context.variables.get("1"),
Some(&Value::String("file.txt".to_string()))
);
assert_eq!(
context.variables.get("ARG_1"),
Some(&Value::String("file.txt".to_string()))
);
}
#[test]
fn test_build_positional_args_context_multiple_args() {
let args = vec![
"content/blog/my-post.md".to_string(),
"output".to_string(),
"format=markdown".to_string(),
];
let context = build_positional_args_context(&args);
assert_eq!(context.variables.len(), 6); assert_eq!(
context.variables.get("1"),
Some(&Value::String("content/blog/my-post.md".to_string()))
);
assert_eq!(
context.variables.get("ARG_2"),
Some(&Value::String("output".to_string()))
);
assert_eq!(
context.variables.get("3"),
Some(&Value::String("format=markdown".to_string()))
);
}
#[test]
fn test_interpolate_env_value_simple_reference() {
let args = vec!["file.txt".to_string()];
let context = build_positional_args_context(&args);
let mut engine = InterpolationEngine::new(false);
let result = interpolate_env_value("$1", &context, &mut engine).unwrap();
assert_eq!(result, "file.txt");
}
#[test]
fn test_interpolate_env_value_braced_reference() {
let args = vec!["file.txt".to_string()];
let context = build_positional_args_context(&args);
let mut engine = InterpolationEngine::new(false);
let result = interpolate_env_value("${ARG_1}", &context, &mut engine).unwrap();
assert_eq!(result, "file.txt");
}
#[test]
fn test_interpolate_env_value_embedded_in_string() {
let args = vec!["my-post.md".to_string()];
let context = build_positional_args_context(&args);
let mut engine = InterpolationEngine::new(false);
let result = interpolate_env_value("content/blog/$1", &context, &mut engine).unwrap();
assert_eq!(result, "content/blog/my-post.md");
}
#[test]
fn test_interpolate_env_value_multiple_references() {
let args = vec!["input.txt".to_string(), "output.txt".to_string()];
let context = build_positional_args_context(&args);
let mut engine = InterpolationEngine::new(false);
let result = interpolate_env_value("cp $1 $2", &context, &mut engine).unwrap();
assert_eq!(result, "cp input.txt output.txt");
}
#[test]
fn test_interpolate_workflow_env_empty() {
let args = vec!["file.txt".to_string()];
let result = interpolate_workflow_env_with_positional_args(None, &args).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_interpolate_workflow_env_no_references() {
let mut env = HashMap::new();
env.insert("SITE_URL".to_string(), "https://example.com".to_string());
env.insert("OUTPUT_DIR".to_string(), "out".to_string());
let args = vec!["file.txt".to_string()];
let result = interpolate_workflow_env_with_positional_args(Some(&env), &args).unwrap();
assert_eq!(result.len(), 2);
assert!(matches!(
result.get("SITE_URL"),
Some(EnvValue::Static(val)) if val == "https://example.com"
));
assert!(matches!(
result.get("OUTPUT_DIR"),
Some(EnvValue::Static(val)) if val == "out"
));
}
#[test]
fn test_interpolate_workflow_env_with_positional_args() {
let mut env = HashMap::new();
env.insert("BLOG_POST".to_string(), "$1".to_string());
env.insert("OUTPUT_DIR".to_string(), "cross-posts".to_string());
env.insert("POST_SLUG".to_string(), "$(basename $1 .md)".to_string());
let args = vec!["content/blog/rethinking-code-quality-analysis.md".to_string()];
let result = interpolate_workflow_env_with_positional_args(Some(&env), &args).unwrap();
assert_eq!(result.len(), 3);
assert!(matches!(
result.get("BLOG_POST"),
Some(EnvValue::Static(val)) if val == "content/blog/rethinking-code-quality-analysis.md"
));
assert!(matches!(
result.get("OUTPUT_DIR"),
Some(EnvValue::Static(val)) if val == "cross-posts"
));
}
#[test]
fn test_positional_args_as_env_vars() {
let args = vec!["file.txt".to_string(), "output.txt".to_string()];
let result = positional_args_as_env_vars(&args);
assert_eq!(result.len(), 2);
assert!(matches!(
result.get("ARG_1"),
Some(EnvValue::Static(val)) if val == "file.txt"
));
assert!(matches!(
result.get("ARG_2"),
Some(EnvValue::Static(val)) if val == "output.txt"
));
}
#[test]
fn test_env_values_to_plain_map() {
let mut env_values = HashMap::new();
env_values.insert("KEY1".to_string(), EnvValue::Static("value1".to_string()));
env_values.insert("KEY2".to_string(), EnvValue::Static("value2".to_string()));
let result = env_values_to_plain_map(&env_values);
assert_eq!(result.len(), 2);
assert_eq!(result.get("KEY1"), Some(&"value1".to_string()));
assert_eq!(result.get("KEY2"), Some(&"value2".to_string()));
}
#[test]
fn test_full_workflow_env_interpolation() {
let mut workflow_env = HashMap::new();
workflow_env.insert("BLOG_POST".to_string(), "$1".to_string());
workflow_env.insert(
"SITE_URL".to_string(),
"https://entropicdrift.com".to_string(),
);
workflow_env.insert("OUTPUT_DIR".to_string(), "cross-posts".to_string());
let args = vec!["content/blog/rethinking-code-quality-analysis.md".to_string()];
let mut interpolated_env =
interpolate_workflow_env_with_positional_args(Some(&workflow_env), &args).unwrap();
let positional_env = positional_args_as_env_vars(&args);
interpolated_env.extend(positional_env);
let plain_map = env_values_to_plain_map(&interpolated_env);
assert_eq!(
plain_map.get("BLOG_POST"),
Some(&"content/blog/rethinking-code-quality-analysis.md".to_string())
);
assert_eq!(
plain_map.get("ARG_1"),
Some(&"content/blog/rethinking-code-quality-analysis.md".to_string())
);
assert_eq!(
plain_map.get("SITE_URL"),
Some(&"https://entropicdrift.com".to_string())
);
}
}