use std::{
collections::HashMap,
path::PathBuf,
};
use crate::request::{TemplateError, VariableStore};
use super::{
context,
spans::prompt_error_to_template_error,
};
pub fn eval_shell_script(
script: &str,
working_dir: &PathBuf,
env: Option<HashMap<String, String>>,
) -> String {
let env = env.unwrap_or_default();
log::debug!("evaluating shell script: {}", script);
log::debug!("using directory {:?}", working_dir);
let output = std::process::Command::new("sh")
.current_dir(working_dir)
.arg("-c")
.envs(env)
.arg(script)
.output()
.expect("failed to execute process");
String::from_utf8(output.stdout).unwrap()
}
pub(super) fn assign_global_variable_for_validation(
store: &mut VariableStore,
key: String,
raw_value: &str,
) -> Result<(), TemplateError> {
let label = format!("${}", key);
let trimmed = raw_value.trim();
let empty_context = HashMap::new();
if trimmed.starts_with("$(") {
store.set_scalar(key.clone(), syntax_placeholder_value(key.as_str()));
return Ok(());
}
if is_array_literal(trimmed) {
let values = parse_array_literal(trimmed, &label, &key, |value| {
Ok(context::inject_from_variable(value, &empty_context))
})?;
store.set_array(key, values);
return Ok(());
}
let value = context::inject_from_variable(trimmed, &empty_context);
store.set_scalar(key, value);
Ok(())
}
pub(super) fn assign_request_variable_for_validation(
store: &mut VariableStore,
key: String,
raw_value: &str,
) -> Result<(), TemplateError> {
let label = format!("request variable ${}", key);
let trimmed = raw_value.trim();
if trimmed.starts_with("$(") {
store.set_scalar(key.clone(), syntax_placeholder_value(key.as_str()));
return Ok(());
}
let current_scalars = store.clone_scalars();
if is_array_literal(trimmed) {
let values = parse_array_literal(trimmed, &label, &key, |value| {
Ok(context::inject_from_variable(value, ¤t_scalars))
})?;
store.set_array(key, values);
return Ok(());
}
let value = context::inject_from_variable(trimmed, ¤t_scalars);
store.set_scalar(key, value);
Ok(())
}
fn syntax_placeholder_value(key: &str) -> String {
format!("__hen_syntax_{}__", key)
}
pub(super) fn assign_global_variable(
store: &mut VariableStore,
key: String,
raw_value: &str,
working_dir: &PathBuf,
) -> Result<(), TemplateError> {
let label = format!("${}", key);
let trimmed = raw_value.trim();
if trimmed.starts_with("$(") {
let script = trimmed.trim_start_matches("$(").trim_end_matches(")");
let value = eval_shell_script(script, working_dir, None)
.trim()
.to_string();
store.set_scalar(key, value);
return Ok(());
}
if is_array_literal(trimmed) {
let values = parse_array_literal(trimmed, &label, &key, |value| {
context::try_inject_from_prompt(value)
.map_err(|err| prompt_error_to_template_error(&label, err))
})?;
store.set_array(key, values);
return Ok(());
}
let value = context::try_inject_from_prompt(trimmed)
.map_err(|err| prompt_error_to_template_error(&label, err))?;
store.set_scalar(key, value);
Ok(())
}
pub(super) fn assign_request_variable(
store: &mut VariableStore,
key: String,
raw_value: &str,
working_dir: &PathBuf,
) -> Result<(), TemplateError> {
let label = format!("request variable ${}", key);
let trimmed = raw_value.trim();
if trimmed.starts_with("$(") {
let script = trimmed.trim_start_matches("$(").trim_end_matches(")");
let value = eval_shell_script(script, working_dir, None)
.trim()
.to_string();
store.set_scalar(key, value);
return Ok(());
}
let current_scalars = store.clone_scalars();
if is_array_literal(trimmed) {
let values = parse_array_literal(trimmed, &label, &key, |value| {
Ok(context::inject_from_variable(value, ¤t_scalars))
})?;
store.set_array(key, values);
return Ok(());
}
let value = context::inject_from_variable(trimmed, ¤t_scalars);
store.set_scalar(key, value);
Ok(())
}
fn is_array_literal(raw: &str) -> bool {
let trimmed = raw.trim();
trimmed.starts_with('[')
&& trimmed.ends_with(']')
&& trimmed.len() >= 2
&& !is_standalone_prompt_placeholder(trimmed)
}
fn is_standalone_prompt_placeholder(raw: &str) -> bool {
raw.strip_prefix("[[")
.and_then(|inner| inner.strip_suffix("]]"))
.map(|inner| !inner.contains('[') && !inner.contains(']'))
.unwrap_or(false)
}
fn parse_array_literal<F>(
raw: &str,
label: &str,
variable: &str,
mut transform: F,
) -> Result<Vec<String>, TemplateError>
where
F: FnMut(&str) -> Result<String, TemplateError>,
{
if !is_array_literal(raw) {
return Err(TemplateError::InvalidArrayValue {
request: label.to_string(),
variable: variable.to_string(),
value: raw.to_string(),
});
}
let inner = &raw[1..raw.len() - 1];
if inner.trim().is_empty() {
return Err(TemplateError::EmptyArrayValues {
request: label.to_string(),
variable: variable.to_string(),
});
}
let mut segments: Vec<String> = Vec::new();
let mut current = String::new();
let mut in_single = false;
let mut in_double = false;
for ch in inner.chars() {
match ch {
'\'' => {
if !in_double {
in_single = !in_single;
}
current.push(ch);
}
'"' => {
if !in_single {
in_double = !in_double;
}
current.push(ch);
}
',' if !in_single && !in_double => {
if current.trim().is_empty() {
return Err(TemplateError::InvalidArrayValue {
request: label.to_string(),
variable: variable.to_string(),
value: raw.to_string(),
});
}
segments.push(current.trim().to_string());
current.clear();
}
_ => current.push(ch),
}
}
if in_single || in_double {
return Err(TemplateError::InvalidArrayValue {
request: label.to_string(),
variable: variable.to_string(),
value: raw.to_string(),
});
}
if current.trim().is_empty() {
return Err(TemplateError::InvalidArrayValue {
request: label.to_string(),
variable: variable.to_string(),
value: raw.to_string(),
});
}
segments.push(current.trim().to_string());
let mut values = Vec::with_capacity(segments.len());
for segment in segments {
let normalized = normalize_array_element(segment.as_str());
if normalized.trim().is_empty() {
return Err(TemplateError::InvalidArrayValue {
request: label.to_string(),
variable: variable.to_string(),
value: raw.to_string(),
});
}
values.push(transform(normalized.trim())?);
}
Ok(values)
}
fn normalize_array_element(value: &str) -> String {
let trimmed = value.trim();
if (trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2)
|| (trimmed.starts_with('\'') && trimmed.ends_with('\'') && trimmed.len() >= 2)
{
trimmed[1..trimmed.len() - 1].to_string()
} else {
trimmed.to_string()
}
}