use anyhow::{Context, Result};
use handlebars::Handlebars;
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ExecutionContext {
pub variables: HashMap<String, Value>,
pub saved_responses: HashMap<String, Value>,
pub environment: HashMap<String, String>,
handlebars: Handlebars<'static>,
}
impl ExecutionContext {
pub fn new() -> Self {
Self {
variables: HashMap::new(),
saved_responses: HashMap::new(),
environment: HashMap::new(),
handlebars: Handlebars::new(),
}
}
pub fn with_variables(variables: HashMap<String, Value>) -> Self {
let mut ctx = Self::new();
ctx.variables = variables;
ctx
}
pub fn set_variable(&mut self, key: String, value: Value) {
self.variables.insert(key, value);
}
pub fn save_response(&mut self, key: String, response: Value) {
self.saved_responses.insert(key, response);
}
pub fn load_environment(&mut self) {
if let Ok(_) = dotenv::dotenv() {
}
for (key, value) in std::env::vars() {
if key.starts_with("COLLECTION_") {
let var_name = key.strip_prefix("COLLECTION_").unwrap();
self.environment.insert(var_name.to_string(), value);
}
}
}
pub fn load_env_file(&mut self, path: &std::path::Path) -> Result<()> {
let env_contents = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read .env file from {:?}", path))?;
for line in env_contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
let value = value
.trim()
.trim_matches('"') .trim_matches('\'');
self.environment.insert(key.to_string(), value.to_string());
}
}
Ok(())
}
pub fn resolve_string(&self, template: &str) -> Result<String> {
if !template.contains("{{") {
return Ok(template.to_string());
}
let mut context = serde_json::Map::new();
for (k, v) in &self.environment {
context.insert(k.clone(), Value::String(v.clone()));
}
for (k, v) in &self.variables {
context.insert(k.clone(), v.clone());
}
for (k, v) in &self.saved_responses {
context.insert(k.clone(), v.clone());
}
self.handlebars
.render_template(template, &context)
.with_context(|| format!("Failed to resolve template: {}", template))
}
pub fn resolve_value(&self, value: &Value) -> Result<Value> {
match value {
Value::String(s) => {
let resolved = self.resolve_string(s)?;
if (resolved.starts_with('{') || resolved.starts_with('['))
&& (resolved.ends_with('}') || resolved.ends_with(']'))
{
if let Ok(parsed) = serde_json::from_str(&resolved) {
return Ok(parsed);
}
}
Ok(Value::String(resolved))
}
Value::Object(map) => {
let mut resolved = serde_json::Map::new();
for (k, v) in map {
resolved.insert(k.clone(), self.resolve_value(v)?);
}
Ok(Value::Object(resolved))
}
Value::Array(arr) => {
let mut resolved = Vec::new();
for v in arr {
resolved.push(self.resolve_value(v)?);
}
Ok(Value::Array(resolved))
}
_ => Ok(value.clone()),
}
}
pub fn resolve_params(
&self,
params: &HashMap<String, Value>,
) -> Result<HashMap<String, Value>> {
let mut resolved = HashMap::new();
for (k, v) in params {
resolved.insert(k.clone(), self.resolve_value(v)?);
}
Ok(resolved)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_simple_variable_resolution() {
let mut ctx = ExecutionContext::new();
ctx.set_variable("username".to_string(), json!("octocat"));
let result = ctx.resolve_string("Hello {{username}}!").unwrap();
assert_eq!(result, "Hello octocat!");
}
#[test]
fn test_nested_variable_resolution() {
let mut ctx = ExecutionContext::new();
ctx.set_variable(
"user".to_string(),
json!({
"name": "octocat",
"id": 123
}),
);
let result = ctx
.resolve_string("User: {{user.name}} (ID: {{user.id}})")
.unwrap();
assert_eq!(result, "User: octocat (ID: 123)");
}
#[test]
fn test_saved_response_resolution() {
let mut ctx = ExecutionContext::new();
ctx.save_response(
"user_response".to_string(),
json!({
"id": 456,
"login": "defunkt"
}),
);
let result = ctx.resolve_string("{{user_response.login}}").unwrap();
assert_eq!(result, "defunkt");
}
#[test]
fn test_resolve_params() {
let mut ctx = ExecutionContext::new();
ctx.set_variable("org".to_string(), json!("github"));
let params = HashMap::from([
("owner".to_string(), json!("{{org}}")),
("repo".to_string(), json!("docs")),
]);
let resolved = ctx.resolve_params(¶ms).unwrap();
assert_eq!(resolved["owner"], json!("github"));
assert_eq!(resolved["repo"], json!("docs"));
}
}