Skip to main content

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> =
13    LazyLock::new(|| Regex::new(r"\$\{__([A-Z_]+)\}").expect("Invalid placeholder regex"));
14
15/// Supported dynamic parameter placeholders
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum DynamicPlaceholder {
18    /// ${__VU} -> Virtual User ID (1-indexed)
19    VU,
20    /// ${__ITER} -> Iteration count (0-indexed)
21    Iteration,
22    /// ${__TIMESTAMP} -> Current timestamp in milliseconds
23    Timestamp,
24    /// ${__UUID} -> Random UUID
25    UUID,
26    /// ${__RANDOM} -> Random float 0-1
27    Random,
28    /// ${__COUNTER} -> Global incrementing counter
29    Counter,
30    /// ${__DATE} -> Current date in ISO format
31    Date,
32    /// ${__VU_ITER} -> Combined VU and iteration for unique IDs
33    VuIter,
34}
35
36impl DynamicPlaceholder {
37    /// Parse a placeholder name into a variant
38    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    /// Get the k6 JavaScript expression for this placeholder
53    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    /// Check if this placeholder requires additional k6 imports
67    ///
68    /// Note: As of k6 v1.0.0+, webcrypto is globally available and no longer
69    /// requires importing from 'k6/experimental/webcrypto'. The crypto object
70    /// (including crypto.randomUUID()) is available globally.
71    pub fn requires_import(&self) -> Option<&'static str> {
72        // No imports required - crypto is globally available in k6 v1.0.0+
73        None
74    }
75
76    /// Check if this placeholder requires global state initialization
77    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/// Result of processing a value for dynamic placeholders
86#[derive(Debug, Clone)]
87pub struct ProcessedValue {
88    /// The processed value (may be a k6 template literal)
89    pub value: String,
90    /// Whether the value contains dynamic placeholders
91    pub is_dynamic: bool,
92    /// Set of placeholders found in the value
93    pub placeholders: HashSet<DynamicPlaceholder>,
94}
95
96impl ProcessedValue {
97    /// Create a static (non-dynamic) processed value
98    pub fn static_value(value: String) -> Self {
99        Self {
100            value,
101            is_dynamic: false,
102            placeholders: HashSet::new(),
103        }
104    }
105}
106
107/// Result of processing a JSON body
108#[derive(Debug, Clone)]
109pub struct ProcessedBody {
110    /// The processed body as a string (may contain JS expressions)
111    pub value: String,
112    /// Whether the body contains dynamic placeholders
113    pub is_dynamic: bool,
114    /// Set of all placeholders found in the body
115    pub placeholders: HashSet<DynamicPlaceholder>,
116}
117
118/// Processor for dynamic parameter placeholders
119pub struct DynamicParamProcessor;
120
121impl DynamicParamProcessor {
122    /// Check if a string contains any dynamic placeholders
123    pub fn has_dynamic_placeholders(value: &str) -> bool {
124        PLACEHOLDER_REGEX.is_match(value)
125    }
126
127    /// Extract all placeholders from a string
128    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    /// Process a string value, converting placeholders to k6 expressions
143    ///
144    /// Input: "load-test-vu-${__VU}"
145    /// Output: ProcessedValue { value: "`load-test-vu-${__VU}`", is_dynamic: true, ... }
146    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        // Convert placeholders to k6 expressions
154        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        // Wrap in backticks to make it a JS template literal
173        ProcessedValue {
174            value: format!("`{}`", result),
175            is_dynamic: true,
176            placeholders,
177        }
178    }
179
180    /// Process a JSON value recursively, handling dynamic placeholders
181    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                    // Mark as dynamic by wrapping in a special format
190                    // The template will handle rendering this as a JS expression
191                    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            // Other types pass through unchanged
217            _ => value.clone(),
218        };
219
220        (processed, all_placeholders)
221    }
222
223    /// Process an entire JSON body for dynamic placeholders
224    ///
225    /// Returns a JavaScript-ready body string that may contain template literals
226    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        // Convert to a JavaScript-compatible string
231        let value = if is_dynamic {
232            // Generate JavaScript code that builds the object with dynamic values
233            Self::generate_dynamic_body_js(&processed)
234        } else {
235            // Static body - just use JSON serialization
236            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    /// Generate JavaScript code for a body with dynamic values
247    fn generate_dynamic_body_js(value: &Value) -> String {
248        match value {
249            Value::String(s) if s.starts_with("__DYNAMIC__") => {
250                // Remove the __DYNAMIC__ prefix and return the template literal
251                s.strip_prefix("__DYNAMIC__").unwrap_or(s).to_string()
252            }
253            Value::String(s) => {
254                // Regular string - quote it
255                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    /// 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.iter().filter_map(|p| p.requires_import()).collect()
286    }
287
288    /// Get all required global initializations based on placeholders used
289    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        // As of k6 v1.0.0+, webcrypto is globally available
355        // No imports are required for any placeholder
356        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        // As of k6 v1.0.0+, webcrypto is globally available
397        // No imports are required for any placeholder
398        let imports = DynamicParamProcessor::get_required_imports(&placeholders);
399        assert_eq!(imports.len(), 0);
400    }
401}