use std::collections::HashMap;
use agentchrome::error::{AppError, ExitCode};
#[derive(Debug, Clone, Default)]
pub struct VarContext {
pub prev: serde_json::Value,
pub vars: HashMap<String, serde_json::Value>,
}
impl VarContext {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn bind(&mut self, name: &str, value: serde_json::Value) {
self.vars.insert(name.to_string(), value);
}
pub fn set_prev(&mut self, value: serde_json::Value) {
self.prev = value;
}
}
#[derive(Debug)]
pub enum SubstitutionError {
Undefined(String),
}
impl From<SubstitutionError> for AppError {
fn from(e: SubstitutionError) -> Self {
match e {
SubstitutionError::Undefined(name) => AppError {
message: format!("undefined variable: $vars.{name}"),
code: ExitCode::GeneralError,
custom_json: None,
},
}
}
}
pub fn substitute(argv: &[String], ctx: &VarContext) -> Result<Vec<String>, SubstitutionError> {
argv.iter().map(|arg| substitute_token(arg, ctx)).collect()
}
fn substitute_token(token: &str, ctx: &VarContext) -> Result<String, SubstitutionError> {
if token == "$prev" {
return Ok(value_to_string(&ctx.prev));
}
if let Some(name) = token.strip_prefix("$vars.") {
let value = ctx
.vars
.get(name)
.ok_or_else(|| SubstitutionError::Undefined(name.to_string()))?;
return Ok(value_to_string(value));
}
Ok(token.to_string())
}
fn value_to_string(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Null => "null".to_string(),
other => serde_json::to_string(other).unwrap_or_default(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx() -> VarContext {
let mut c = VarContext::new();
c.set_prev(serde_json::json!("previous output"));
c.bind("title", serde_json::json!("Example Domain"));
c.bind("count", serde_json::json!(42));
c.bind("obj", serde_json::json!({"key": "value"}));
c
}
#[test]
fn whole_token_prev_string() {
let ctx = ctx();
let argv = vec!["$prev".to_string()];
let result = substitute(&argv, &ctx).expect("ok");
assert_eq!(result, vec!["previous output"]);
}
#[test]
fn whole_token_vars_string() {
let ctx = ctx();
let argv = vec!["$vars.title".to_string()];
let result = substitute(&argv, &ctx).expect("ok");
assert_eq!(result, vec!["Example Domain"]);
}
#[test]
fn whole_token_vars_number() {
let ctx = ctx();
let argv = vec!["$vars.count".to_string()];
let result = substitute(&argv, &ctx).expect("ok");
assert_eq!(result, vec!["42"]);
}
#[test]
fn whole_token_vars_object_serialized() {
let ctx = ctx();
let argv = vec!["$vars.obj".to_string()];
let result = substitute(&argv, &ctx).expect("ok");
assert_eq!(result[0], r#"{"key":"value"}"#);
}
#[test]
fn no_substitution_passthrough() {
let ctx = ctx();
let argv = vec!["navigate".to_string(), "https://example.com".to_string()];
let result = substitute(&argv, &ctx).expect("ok");
assert_eq!(result, argv);
}
#[test]
fn undefined_variable_returns_error() {
let ctx = ctx();
let argv = vec!["$vars.does_not_exist".to_string()];
let err = substitute(&argv, &ctx).expect_err("should fail");
match err {
SubstitutionError::Undefined(name) => assert_eq!(name, "does_not_exist"),
}
}
#[test]
fn prev_null_serializes_as_null() {
let mut ctx = VarContext::new();
ctx.set_prev(serde_json::Value::Null);
let argv = vec!["$prev".to_string()];
let result = substitute(&argv, &ctx).expect("ok");
assert_eq!(result, vec!["null"]);
}
}