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