mockforge_bench/
dynamic_params.rs

1//! Dynamic parameter placeholder processing for k6 script generation
2//!
3//! This module handles placeholders like `${__VU}`, `${__ITER}`, `${__TIMESTAMP}`, and `${__UUID}`
4//! that are replaced with k6 runtime expressions in the generated scripts.
5
6use regex::Regex;
7use serde_json::Value;
8use std::collections::HashSet;
9use std::sync::LazyLock;
10
11/// Regex pattern for dynamic placeholders: ${__VU}, ${__ITER}, ${__TIMESTAMP}, ${__UUID}, ${__RANDOM}, ${__COUNTER}
12static PLACEHOLDER_REGEX: LazyLock<Regex> = LazyLock::new(|| {
13    Regex::new(r"\$\{__([A-Z_]+)\}").expect("Invalid placeholder regex")
14});
15
16/// Supported dynamic parameter placeholders
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum DynamicPlaceholder {
19    /// ${__VU} -> Virtual User ID (1-indexed)
20    VU,
21    /// ${__ITER} -> Iteration count (0-indexed)
22    Iteration,
23    /// ${__TIMESTAMP} -> Current timestamp in milliseconds
24    Timestamp,
25    /// ${__UUID} -> Random UUID
26    UUID,
27    /// ${__RANDOM} -> Random float 0-1
28    Random,
29    /// ${__COUNTER} -> Global incrementing counter
30    Counter,
31    /// ${__DATE} -> Current date in ISO format
32    Date,
33    /// ${__VU_ITER} -> Combined VU and iteration for unique IDs
34    VuIter,
35}
36
37impl DynamicPlaceholder {
38    /// Parse a placeholder name into a variant
39    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    /// Get the k6 JavaScript expression for this placeholder
54    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    /// Check if this placeholder requires additional k6 imports
68    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    /// Check if this placeholder requires global state initialization
76    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/// Result of processing a value for dynamic placeholders
85#[derive(Debug, Clone)]
86pub struct ProcessedValue {
87    /// The processed value (may be a k6 template literal)
88    pub value: String,
89    /// Whether the value contains dynamic placeholders
90    pub is_dynamic: bool,
91    /// Set of placeholders found in the value
92    pub placeholders: HashSet<DynamicPlaceholder>,
93}
94
95impl ProcessedValue {
96    /// Create a static (non-dynamic) processed value
97    pub fn static_value(value: String) -> Self {
98        Self {
99            value,
100            is_dynamic: false,
101            placeholders: HashSet::new(),
102        }
103    }
104}
105
106/// Result of processing a JSON body
107#[derive(Debug, Clone)]
108pub struct ProcessedBody {
109    /// The processed body as a string (may contain JS expressions)
110    pub value: String,
111    /// Whether the body contains dynamic placeholders
112    pub is_dynamic: bool,
113    /// Set of all placeholders found in the body
114    pub placeholders: HashSet<DynamicPlaceholder>,
115}
116
117/// Processor for dynamic parameter placeholders
118pub struct DynamicParamProcessor;
119
120impl DynamicParamProcessor {
121    /// Check if a string contains any dynamic placeholders
122    pub fn has_dynamic_placeholders(value: &str) -> bool {
123        PLACEHOLDER_REGEX.is_match(value)
124    }
125
126    /// Extract all placeholders from a string
127    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    /// Process a string value, converting placeholders to k6 expressions
142    ///
143    /// Input: "load-test-vu-${__VU}"
144    /// Output: ProcessedValue { value: "`load-test-vu-${__VU}`", is_dynamic: true, ... }
145    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        // Convert placeholders to k6 expressions
153        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        // Wrap in backticks to make it a JS template literal
172        ProcessedValue {
173            value: format!("`{}`", result),
174            is_dynamic: true,
175            placeholders,
176        }
177    }
178
179    /// Process a JSON value recursively, handling dynamic placeholders
180    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                    // Mark as dynamic by wrapping in a special format
189                    // The template will handle rendering this as a JS expression
190                    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            // Other types pass through unchanged
216            _ => value.clone(),
217        };
218
219        (processed, all_placeholders)
220    }
221
222    /// Process an entire JSON body for dynamic placeholders
223    ///
224    /// Returns a JavaScript-ready body string that may contain template literals
225    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        // Convert to a JavaScript-compatible string
230        let value = if is_dynamic {
231            // Generate JavaScript code that builds the object with dynamic values
232            Self::generate_dynamic_body_js(&processed)
233        } else {
234            // Static body - just use JSON serialization
235            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    /// Generate JavaScript code for a body with dynamic values
246    fn generate_dynamic_body_js(value: &Value) -> String {
247        match value {
248            Value::String(s) if s.starts_with("__DYNAMIC__") => {
249                // Remove the __DYNAMIC__ prefix and return the template literal
250                s.strip_prefix("__DYNAMIC__").unwrap_or(s).to_string()
251            }
252            Value::String(s) => {
253                // Regular string - quote it
254                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    /// Process a URL path for dynamic placeholders
279    pub fn process_path(path: &str) -> ProcessedValue {
280        Self::process_value(path)
281    }
282
283    /// Get all required imports based on placeholders used
284    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    /// Get all required global initializations based on placeholders used
292    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}