hen 0.15.0

Run protocol-aware API request collections from the command line or through MCP.
Documentation
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, &current_scalars))
        })?;
        store.set_array(key, values);
        return Ok(());
    }

    let value = context::inject_from_variable(trimmed, &current_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, &current_scalars))
        })?;
        store.set_array(key, values);
        return Ok(());
    }

    let value = context::inject_from_variable(trimmed, &current_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()
    }
}