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> {
72 None
74 }
75
76 pub fn requires_global_init(&self) -> Option<&'static str> {
78 match self {
79 Self::Counter => Some("let globalCounter = 0;"),
80 _ => None,
81 }
82 }
83}
84
85#[derive(Debug, Clone)]
87pub struct ProcessedValue {
88 pub value: String,
90 pub is_dynamic: bool,
92 pub placeholders: HashSet<DynamicPlaceholder>,
94}
95
96impl ProcessedValue {
97 pub fn static_value(value: String) -> Self {
99 Self {
100 value,
101 is_dynamic: false,
102 placeholders: HashSet::new(),
103 }
104 }
105}
106
107#[derive(Debug, Clone)]
109pub struct ProcessedBody {
110 pub value: String,
112 pub is_dynamic: bool,
114 pub placeholders: HashSet<DynamicPlaceholder>,
116}
117
118pub struct DynamicParamProcessor;
120
121impl DynamicParamProcessor {
122 pub fn has_dynamic_placeholders(value: &str) -> bool {
124 PLACEHOLDER_REGEX.is_match(value)
125 }
126
127 pub fn extract_placeholders(value: &str) -> HashSet<DynamicPlaceholder> {
129 let mut placeholders = HashSet::new();
130
131 for cap in PLACEHOLDER_REGEX.captures_iter(value) {
132 if let Some(name) = cap.get(1) {
133 if let Some(placeholder) = DynamicPlaceholder::from_name(name.as_str()) {
134 placeholders.insert(placeholder);
135 }
136 }
137 }
138
139 placeholders
140 }
141
142 pub fn process_value(value: &str) -> ProcessedValue {
147 let placeholders = Self::extract_placeholders(value);
148
149 if placeholders.is_empty() {
150 return ProcessedValue::static_value(value.to_string());
151 }
152
153 let mut result = value.to_string();
155
156 for placeholder in &placeholders {
157 let pattern = match placeholder {
158 DynamicPlaceholder::VU => "${__VU}",
159 DynamicPlaceholder::Iteration => "${__ITER}",
160 DynamicPlaceholder::Timestamp => "${__TIMESTAMP}",
161 DynamicPlaceholder::UUID => "${__UUID}",
162 DynamicPlaceholder::Random => "${__RANDOM}",
163 DynamicPlaceholder::Counter => "${__COUNTER}",
164 DynamicPlaceholder::Date => "${__DATE}",
165 DynamicPlaceholder::VuIter => "${__VU_ITER}",
166 };
167
168 let replacement = format!("${{{}}}", placeholder.to_k6_expression());
169 result = result.replace(pattern, &replacement);
170 }
171
172 ProcessedValue {
174 value: format!("`{}`", result),
175 is_dynamic: true,
176 placeholders,
177 }
178 }
179
180 pub fn process_json_value(value: &Value) -> (Value, HashSet<DynamicPlaceholder>) {
182 let mut all_placeholders = HashSet::new();
183
184 let processed = match value {
185 Value::String(s) => {
186 let processed = Self::process_value(s);
187 all_placeholders.extend(processed.placeholders);
188 if processed.is_dynamic {
189 Value::String(format!("__DYNAMIC__{}", processed.value))
192 } else {
193 Value::String(s.clone())
194 }
195 }
196 Value::Object(map) => {
197 let mut new_map = serde_json::Map::new();
198 for (key, val) in map {
199 let (processed_val, placeholders) = Self::process_json_value(val);
200 all_placeholders.extend(placeholders);
201 new_map.insert(key.clone(), processed_val);
202 }
203 Value::Object(new_map)
204 }
205 Value::Array(arr) => {
206 let processed_arr: Vec<Value> = arr
207 .iter()
208 .map(|v| {
209 let (processed, placeholders) = Self::process_json_value(v);
210 all_placeholders.extend(placeholders);
211 processed
212 })
213 .collect();
214 Value::Array(processed_arr)
215 }
216 _ => value.clone(),
218 };
219
220 (processed, all_placeholders)
221 }
222
223 pub fn process_json_body(body: &Value) -> ProcessedBody {
227 let (processed, placeholders) = Self::process_json_value(body);
228 let is_dynamic = !placeholders.is_empty();
229
230 let value = if is_dynamic {
232 Self::generate_dynamic_body_js(&processed)
234 } else {
235 serde_json::to_string_pretty(&processed).unwrap_or_else(|_| "{}".to_string())
237 };
238
239 ProcessedBody {
240 value,
241 is_dynamic,
242 placeholders,
243 }
244 }
245
246 fn generate_dynamic_body_js(value: &Value) -> String {
248 match value {
249 Value::String(s) if s.starts_with("__DYNAMIC__") => {
250 s.strip_prefix("__DYNAMIC__").unwrap_or(s).to_string()
252 }
253 Value::String(s) => {
254 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
256 }
257 Value::Object(map) => {
258 let pairs: Vec<String> = map
259 .iter()
260 .map(|(k, v)| {
261 let key = format!("\"{}\"", k);
262 let val = Self::generate_dynamic_body_js(v);
263 format!("{}: {}", key, val)
264 })
265 .collect();
266 format!("{{\n {}\n}}", pairs.join(",\n "))
267 }
268 Value::Array(arr) => {
269 let items: Vec<String> = 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.iter().filter_map(|p| p.requires_import()).collect()
286 }
287
288 pub fn get_required_globals(placeholders: &HashSet<DynamicPlaceholder>) -> Vec<&'static str> {
290 placeholders.iter().filter_map(|p| p.requires_global_init()).collect()
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn test_has_dynamic_placeholders() {
300 assert!(DynamicParamProcessor::has_dynamic_placeholders("test-${__VU}"));
301 assert!(DynamicParamProcessor::has_dynamic_placeholders("${__ITER}-${__VU}"));
302 assert!(!DynamicParamProcessor::has_dynamic_placeholders("static-value"));
303 assert!(!DynamicParamProcessor::has_dynamic_placeholders("${normal_var}"));
304 }
305
306 #[test]
307 fn test_extract_placeholders() {
308 let placeholders = DynamicParamProcessor::extract_placeholders("vu-${__VU}-iter-${__ITER}");
309 assert!(placeholders.contains(&DynamicPlaceholder::VU));
310 assert!(placeholders.contains(&DynamicPlaceholder::Iteration));
311 assert_eq!(placeholders.len(), 2);
312 }
313
314 #[test]
315 fn test_process_value_static() {
316 let result = DynamicParamProcessor::process_value("static-value");
317 assert!(!result.is_dynamic);
318 assert_eq!(result.value, "static-value");
319 assert!(result.placeholders.is_empty());
320 }
321
322 #[test]
323 fn test_process_value_dynamic() {
324 let result = DynamicParamProcessor::process_value("test-${__VU}");
325 assert!(result.is_dynamic);
326 assert_eq!(result.value, "`test-${__VU}`");
327 assert!(result.placeholders.contains(&DynamicPlaceholder::VU));
328 }
329
330 #[test]
331 fn test_process_value_multiple_placeholders() {
332 let result = DynamicParamProcessor::process_value("vu-${__VU}-iter-${__ITER}");
333 assert!(result.is_dynamic);
334 assert_eq!(result.value, "`vu-${__VU}-iter-${__ITER}`");
335 assert_eq!(result.placeholders.len(), 2);
336 }
337
338 #[test]
339 fn test_process_value_timestamp() {
340 let result = DynamicParamProcessor::process_value("created-${__TIMESTAMP}");
341 assert!(result.is_dynamic);
342 assert!(result.value.contains("Date.now()"));
343 }
344
345 #[test]
346 fn test_process_value_uuid() {
347 let result = DynamicParamProcessor::process_value("id-${__UUID}");
348 assert!(result.is_dynamic);
349 assert!(result.value.contains("crypto.randomUUID()"));
350 }
351
352 #[test]
353 fn test_placeholder_requires_import() {
354 assert!(DynamicPlaceholder::UUID.requires_import().is_none());
357 assert!(DynamicPlaceholder::VU.requires_import().is_none());
358 assert!(DynamicPlaceholder::Iteration.requires_import().is_none());
359 }
360
361 #[test]
362 fn test_placeholder_requires_global() {
363 assert!(DynamicPlaceholder::Counter.requires_global_init().is_some());
364 assert!(DynamicPlaceholder::VU.requires_global_init().is_none());
365 }
366
367 #[test]
368 fn test_process_json_body_static() {
369 let body = serde_json::json!({
370 "name": "test",
371 "count": 42
372 });
373 let result = DynamicParamProcessor::process_json_body(&body);
374 assert!(!result.is_dynamic);
375 assert!(result.placeholders.is_empty());
376 }
377
378 #[test]
379 fn test_process_json_body_dynamic() {
380 let body = serde_json::json!({
381 "name": "test-${__VU}",
382 "id": "${__UUID}"
383 });
384 let result = DynamicParamProcessor::process_json_body(&body);
385 assert!(result.is_dynamic);
386 assert!(result.placeholders.contains(&DynamicPlaceholder::VU));
387 assert!(result.placeholders.contains(&DynamicPlaceholder::UUID));
388 }
389
390 #[test]
391 fn test_get_required_imports() {
392 let mut placeholders = HashSet::new();
393 placeholders.insert(DynamicPlaceholder::UUID);
394 placeholders.insert(DynamicPlaceholder::VU);
395
396 let imports = DynamicParamProcessor::get_required_imports(&placeholders);
399 assert_eq!(imports.len(), 0);
400 }
401}