use crate::error::Error;
use std::collections::HashMap;
#[derive(Debug, Default)]
pub struct VariableStore {
pub scalars: HashMap<String, String>,
pub lists: HashMap<String, Vec<String>>,
}
impl VariableStore {
fn resolve(&self, name: &str) -> Option<String> {
if let Some(scalar) = self.scalars.get(name) {
return Some(scalar.clone());
}
if let Some(list) = self.lists.get(name) {
let json_array = serde_json::to_string(list)
.expect("serializing Vec<String> to JSON should never fail");
return Some(json_array);
}
None
}
}
fn interpolate_arg(arg: &str, store: &VariableStore, operation_id: &str) -> Result<String, Error> {
let mut result = String::with_capacity(arg.len());
let mut remaining = arg;
while let Some(start) = remaining.find("{{") {
result.push_str(&remaining[..start]);
let after_open = &remaining[start + 2..];
let Some(end) = after_open.find("}}") else {
result.push_str("{{");
remaining = after_open;
continue;
};
let var_name = &after_open[..end];
let value = store
.resolve(var_name)
.ok_or_else(|| Error::batch_undefined_variable(operation_id, var_name))?;
result.push_str(&value);
remaining = &after_open[end + 2..];
}
result.push_str(remaining);
Ok(result)
}
pub fn interpolate_string(
s: &str,
store: &VariableStore,
operation_id: &str,
) -> Result<String, Error> {
interpolate_arg(s, store, operation_id)
}
pub fn interpolate_args(
args: &[String],
store: &VariableStore,
operation_id: &str,
) -> Result<Vec<String>, Error> {
args.iter()
.map(|arg| interpolate_arg(arg, store, operation_id))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn store_with_scalar(name: &str, value: &str) -> VariableStore {
let mut store = VariableStore::default();
store.scalars.insert(name.into(), value.into());
store
}
fn store_with_list(name: &str, values: &[&str]) -> VariableStore {
let mut store = VariableStore::default();
store.lists.insert(
name.into(),
values.iter().map(|s| (*s).to_string()).collect(),
);
store
}
#[test]
fn scalar_interpolation() {
let store = store_with_scalar("user_id", "abc-123");
let args = vec!["--user-id".into(), "{{user_id}}".into()];
let result = interpolate_args(&args, &store, "test-op").unwrap();
assert_eq!(result, vec!["--user-id", "abc-123"]);
}
#[test]
fn scalar_embedded_in_string() {
let store = store_with_scalar("id", "42");
let args = vec!["prefix-{{id}}-suffix".into()];
let result = interpolate_args(&args, &store, "test-op").unwrap();
assert_eq!(result, vec!["prefix-42-suffix"]);
}
#[test]
fn multiple_variables_in_single_arg() {
let mut store = VariableStore::default();
store.scalars.insert("a".into(), "1".into());
store.scalars.insert("b".into(), "2".into());
let args = vec!["{{a}}-{{b}}".into()];
let result = interpolate_args(&args, &store, "test-op").unwrap();
assert_eq!(result, vec!["1-2"]);
}
#[test]
fn list_interpolation_as_json_array() {
let store = store_with_list("ids", &["id-a", "id-b", "id-c"]);
let args = vec!["{\"eventIds\": {{ids}}}".into()];
let result = interpolate_args(&args, &store, "test-op").unwrap();
assert_eq!(result, vec![r#"{"eventIds": ["id-a","id-b","id-c"]}"#]);
}
#[test]
fn list_interpolation_escapes_json_elements() {
let store = store_with_list("ids", &["a\"b", "line\nbreak"]);
let args = vec!["{\"eventIds\": {{ids}}}".into()];
let result = interpolate_args(&args, &store, "test-op").unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result[0]).unwrap();
assert_eq!(parsed["eventIds"][0], "a\"b");
assert_eq!(parsed["eventIds"][1], "line\nbreak");
}
#[test]
fn empty_list_interpolates_as_empty_array() {
let store = store_with_list("ids", &[]);
let args = vec!["{{ids}}".into()];
let result = interpolate_args(&args, &store, "test-op").unwrap();
assert_eq!(result, vec!["[]"]);
}
#[test]
fn undefined_variable_produces_error() {
let store = VariableStore::default();
let args = vec!["{{missing}}".into()];
let result = interpolate_args(&args, &store, "my-op");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("missing"), "expected var name, got: {err}");
assert!(err.contains("my-op"), "expected op id, got: {err}");
}
#[test]
fn no_variables_passthrough() {
let store = VariableStore::default();
let args = vec!["--flag".into(), "value".into()];
let result = interpolate_args(&args, &store, "test-op").unwrap();
assert_eq!(result, vec!["--flag", "value"]);
}
#[test]
fn unclosed_brace_treated_as_literal() {
let store = VariableStore::default();
let args = vec!["{{unclosed".into()];
let result = interpolate_args(&args, &store, "test-op").unwrap();
assert_eq!(result, vec!["{{unclosed"]);
}
#[test]
fn scalar_takes_precedence_over_list() {
let mut store = VariableStore::default();
store.scalars.insert("x".into(), "scalar-val".into());
store.lists.insert("x".into(), vec!["list-val".into()]);
let args = vec!["{{x}}".into()];
let result = interpolate_args(&args, &store, "test-op").unwrap();
assert_eq!(result, vec!["scalar-val"]);
}
}