use regex::Regex;
use serde_json::Value;
use std::collections::HashSet;
use std::sync::LazyLock;
static PLACEHOLDER_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\$\{__([A-Z_]+)\}").expect("Invalid placeholder regex"));
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DynamicPlaceholder {
VU,
Iteration,
Timestamp,
UUID,
Random,
Counter,
Date,
VuIter,
}
impl DynamicPlaceholder {
pub fn from_name(name: &str) -> Option<Self> {
match name {
"VU" => Some(Self::VU),
"ITER" => Some(Self::Iteration),
"TIMESTAMP" => Some(Self::Timestamp),
"UUID" => Some(Self::UUID),
"RANDOM" => Some(Self::Random),
"COUNTER" => Some(Self::Counter),
"DATE" => Some(Self::Date),
"VU_ITER" => Some(Self::VuIter),
_ => None,
}
}
pub fn to_k6_expression(&self) -> &'static str {
match self {
Self::VU => "__VU",
Self::Iteration => "__ITER",
Self::Timestamp => "Date.now()",
Self::UUID => "crypto.randomUUID()",
Self::Random => "Math.random()",
Self::Counter => "globalCounter++",
Self::Date => "new Date().toISOString()",
Self::VuIter => "`${__VU}-${__ITER}`",
}
}
pub fn requires_import(&self) -> Option<&'static str> {
None
}
pub fn requires_global_init(&self) -> Option<&'static str> {
match self {
Self::Counter => Some("let globalCounter = 0;"),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct ProcessedValue {
pub value: String,
pub is_dynamic: bool,
pub placeholders: HashSet<DynamicPlaceholder>,
}
impl ProcessedValue {
pub fn static_value(value: String) -> Self {
Self {
value,
is_dynamic: false,
placeholders: HashSet::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct ProcessedBody {
pub value: String,
pub is_dynamic: bool,
pub placeholders: HashSet<DynamicPlaceholder>,
}
pub struct DynamicParamProcessor;
impl DynamicParamProcessor {
pub fn has_dynamic_placeholders(value: &str) -> bool {
PLACEHOLDER_REGEX.is_match(value)
}
pub fn extract_placeholders(value: &str) -> HashSet<DynamicPlaceholder> {
let mut placeholders = HashSet::new();
for cap in PLACEHOLDER_REGEX.captures_iter(value) {
if let Some(name) = cap.get(1) {
if let Some(placeholder) = DynamicPlaceholder::from_name(name.as_str()) {
placeholders.insert(placeholder);
}
}
}
placeholders
}
pub fn process_value(value: &str) -> ProcessedValue {
let placeholders = Self::extract_placeholders(value);
if placeholders.is_empty() {
return ProcessedValue::static_value(value.to_string());
}
let mut result = value.to_string();
for placeholder in &placeholders {
let pattern = match placeholder {
DynamicPlaceholder::VU => "${__VU}",
DynamicPlaceholder::Iteration => "${__ITER}",
DynamicPlaceholder::Timestamp => "${__TIMESTAMP}",
DynamicPlaceholder::UUID => "${__UUID}",
DynamicPlaceholder::Random => "${__RANDOM}",
DynamicPlaceholder::Counter => "${__COUNTER}",
DynamicPlaceholder::Date => "${__DATE}",
DynamicPlaceholder::VuIter => "${__VU_ITER}",
};
let replacement = format!("${{{}}}", placeholder.to_k6_expression());
result = result.replace(pattern, &replacement);
}
ProcessedValue {
value: format!("`{}`", result),
is_dynamic: true,
placeholders,
}
}
pub fn process_json_value(value: &Value) -> (Value, HashSet<DynamicPlaceholder>) {
let mut all_placeholders = HashSet::new();
let processed = match value {
Value::String(s) => {
let processed = Self::process_value(s);
all_placeholders.extend(processed.placeholders);
if processed.is_dynamic {
Value::String(format!("__DYNAMIC__{}", processed.value))
} else {
Value::String(s.clone())
}
}
Value::Object(map) => {
let mut new_map = serde_json::Map::new();
for (key, val) in map {
let (processed_val, placeholders) = Self::process_json_value(val);
all_placeholders.extend(placeholders);
new_map.insert(key.clone(), processed_val);
}
Value::Object(new_map)
}
Value::Array(arr) => {
let processed_arr: Vec<Value> = arr
.iter()
.map(|v| {
let (processed, placeholders) = Self::process_json_value(v);
all_placeholders.extend(placeholders);
processed
})
.collect();
Value::Array(processed_arr)
}
_ => value.clone(),
};
(processed, all_placeholders)
}
pub fn process_json_body(body: &Value) -> ProcessedBody {
let (processed, placeholders) = Self::process_json_value(body);
let is_dynamic = !placeholders.is_empty();
let value = if is_dynamic {
Self::generate_dynamic_body_js(&processed)
} else {
serde_json::to_string_pretty(&processed).unwrap_or_else(|_| "{}".to_string())
};
ProcessedBody {
value,
is_dynamic,
placeholders,
}
}
fn generate_dynamic_body_js(value: &Value) -> String {
match value {
Value::String(s) if s.starts_with("__DYNAMIC__") => {
s.strip_prefix("__DYNAMIC__").unwrap_or(s).to_string()
}
Value::String(s) => {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}
Value::Object(map) => {
let pairs: Vec<String> = map
.iter()
.map(|(k, v)| {
let key = format!("\"{}\"", k);
let val = Self::generate_dynamic_body_js(v);
format!("{}: {}", key, val)
})
.collect();
format!("{{\n {}\n}}", pairs.join(",\n "))
}
Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(Self::generate_dynamic_body_js).collect();
format!("[{}]", items.join(", "))
}
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => "null".to_string(),
}
}
pub fn process_path(path: &str) -> ProcessedValue {
Self::process_value(path)
}
pub fn get_required_imports(placeholders: &HashSet<DynamicPlaceholder>) -> Vec<&'static str> {
placeholders.iter().filter_map(|p| p.requires_import()).collect()
}
pub fn get_required_globals(placeholders: &HashSet<DynamicPlaceholder>) -> Vec<&'static str> {
placeholders.iter().filter_map(|p| p.requires_global_init()).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_has_dynamic_placeholders() {
assert!(DynamicParamProcessor::has_dynamic_placeholders("test-${__VU}"));
assert!(DynamicParamProcessor::has_dynamic_placeholders("${__ITER}-${__VU}"));
assert!(!DynamicParamProcessor::has_dynamic_placeholders("static-value"));
assert!(!DynamicParamProcessor::has_dynamic_placeholders("${normal_var}"));
}
#[test]
fn test_extract_placeholders() {
let placeholders = DynamicParamProcessor::extract_placeholders("vu-${__VU}-iter-${__ITER}");
assert!(placeholders.contains(&DynamicPlaceholder::VU));
assert!(placeholders.contains(&DynamicPlaceholder::Iteration));
assert_eq!(placeholders.len(), 2);
}
#[test]
fn test_process_value_static() {
let result = DynamicParamProcessor::process_value("static-value");
assert!(!result.is_dynamic);
assert_eq!(result.value, "static-value");
assert!(result.placeholders.is_empty());
}
#[test]
fn test_process_value_dynamic() {
let result = DynamicParamProcessor::process_value("test-${__VU}");
assert!(result.is_dynamic);
assert_eq!(result.value, "`test-${__VU}`");
assert!(result.placeholders.contains(&DynamicPlaceholder::VU));
}
#[test]
fn test_process_value_multiple_placeholders() {
let result = DynamicParamProcessor::process_value("vu-${__VU}-iter-${__ITER}");
assert!(result.is_dynamic);
assert_eq!(result.value, "`vu-${__VU}-iter-${__ITER}`");
assert_eq!(result.placeholders.len(), 2);
}
#[test]
fn test_process_value_timestamp() {
let result = DynamicParamProcessor::process_value("created-${__TIMESTAMP}");
assert!(result.is_dynamic);
assert!(result.value.contains("Date.now()"));
}
#[test]
fn test_process_value_uuid() {
let result = DynamicParamProcessor::process_value("id-${__UUID}");
assert!(result.is_dynamic);
assert!(result.value.contains("crypto.randomUUID()"));
}
#[test]
fn test_placeholder_requires_import() {
assert!(DynamicPlaceholder::UUID.requires_import().is_none());
assert!(DynamicPlaceholder::VU.requires_import().is_none());
assert!(DynamicPlaceholder::Iteration.requires_import().is_none());
}
#[test]
fn test_placeholder_requires_global() {
assert!(DynamicPlaceholder::Counter.requires_global_init().is_some());
assert!(DynamicPlaceholder::VU.requires_global_init().is_none());
}
#[test]
fn test_process_json_body_static() {
let body = serde_json::json!({
"name": "test",
"count": 42
});
let result = DynamicParamProcessor::process_json_body(&body);
assert!(!result.is_dynamic);
assert!(result.placeholders.is_empty());
}
#[test]
fn test_process_json_body_dynamic() {
let body = serde_json::json!({
"name": "test-${__VU}",
"id": "${__UUID}"
});
let result = DynamicParamProcessor::process_json_body(&body);
assert!(result.is_dynamic);
assert!(result.placeholders.contains(&DynamicPlaceholder::VU));
assert!(result.placeholders.contains(&DynamicPlaceholder::UUID));
}
#[test]
fn test_get_required_imports() {
let mut placeholders = HashSet::new();
placeholders.insert(DynamicPlaceholder::UUID);
placeholders.insert(DynamicPlaceholder::VU);
let imports = DynamicParamProcessor::get_required_imports(&placeholders);
assert_eq!(imports.len(), 0);
}
}