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    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    /// Check if this placeholder requires global state initialization
75    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/// Result of processing a value for dynamic placeholders
84#[derive(Debug, Clone)]
85pub struct ProcessedValue {
86    /// The processed value (may be a k6 template literal)
87    pub value: String,
88    /// Whether the value contains dynamic placeholders
89    pub is_dynamic: bool,
90    /// Set of placeholders found in the value
91    pub placeholders: HashSet<DynamicPlaceholder>,
92}
93
94impl ProcessedValue {
95    /// Create a static (non-dynamic) processed value
96    pub fn static_value(value: String) -> Self {
97        Self {
98            value,
99            is_dynamic: false,
100            placeholders: HashSet::new(),
101        }
102    }
103}
104
105/// Result of processing a JSON body
106#[derive(Debug, Clone)]
107pub struct ProcessedBody {
108    /// The processed body as a string (may contain JS expressions)
109    pub value: String,
110    /// Whether the body contains dynamic placeholders
111    pub is_dynamic: bool,
112    /// Set of all placeholders found in the body
113    pub placeholders: HashSet<DynamicPlaceholder>,
114}
115
116/// Processor for dynamic parameter placeholders
117pub struct DynamicParamProcessor;
118
119impl DynamicParamProcessor {
120    /// Check if a string contains any dynamic placeholders
121    pub fn has_dynamic_placeholders(value: &str) -> bool {
122        PLACEHOLDER_REGEX.is_match(value)
123    }
124
125    /// Extract all placeholders from a string
126    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    /// Process a string value, converting placeholders to k6 expressions
141    ///
142    /// Input: "load-test-vu-${__VU}"
143    /// Output: ProcessedValue { value: "`load-test-vu-${__VU}`", is_dynamic: true, ... }
144    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        // Convert placeholders to k6 expressions
152        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        // Wrap in backticks to make it a JS template literal
171        ProcessedValue {
172            value: format!("`{}`", result),
173            is_dynamic: true,
174            placeholders,
175        }
176    }
177
178    /// Process a JSON value recursively, handling dynamic placeholders
179    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                    // Mark as dynamic by wrapping in a special format
188                    // The template will handle rendering this as a JS expression
189                    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            // Other types pass through unchanged
215            _ => value.clone(),
216        };
217
218        (processed, all_placeholders)
219    }
220
221    /// Process an entire JSON body for dynamic placeholders
222    ///
223    /// Returns a JavaScript-ready body string that may contain template literals
224    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        // Convert to a JavaScript-compatible string
229        let value = if is_dynamic {
230            // Generate JavaScript code that builds the object with dynamic values
231            Self::generate_dynamic_body_js(&processed)
232        } else {
233            // Static body - just use JSON serialization
234            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    /// Generate JavaScript code for a body with dynamic values
245    fn generate_dynamic_body_js(value: &Value) -> String {
246        match value {
247            Value::String(s) if s.starts_with("__DYNAMIC__") => {
248                // Remove the __DYNAMIC__ prefix and return the template literal
249                s.strip_prefix("__DYNAMIC__").unwrap_or(s).to_string()
250            }
251            Value::String(s) => {
252                // Regular string - quote it
253                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    /// Process a URL path for dynamic placeholders
277    pub fn process_path(path: &str) -> ProcessedValue {
278        Self::process_value(path)
279    }
280
281    /// Get all required imports based on placeholders used
282    pub fn get_required_imports(placeholders: &HashSet<DynamicPlaceholder>) -> Vec<&'static str> {
283        placeholders.iter().filter_map(|p| p.requires_import()).collect()
284    }
285
286    /// Get all required global initializations based on placeholders used
287    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}