1use regex::Regex;
7use serde_json::Value;
8use std::collections::HashSet;
9use std::sync::LazyLock;
10
11static PLACEHOLDER_REGEX: LazyLock<Regex> = LazyLock::new(|| {
13 Regex::new(r"\$\{__([A-Z_]+)\}").expect("Invalid placeholder regex")
14});
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum DynamicPlaceholder {
19 VU,
21 Iteration,
23 Timestamp,
25 UUID,
27 Random,
29 Counter,
31 Date,
33 VuIter,
35}
36
37impl DynamicPlaceholder {
38 pub fn from_name(name: &str) -> Option<Self> {
40 match name {
41 "VU" => Some(Self::VU),
42 "ITER" => Some(Self::Iteration),
43 "TIMESTAMP" => Some(Self::Timestamp),
44 "UUID" => Some(Self::UUID),
45 "RANDOM" => Some(Self::Random),
46 "COUNTER" => Some(Self::Counter),
47 "DATE" => Some(Self::Date),
48 "VU_ITER" => Some(Self::VuIter),
49 _ => None,
50 }
51 }
52
53 pub fn to_k6_expression(&self) -> &'static str {
55 match self {
56 Self::VU => "__VU",
57 Self::Iteration => "__ITER",
58 Self::Timestamp => "Date.now()",
59 Self::UUID => "crypto.randomUUID()",
60 Self::Random => "Math.random()",
61 Self::Counter => "globalCounter++",
62 Self::Date => "new Date().toISOString()",
63 Self::VuIter => "`${__VU}-${__ITER}`",
64 }
65 }
66
67 pub fn requires_import(&self) -> Option<&'static str> {
69 match self {
70 Self::UUID => Some("import { crypto } from 'k6/experimental/webcrypto';"),
71 _ => None,
72 }
73 }
74
75 pub fn requires_global_init(&self) -> Option<&'static str> {
77 match self {
78 Self::Counter => Some("let globalCounter = 0;"),
79 _ => None,
80 }
81 }
82}
83
84#[derive(Debug, Clone)]
86pub struct ProcessedValue {
87 pub value: String,
89 pub is_dynamic: bool,
91 pub placeholders: HashSet<DynamicPlaceholder>,
93}
94
95impl ProcessedValue {
96 pub fn static_value(value: String) -> Self {
98 Self {
99 value,
100 is_dynamic: false,
101 placeholders: HashSet::new(),
102 }
103 }
104}
105
106#[derive(Debug, Clone)]
108pub struct ProcessedBody {
109 pub value: String,
111 pub is_dynamic: bool,
113 pub placeholders: HashSet<DynamicPlaceholder>,
115}
116
117pub struct DynamicParamProcessor;
119
120impl DynamicParamProcessor {
121 pub fn has_dynamic_placeholders(value: &str) -> bool {
123 PLACEHOLDER_REGEX.is_match(value)
124 }
125
126 pub fn extract_placeholders(value: &str) -> HashSet<DynamicPlaceholder> {
128 let mut placeholders = HashSet::new();
129
130 for cap in PLACEHOLDER_REGEX.captures_iter(value) {
131 if let Some(name) = cap.get(1) {
132 if let Some(placeholder) = DynamicPlaceholder::from_name(name.as_str()) {
133 placeholders.insert(placeholder);
134 }
135 }
136 }
137
138 placeholders
139 }
140
141 pub fn process_value(value: &str) -> ProcessedValue {
146 let placeholders = Self::extract_placeholders(value);
147
148 if placeholders.is_empty() {
149 return ProcessedValue::static_value(value.to_string());
150 }
151
152 let mut result = value.to_string();
154
155 for placeholder in &placeholders {
156 let pattern = match placeholder {
157 DynamicPlaceholder::VU => "${__VU}",
158 DynamicPlaceholder::Iteration => "${__ITER}",
159 DynamicPlaceholder::Timestamp => "${__TIMESTAMP}",
160 DynamicPlaceholder::UUID => "${__UUID}",
161 DynamicPlaceholder::Random => "${__RANDOM}",
162 DynamicPlaceholder::Counter => "${__COUNTER}",
163 DynamicPlaceholder::Date => "${__DATE}",
164 DynamicPlaceholder::VuIter => "${__VU_ITER}",
165 };
166
167 let replacement = format!("${{{}}}", placeholder.to_k6_expression());
168 result = result.replace(pattern, &replacement);
169 }
170
171 ProcessedValue {
173 value: format!("`{}`", result),
174 is_dynamic: true,
175 placeholders,
176 }
177 }
178
179 pub fn process_json_value(value: &Value) -> (Value, HashSet<DynamicPlaceholder>) {
181 let mut all_placeholders = HashSet::new();
182
183 let processed = match value {
184 Value::String(s) => {
185 let processed = Self::process_value(s);
186 all_placeholders.extend(processed.placeholders);
187 if processed.is_dynamic {
188 Value::String(format!("__DYNAMIC__{}", processed.value))
191 } else {
192 Value::String(s.clone())
193 }
194 }
195 Value::Object(map) => {
196 let mut new_map = serde_json::Map::new();
197 for (key, val) in map {
198 let (processed_val, placeholders) = Self::process_json_value(val);
199 all_placeholders.extend(placeholders);
200 new_map.insert(key.clone(), processed_val);
201 }
202 Value::Object(new_map)
203 }
204 Value::Array(arr) => {
205 let processed_arr: Vec<Value> = arr
206 .iter()
207 .map(|v| {
208 let (processed, placeholders) = Self::process_json_value(v);
209 all_placeholders.extend(placeholders);
210 processed
211 })
212 .collect();
213 Value::Array(processed_arr)
214 }
215 _ => value.clone(),
217 };
218
219 (processed, all_placeholders)
220 }
221
222 pub fn process_json_body(body: &Value) -> ProcessedBody {
226 let (processed, placeholders) = Self::process_json_value(body);
227 let is_dynamic = !placeholders.is_empty();
228
229 let value = if is_dynamic {
231 Self::generate_dynamic_body_js(&processed)
233 } else {
234 serde_json::to_string_pretty(&processed).unwrap_or_else(|_| "{}".to_string())
236 };
237
238 ProcessedBody {
239 value,
240 is_dynamic,
241 placeholders,
242 }
243 }
244
245 fn generate_dynamic_body_js(value: &Value) -> String {
247 match value {
248 Value::String(s) if s.starts_with("__DYNAMIC__") => {
249 s.strip_prefix("__DYNAMIC__").unwrap_or(s).to_string()
251 }
252 Value::String(s) => {
253 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
255 }
256 Value::Object(map) => {
257 let pairs: Vec<String> = map
258 .iter()
259 .map(|(k, v)| {
260 let key = format!("\"{}\"", k);
261 let val = Self::generate_dynamic_body_js(v);
262 format!("{}: {}", key, val)
263 })
264 .collect();
265 format!("{{\n {}\n}}", pairs.join(",\n "))
266 }
267 Value::Array(arr) => {
268 let items: Vec<String> =
269 arr.iter().map(Self::generate_dynamic_body_js).collect();
270 format!("[{}]", items.join(", "))
271 }
272 Value::Number(n) => n.to_string(),
273 Value::Bool(b) => b.to_string(),
274 Value::Null => "null".to_string(),
275 }
276 }
277
278 pub fn process_path(path: &str) -> ProcessedValue {
280 Self::process_value(path)
281 }
282
283 pub fn get_required_imports(placeholders: &HashSet<DynamicPlaceholder>) -> Vec<&'static str> {
285 placeholders
286 .iter()
287 .filter_map(|p| p.requires_import())
288 .collect()
289 }
290
291 pub fn get_required_globals(placeholders: &HashSet<DynamicPlaceholder>) -> Vec<&'static str> {
293 placeholders
294 .iter()
295 .filter_map(|p| p.requires_global_init())
296 .collect()
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_has_dynamic_placeholders() {
306 assert!(DynamicParamProcessor::has_dynamic_placeholders(
307 "test-${__VU}"
308 ));
309 assert!(DynamicParamProcessor::has_dynamic_placeholders(
310 "${__ITER}-${__VU}"
311 ));
312 assert!(!DynamicParamProcessor::has_dynamic_placeholders(
313 "static-value"
314 ));
315 assert!(!DynamicParamProcessor::has_dynamic_placeholders(
316 "${normal_var}"
317 ));
318 }
319
320 #[test]
321 fn test_extract_placeholders() {
322 let placeholders =
323 DynamicParamProcessor::extract_placeholders("vu-${__VU}-iter-${__ITER}");
324 assert!(placeholders.contains(&DynamicPlaceholder::VU));
325 assert!(placeholders.contains(&DynamicPlaceholder::Iteration));
326 assert_eq!(placeholders.len(), 2);
327 }
328
329 #[test]
330 fn test_process_value_static() {
331 let result = DynamicParamProcessor::process_value("static-value");
332 assert!(!result.is_dynamic);
333 assert_eq!(result.value, "static-value");
334 assert!(result.placeholders.is_empty());
335 }
336
337 #[test]
338 fn test_process_value_dynamic() {
339 let result = DynamicParamProcessor::process_value("test-${__VU}");
340 assert!(result.is_dynamic);
341 assert_eq!(result.value, "`test-${__VU}`");
342 assert!(result.placeholders.contains(&DynamicPlaceholder::VU));
343 }
344
345 #[test]
346 fn test_process_value_multiple_placeholders() {
347 let result = DynamicParamProcessor::process_value("vu-${__VU}-iter-${__ITER}");
348 assert!(result.is_dynamic);
349 assert_eq!(result.value, "`vu-${__VU}-iter-${__ITER}`");
350 assert_eq!(result.placeholders.len(), 2);
351 }
352
353 #[test]
354 fn test_process_value_timestamp() {
355 let result = DynamicParamProcessor::process_value("created-${__TIMESTAMP}");
356 assert!(result.is_dynamic);
357 assert!(result.value.contains("Date.now()"));
358 }
359
360 #[test]
361 fn test_process_value_uuid() {
362 let result = DynamicParamProcessor::process_value("id-${__UUID}");
363 assert!(result.is_dynamic);
364 assert!(result.value.contains("crypto.randomUUID()"));
365 }
366
367 #[test]
368 fn test_placeholder_requires_import() {
369 assert!(DynamicPlaceholder::UUID.requires_import().is_some());
370 assert!(DynamicPlaceholder::VU.requires_import().is_none());
371 assert!(DynamicPlaceholder::Iteration.requires_import().is_none());
372 }
373
374 #[test]
375 fn test_placeholder_requires_global() {
376 assert!(DynamicPlaceholder::Counter.requires_global_init().is_some());
377 assert!(DynamicPlaceholder::VU.requires_global_init().is_none());
378 }
379
380 #[test]
381 fn test_process_json_body_static() {
382 let body = serde_json::json!({
383 "name": "test",
384 "count": 42
385 });
386 let result = DynamicParamProcessor::process_json_body(&body);
387 assert!(!result.is_dynamic);
388 assert!(result.placeholders.is_empty());
389 }
390
391 #[test]
392 fn test_process_json_body_dynamic() {
393 let body = serde_json::json!({
394 "name": "test-${__VU}",
395 "id": "${__UUID}"
396 });
397 let result = DynamicParamProcessor::process_json_body(&body);
398 assert!(result.is_dynamic);
399 assert!(result.placeholders.contains(&DynamicPlaceholder::VU));
400 assert!(result.placeholders.contains(&DynamicPlaceholder::UUID));
401 }
402
403 #[test]
404 fn test_get_required_imports() {
405 let mut placeholders = HashSet::new();
406 placeholders.insert(DynamicPlaceholder::UUID);
407 placeholders.insert(DynamicPlaceholder::VU);
408
409 let imports = DynamicParamProcessor::get_required_imports(&placeholders);
410 assert_eq!(imports.len(), 1);
411 assert!(imports[0].contains("webcrypto"));
412 }
413}