mockforge_core/
templating.rs

1//! Extended templating system for MockForge with request chaining support
2//!
3//! This module provides template expansion with support for:
4//! - Standard tokens (UUID, timestamps, random data, faker)
5//! - Request chaining context variables
6//! - End-to-end encryption functions
7
8use crate::encryption::init_key_store;
9use crate::request_chaining::ChainTemplatingContext;
10use crate::time_travel::VirtualClock;
11use crate::Config;
12use chrono::{Duration as ChronoDuration, Utc};
13use once_cell::sync::{Lazy, OnceCell};
14use rand::{rng, Rng};
15use regex::Regex;
16use serde_json::Value;
17use std::collections::HashMap;
18use std::sync::Arc;
19
20// Pre-compiled regex patterns for templating
21static RANDINT_RE: Lazy<Regex> = Lazy::new(|| {
22    Regex::new(r"\{\{\s*(?:randInt|rand\.int)\s+(-?\d+)\s+(-?\d+)\s*\}\}")
23        .expect("RANDINT_RE regex pattern is valid")
24});
25
26static NOW_OFFSET_RE: Lazy<Regex> = Lazy::new(|| {
27    Regex::new(r"\{\{\s*now\s*([+-])\s*(\d+)\s*([smhd])\s*\}\}")
28        .expect("NOW_OFFSET_RE regex pattern is valid")
29});
30
31static ENV_TOKEN_RE: Lazy<Regex> = Lazy::new(|| {
32    Regex::new(r"\{\{\s*([^{}\s]+)\s*\}\}").expect("ENV_TOKEN_RE regex pattern is valid")
33});
34
35static CHAIN_TOKEN_RE: Lazy<Regex> = Lazy::new(|| {
36    Regex::new(r"\{\{\s*chain\.([^}]+)\s*\}\}").expect("CHAIN_TOKEN_RE regex pattern is valid")
37});
38
39static RESPONSE_FN_RE: Lazy<Regex> = Lazy::new(|| {
40    Regex::new(r#"response\s*\(\s*['"]([^'"]*)['"]\s*,\s*['"]([^'"]*)['"]\s*\)"#)
41        .expect("RESPONSE_FN_RE regex pattern is valid")
42});
43
44static ENCRYPT_RE: Lazy<Regex> = Lazy::new(|| {
45    Regex::new(r#"\{\{\s*encrypt\s+(?:([^\s}]+)\s+)?\s*"([^"]+)"\s*\}\}"#)
46        .expect("ENCRYPT_RE regex pattern is valid")
47});
48
49static SECURE_RE: Lazy<Regex> = Lazy::new(|| {
50    Regex::new(r#"\{\{\s*secure\s+(?:([^\s}]+)\s+)?\s*"([^"]+)"\s*\}\}"#)
51        .expect("SECURE_RE regex pattern is valid")
52});
53
54static DECRYPT_RE: Lazy<Regex> = Lazy::new(|| {
55    Regex::new(r#"\{\{\s*decrypt\s+(?:([^\s}]+)\s+)?\s*"([^"]+)"\s*\}\}"#)
56        .expect("DECRYPT_RE regex pattern is valid")
57});
58
59static FS_READFILE_RE: Lazy<Regex> = Lazy::new(|| {
60    Regex::new(r#"\{\{\s*fs\.readFile\s*(?:\(?\s*(?:'([^']*)'|"([^"]*)")\s*\)?)?\s*\}\}"#)
61        .expect("FS_READFILE_RE regex pattern is valid")
62});
63
64/// Template engine for processing template strings with various token types
65#[derive(Debug, Clone)]
66pub struct TemplateEngine {
67    /// Configuration for the template engine
68    _config: Config,
69}
70
71impl Default for TemplateEngine {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl TemplateEngine {
78    /// Create a new template engine
79    pub fn new() -> Self {
80        Self {
81            _config: Config::default(),
82        }
83    }
84
85    /// Create a new template engine with configuration
86    pub fn new_with_config(
87        config: Config,
88    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
89        Ok(Self { _config: config })
90    }
91
92    /// Expand templating tokens in a string
93    pub fn expand_str(&self, input: &str) -> String {
94        expand_str(input)
95    }
96
97    /// Expand templating tokens in a string with context
98    pub fn expand_str_with_context(&self, input: &str, context: &TemplatingContext) -> String {
99        expand_str_with_context(input, context)
100    }
101
102    /// Expand templating tokens in a JSON value
103    pub fn expand_tokens(&self, value: &Value) -> Value {
104        expand_tokens(value)
105    }
106
107    /// Expand templating tokens in a JSON value with context
108    pub fn expand_tokens_with_context(&self, value: &Value, context: &TemplatingContext) -> Value {
109        expand_tokens_with_context(value, context)
110    }
111}
112
113/// Context for environment variables during template expansion
114#[derive(Debug, Clone)]
115pub struct EnvironmentTemplatingContext {
116    /// Map of environment variable names to values
117    pub variables: HashMap<String, String>,
118}
119
120impl EnvironmentTemplatingContext {
121    /// Create a new environment context
122    pub fn new(variables: HashMap<String, String>) -> Self {
123        Self { variables }
124    }
125
126    /// Get a variable value by name
127    pub fn get_variable(&self, name: &str) -> Option<&String> {
128        self.variables.get(name)
129    }
130}
131
132/// Combined templating context with chain, environment, and time variables
133#[derive(Debug, Clone)]
134pub struct TemplatingContext {
135    /// Request chaining context for accessing previous request responses
136    pub chain_context: Option<ChainTemplatingContext>,
137    /// Environment variable context for variable substitution
138    pub env_context: Option<EnvironmentTemplatingContext>,
139    /// Virtual clock for time-based template tokens
140    pub virtual_clock: Option<Arc<VirtualClock>>,
141}
142
143impl TemplatingContext {
144    /// Create empty context
145    pub fn empty() -> Self {
146        Self {
147            chain_context: None,
148            env_context: None,
149            virtual_clock: None,
150        }
151    }
152
153    /// Create context with environment variables only
154    pub fn with_env(variables: HashMap<String, String>) -> Self {
155        Self {
156            chain_context: None,
157            env_context: Some(EnvironmentTemplatingContext::new(variables)),
158            virtual_clock: None,
159        }
160    }
161
162    /// Create context with chain context only
163    pub fn with_chain(chain_context: ChainTemplatingContext) -> Self {
164        Self {
165            chain_context: Some(chain_context),
166            env_context: None,
167            virtual_clock: None,
168        }
169    }
170
171    /// Create context with both chain and environment contexts
172    pub fn with_both(
173        chain_context: ChainTemplatingContext,
174        variables: HashMap<String, String>,
175    ) -> Self {
176        Self {
177            chain_context: Some(chain_context),
178            env_context: Some(EnvironmentTemplatingContext::new(variables)),
179            virtual_clock: None,
180        }
181    }
182
183    /// Create context with virtual clock
184    pub fn with_virtual_clock(clock: Arc<VirtualClock>) -> Self {
185        Self {
186            chain_context: None,
187            env_context: None,
188            virtual_clock: Some(clock),
189        }
190    }
191
192    /// Add virtual clock to existing context
193    pub fn with_clock(mut self, clock: Arc<VirtualClock>) -> Self {
194        self.virtual_clock = Some(clock);
195        self
196    }
197}
198
199/// Expand templating tokens in a JSON value recursively
200///
201/// Processes all string values in the JSON structure and expands template tokens
202/// like `{{uuid}}`, `{{now}}`, `{{faker.email}}`, etc.
203///
204/// # Arguments
205/// * `v` - JSON value to process
206///
207/// # Returns
208/// New JSON value with all template tokens expanded
209pub fn expand_tokens(v: &Value) -> Value {
210    expand_tokens_with_context(v, &TemplatingContext::empty())
211}
212
213/// Expand templating tokens in a JSON value recursively with context
214///
215/// Similar to `expand_tokens` but uses the provided context for chain variables,
216/// environment variables, and virtual clock.
217///
218/// # Arguments
219/// * `v` - JSON value to process
220/// * `context` - Templating context with chain, environment, and time information
221///
222/// # Returns
223/// New JSON value with all template tokens expanded
224pub fn expand_tokens_with_context(v: &Value, context: &TemplatingContext) -> Value {
225    match v {
226        Value::String(s) => Value::String(expand_str_with_context(s, context)),
227        Value::Array(a) => {
228            Value::Array(a.iter().map(|item| expand_tokens_with_context(item, context)).collect())
229        }
230        Value::Object(o) => {
231            let mut map = serde_json::Map::new();
232            for (k, vv) in o {
233                map.insert(k.clone(), expand_tokens_with_context(vv, context));
234            }
235            Value::Object(map)
236        }
237        _ => v.clone(),
238    }
239}
240
241/// Expand templating tokens in a string
242///
243/// Processes template tokens in the input string and replaces them with generated values.
244/// Supports UUID, timestamps, random numbers, faker data, environment variables, and more.
245///
246/// # Arguments
247/// * `input` - String containing template tokens (e.g., "{{uuid}}", "{{now}}")
248///
249/// # Returns
250/// String with all template tokens replaced
251pub fn expand_str(input: &str) -> String {
252    expand_str_with_context(input, &TemplatingContext::empty())
253}
254
255/// Expand templating tokens in a string with templating context
256///
257/// Similar to `expand_str` but uses the provided context for chain variables,
258/// environment variables, and virtual clock operations.
259///
260/// # Arguments
261/// * `input` - String containing template tokens
262/// * `context` - Templating context with chain, environment, and time information
263///
264/// # Returns
265/// String with all template tokens replaced
266pub fn expand_str_with_context(input: &str, context: &TemplatingContext) -> String {
267    // Basic replacements first (fast paths)
268    let mut out = input.replace("{{uuid}}", &uuid::Uuid::new_v4().to_string());
269
270    // Use virtual clock if available, otherwise use real time
271    let current_time = if let Some(clock) = &context.virtual_clock {
272        clock.now()
273    } else {
274        Utc::now()
275    };
276    out = out.replace("{{now}}", &current_time.to_rfc3339());
277
278    // now±Nd (days), now±Nh (hours), now±Nm (minutes), now±Ns (seconds)
279    out = replace_now_offset_with_time(&out, current_time);
280
281    // Randoms
282    if out.contains("{{rand.int}}") {
283        let n: i64 = rng().random_range(0..=1_000_000);
284        out = out.replace("{{rand.int}}", &n.to_string());
285    }
286    if out.contains("{{rand.float}}") {
287        let n: f64 = rng().random();
288        out = out.replace("{{rand.float}}", &format!("{:.6}", n));
289    }
290    out = replace_randint_ranges(&out);
291
292    // Response function tokens (new response() syntax)
293    if out.contains("response(") {
294        out = replace_response_function(&out, context.chain_context.as_ref());
295    }
296
297    // Environment variables (check before chain context to allow env vars in chain expressions)
298    if out.contains("{{") {
299        if let Some(env_ctx) = context.env_context.as_ref() {
300            out = replace_env_tokens(&out, env_ctx);
301        }
302    }
303
304    // Chain context variables
305    if out.contains("{{chain.") {
306        out = replace_chain_tokens(&out, context.chain_context.as_ref());
307    }
308
309    // Faker tokens (can be disabled for determinism)
310    let faker_enabled = std::env::var("MOCKFORGE_FAKE_TOKENS")
311        .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
312        .unwrap_or(true);
313    if faker_enabled {
314        out = replace_faker_tokens(&out);
315    }
316
317    // File system tokens
318    if out.contains("{{fs.readFile") {
319        out = replace_fs_tokens(&out);
320    }
321
322    // Encryption tokens
323    if out.contains("{{encrypt") || out.contains("{{decrypt") || out.contains("{{secure") {
324        out = replace_encryption_tokens(&out);
325    }
326
327    out
328}
329
330// Provider wiring (optional)
331static FAKER_PROVIDER: OnceCell<Arc<dyn FakerProvider + Send + Sync>> = OnceCell::new();
332
333/// Provider trait for generating fake data in templates
334///
335/// Implement this trait to customize how fake data is generated for template tokens
336/// like `{{faker.email}}`, `{{faker.name}}`, etc.
337pub trait FakerProvider {
338    /// Generate a random UUID
339    fn uuid(&self) -> String {
340        uuid::Uuid::new_v4().to_string()
341    }
342    /// Generate a fake email address
343    fn email(&self) -> String {
344        format!("user{}@example.com", rng().random_range(1000..=9999))
345    }
346    /// Generate a fake person name
347    fn name(&self) -> String {
348        "Alex Smith".to_string()
349    }
350    /// Generate a fake street address
351    fn address(&self) -> String {
352        "1 Main St".to_string()
353    }
354    /// Generate a fake phone number
355    fn phone(&self) -> String {
356        "+1-555-0100".to_string()
357    }
358    /// Generate a fake company name
359    fn company(&self) -> String {
360        "Example Inc".to_string()
361    }
362    /// Generate a fake URL
363    fn url(&self) -> String {
364        "https://example.com".to_string()
365    }
366    /// Generate a fake IP address
367    fn ip(&self) -> String {
368        "192.168.1.1".to_string()
369    }
370    /// Generate a fake color name
371    fn color(&self) -> String {
372        "blue".to_string()
373    }
374    /// Generate a fake word
375    fn word(&self) -> String {
376        "word".to_string()
377    }
378    /// Generate a fake sentence
379    fn sentence(&self) -> String {
380        "A sample sentence.".to_string()
381    }
382    /// Generate a fake paragraph
383    fn paragraph(&self) -> String {
384        "A sample paragraph.".to_string()
385    }
386}
387
388/// Register a custom faker provider for template token generation
389///
390/// This allows you to replace the default faker implementation with a custom one
391/// that can generate more realistic or customized fake data.
392///
393/// # Arguments
394/// * `provider` - Custom faker provider implementation
395pub fn register_faker_provider(provider: Arc<dyn FakerProvider + Send + Sync>) {
396    let _ = FAKER_PROVIDER.set(provider);
397}
398
399fn replace_randint_ranges(input: &str) -> String {
400    // Supports {{randInt a b}} and {{rand.int a b}}
401    let mut s = input.to_string();
402    loop {
403        let mat = RANDINT_RE.captures(&s);
404        if let Some(caps) = mat {
405            let a: i64 = caps.get(1).map(|m| m.as_str().parse().unwrap_or(0)).unwrap_or(0);
406            let b: i64 = caps.get(2).map(|m| m.as_str().parse().unwrap_or(100)).unwrap_or(100);
407            let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
408            let n: i64 = rng().random_range(lo..=hi);
409            s = RANDINT_RE.replace(&s, n.to_string()).to_string();
410        } else {
411            break;
412        }
413    }
414    s
415}
416
417/// Replace `{{ now+1d }}` style templates in input string
418///
419/// This is a convenience wrapper around `replace_now_offset_with_time` that uses the current time.
420/// Currently kept for future templating enhancements.
421///
422/// TODO: Integrate into full templating system when date/time placeholders are implemented
423#[allow(dead_code)] // TODO: Remove when date/time template feature is implemented
424fn replace_now_offset(input: &str) -> String {
425    replace_now_offset_with_time(input, Utc::now())
426}
427
428fn replace_now_offset_with_time(input: &str, current_time: chrono::DateTime<Utc>) -> String {
429    // {{ now+1d }}, {{now-2h}}, {{now+30m}}, {{now-10s}}
430    NOW_OFFSET_RE
431        .replace_all(input, |caps: &regex::Captures| {
432            let sign = caps.get(1).map(|m| m.as_str()).unwrap_or("+");
433            let amount: i64 = caps.get(2).map(|m| m.as_str().parse().unwrap_or(0)).unwrap_or(0);
434            let unit = caps.get(3).map(|m| m.as_str()).unwrap_or("d");
435            let dur = match unit {
436                "s" => ChronoDuration::seconds(amount),
437                "m" => ChronoDuration::minutes(amount),
438                "h" => ChronoDuration::hours(amount),
439                _ => ChronoDuration::days(amount),
440            };
441            let ts = if sign == "+" {
442                current_time + dur
443            } else {
444                current_time - dur
445            };
446            ts.to_rfc3339()
447        })
448        .to_string()
449}
450
451/// Replace environment variable tokens in a template string
452fn replace_env_tokens(input: &str, env_context: &EnvironmentTemplatingContext) -> String {
453    ENV_TOKEN_RE
454        .replace_all(input, |caps: &regex::Captures| {
455            let var_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
456
457            // Skip built-in tokens (uuid, now, rand.*, faker.*, chain.*, encrypt.*, decrypt.*, secure.*)
458            if matches!(var_name, "uuid" | "now")
459                || var_name.starts_with("rand.")
460                || var_name.starts_with("faker.")
461                || var_name.starts_with("chain.")
462                || var_name.starts_with("encrypt")
463                || var_name.starts_with("decrypt")
464                || var_name.starts_with("secure")
465            {
466                return caps.get(0).map(|m| m.as_str().to_string()).unwrap_or_default();
467            }
468
469            // Look up the variable in environment context
470            match env_context.get_variable(var_name) {
471                Some(value) => value.clone(),
472                None => format!("{{{{{}}}}}", var_name), // Keep original if not found
473            }
474        })
475        .to_string()
476}
477
478/// Replace chain context tokens in a template string
479fn replace_chain_tokens(input: &str, chain_context: Option<&ChainTemplatingContext>) -> String {
480    if let Some(context) = chain_context {
481        CHAIN_TOKEN_RE
482            .replace_all(input, |caps: &regex::Captures| {
483                let path = caps.get(1).map(|m| m.as_str()).unwrap_or("");
484
485                match context.extract_value(path) {
486                    Some(Value::String(s)) => s,
487                    Some(Value::Number(n)) => n.to_string(),
488                    Some(Value::Bool(b)) => b.to_string(),
489                    Some(val) => serde_json::to_string(&val).unwrap_or_else(|_| "null".to_string()),
490                    None => "null".to_string(), // Return null for missing values instead of empty string
491                }
492            })
493            .to_string()
494    } else {
495        // No chain context available, return input unchanged
496        input.to_string()
497    }
498}
499
500/// Replace response function tokens (new response() syntax)
501fn replace_response_function(
502    input: &str,
503    chain_context: Option<&ChainTemplatingContext>,
504) -> String {
505    // Match response('request_id', 'jsonpath') - handle both single and double quotes
506    if let Some(context) = chain_context {
507        let result = RESPONSE_FN_RE
508            .replace_all(input, |caps: &regex::Captures| {
509                let request_id = caps.get(1).map(|m| m.as_str()).unwrap_or("");
510                let json_path = caps.get(2).map(|m| m.as_str()).unwrap_or("");
511
512                // Build the full path like "request_id.json_path"
513                let full_path = if json_path.is_empty() {
514                    request_id.to_string()
515                } else {
516                    format!("{}.{}", request_id, json_path)
517                };
518
519                match context.extract_value(&full_path) {
520                    Some(Value::String(s)) => s,
521                    Some(Value::Number(n)) => n.to_string(),
522                    Some(Value::Bool(b)) => b.to_string(),
523                    Some(val) => serde_json::to_string(&val).unwrap_or_else(|_| "null".to_string()),
524                    None => "null".to_string(), // Return null for missing values
525                }
526            })
527            .to_string();
528
529        result
530    } else {
531        // No chain context available, return input unchanged
532        input.to_string()
533    }
534}
535
536/// Replace encryption tokens in a template string
537fn replace_encryption_tokens(input: &str) -> String {
538    // Key store is initialized at startup
539    let key_store = init_key_store();
540
541    // Default key ID for templating
542    let default_key_id = "mockforge_default";
543
544    let mut out = input.to_string();
545
546    // Process encrypt tokens
547    out = ENCRYPT_RE
548        .replace_all(&out, |caps: &regex::Captures| {
549            let key_id = caps.get(1).map(|m| m.as_str()).unwrap_or(default_key_id);
550            let plaintext = caps.get(2).map(|m| m.as_str()).unwrap_or("");
551
552            match key_store.get_key(key_id) {
553                Some(key) => match key.encrypt(plaintext, None) {
554                    Ok(ciphertext) => ciphertext,
555                    Err(_) => "<encryption_error>".to_string(),
556                },
557                None => {
558                    // Create a default key if none exists
559                    let password = std::env::var("MOCKFORGE_ENCRYPTION_KEY")
560                        .unwrap_or_else(|_| "mockforge_default_encryption_key_2024".to_string());
561                    match crate::encryption::EncryptionKey::from_password_pbkdf2(
562                        &password,
563                        None,
564                        crate::encryption::EncryptionAlgorithm::Aes256Gcm,
565                    ) {
566                        Ok(key) => match key.encrypt(plaintext, None) {
567                            Ok(ciphertext) => ciphertext,
568                            Err(_) => "<encryption_error>".to_string(),
569                        },
570                        Err(_) => "<key_creation_error>".to_string(),
571                    }
572                }
573            }
574        })
575        .to_string();
576
577    // Process secure tokens (ChaCha20-Poly1305)
578    out = SECURE_RE
579        .replace_all(&out, |caps: &regex::Captures| {
580            let key_id = caps.get(1).map(|m| m.as_str()).unwrap_or(default_key_id);
581            let plaintext = caps.get(2).map(|m| m.as_str()).unwrap_or("");
582
583            match key_store.get_key(key_id) {
584                Some(key) => {
585                    // Use ChaCha20-Poly1305 for secure() function
586                    match key.encrypt_chacha20(plaintext, None) {
587                        Ok(ciphertext) => ciphertext,
588                        Err(_) => "<encryption_error>".to_string(),
589                    }
590                }
591                None => {
592                    // Create a default key if none exists
593                    let password = std::env::var("MOCKFORGE_ENCRYPTION_KEY")
594                        .unwrap_or_else(|_| "mockforge_default_encryption_key_2024".to_string());
595                    match crate::encryption::EncryptionKey::from_password_pbkdf2(
596                        &password,
597                        None,
598                        crate::encryption::EncryptionAlgorithm::ChaCha20Poly1305,
599                    ) {
600                        Ok(key) => match key.encrypt_chacha20(plaintext, None) {
601                            Ok(ciphertext) => ciphertext,
602                            Err(_) => "<encryption_error>".to_string(),
603                        },
604                        Err(_) => "<key_creation_error>".to_string(),
605                    }
606                }
607            }
608        })
609        .to_string();
610
611    // Process decrypt tokens
612    out = DECRYPT_RE
613        .replace_all(&out, |caps: &regex::Captures| {
614            let key_id = caps.get(1).map(|m| m.as_str()).unwrap_or(default_key_id);
615            let ciphertext = caps.get(2).map(|m| m.as_str()).unwrap_or("");
616
617            match key_store.get_key(key_id) {
618                Some(key) => match key.decrypt(ciphertext, None) {
619                    Ok(plaintext) => plaintext,
620                    Err(_) => "<decryption_error>".to_string(),
621                },
622                None => {
623                    // Create a default key if none exists
624                    let password = std::env::var("MOCKFORGE_ENCRYPTION_KEY")
625                        .unwrap_or_else(|_| "mockforge_default_encryption_key_2024".to_string());
626                    match crate::encryption::EncryptionKey::from_password_pbkdf2(
627                        &password,
628                        None,
629                        crate::encryption::EncryptionAlgorithm::Aes256Gcm,
630                    ) {
631                        Ok(key) => match key.decrypt(ciphertext, None) {
632                            Ok(plaintext) => plaintext,
633                            Err(_) => "<decryption_error>".to_string(),
634                        },
635                        Err(_) => "<key_creation_error>".to_string(),
636                    }
637                }
638            }
639        })
640        .to_string();
641
642    out
643}
644
645/// Replace file system tokens in a template string
646fn replace_fs_tokens(input: &str) -> String {
647    // Handle {{fs.readFile "path/to/file"}} or {{fs.readFile('path/to/file')}}
648    FS_READFILE_RE
649        .replace_all(input, |caps: &regex::Captures| {
650            let file_path = caps.get(1).or_else(|| caps.get(2)).map(|m| m.as_str()).unwrap_or("");
651
652            if file_path.is_empty() {
653                return "<fs.readFile: empty path>".to_string();
654            }
655
656            match std::fs::read_to_string(file_path) {
657                Ok(content) => content,
658                Err(e) => format!("<fs.readFile error: {}>", e),
659            }
660        })
661        .to_string()
662}
663
664fn replace_faker_tokens(input: &str) -> String {
665    // If a provider is registered (e.g., from mockforge-data), use it; else fallback
666    if let Some(provider) = FAKER_PROVIDER.get() {
667        return replace_with_provider(input, provider.as_ref());
668    }
669    replace_with_fallback(input)
670}
671
672fn replace_with_provider(input: &str, p: &dyn FakerProvider) -> String {
673    let mut out = input.to_string();
674    let map = [
675        ("{{faker.uuid}}", p.uuid()),
676        ("{{faker.email}}", p.email()),
677        ("{{faker.name}}", p.name()),
678        ("{{faker.address}}", p.address()),
679        ("{{faker.phone}}", p.phone()),
680        ("{{faker.company}}", p.company()),
681        ("{{faker.url}}", p.url()),
682        ("{{faker.ip}}", p.ip()),
683        ("{{faker.color}}", p.color()),
684        ("{{faker.word}}", p.word()),
685        ("{{faker.sentence}}", p.sentence()),
686        ("{{faker.paragraph}}", p.paragraph()),
687    ];
688    for (pat, val) in map {
689        if out.contains(pat) {
690            out = out.replace(pat, &val);
691        }
692    }
693    out
694}
695
696fn replace_with_fallback(input: &str) -> String {
697    let mut out = input.to_string();
698    if out.contains("{{faker.uuid}}") {
699        out = out.replace("{{faker.uuid}}", &uuid::Uuid::new_v4().to_string());
700    }
701    if out.contains("{{faker.email}}") {
702        let user: String = (0..8).map(|_| (b'a' + (rng().random::<u8>() % 26)) as char).collect();
703        let dom: String = (0..6).map(|_| (b'a' + (rng().random::<u8>() % 26)) as char).collect();
704        out = out.replace("{{faker.email}}", &format!("{}@{}.example", user, dom));
705    }
706    if out.contains("{{faker.name}}") {
707        let firsts = ["Alex", "Sam", "Taylor", "Jordan", "Casey", "Riley"];
708        let lasts = ["Smith", "Lee", "Patel", "Garcia", "Kim", "Brown"];
709        let fi = rng().random::<u8>() as usize % firsts.len();
710        let li = rng().random::<u8>() as usize % lasts.len();
711        out = out.replace("{{faker.name}}", &format!("{} {}", firsts[fi], lasts[li]));
712    }
713    out
714}
715
716#[cfg(test)]
717mod tests {
718    use super::*;
719    use crate::request_chaining::{ChainContext, ChainResponse, ChainTemplatingContext};
720    use serde_json::json;
721
722    #[test]
723    fn test_expand_str_with_context() {
724        let chain_context = ChainTemplatingContext::new(ChainContext::new());
725        let context = TemplatingContext::with_chain(chain_context);
726        let result = expand_str_with_context("{{uuid}}", &context);
727        assert!(!result.is_empty());
728    }
729
730    #[test]
731    fn test_replace_env_tokens() {
732        let mut vars = HashMap::new();
733        vars.insert("api_key".to_string(), "secret123".to_string());
734        let env_context = EnvironmentTemplatingContext::new(vars);
735        let result = replace_env_tokens("{{api_key}}", &env_context);
736        assert_eq!(result, "secret123");
737    }
738
739    #[test]
740    fn test_replace_chain_tokens() {
741        let chain_ctx = ChainContext::new();
742        let template_ctx = ChainTemplatingContext::new(chain_ctx);
743        let context = Some(&template_ctx);
744        // Note: This test would need a proper response stored in the chain context
745        let result = replace_chain_tokens("{{chain.test.body}}", context);
746        assert_eq!(result, "null");
747    }
748
749    #[test]
750    fn test_response_function() {
751        // Test with no chain context
752        let result = replace_response_function(r#"response('login', 'body.user_id')"#, None);
753        assert_eq!(result, r#"response('login', 'body.user_id')"#);
754
755        // Test with chain context but no matching response
756        let chain_ctx = ChainContext::new();
757        let template_ctx = ChainTemplatingContext::new(chain_ctx);
758        let context = Some(&template_ctx);
759        let result = replace_response_function(r#"response('login', 'body.user_id')"#, context);
760        assert_eq!(result, "null");
761
762        // Test with stored response
763        let mut chain_ctx = ChainContext::new();
764        let response = ChainResponse {
765            status: 200,
766            headers: HashMap::new(),
767            body: Some(json!({"user_id": 12345})),
768            duration_ms: 150,
769            executed_at: "2023-01-01T00:00:00Z".to_string(),
770            error: None,
771        };
772        chain_ctx.store_response("login".to_string(), response);
773        let template_ctx = ChainTemplatingContext::new(chain_ctx);
774        let context = Some(&template_ctx);
775
776        let result = replace_response_function(r#"response('login', 'user_id')"#, context);
777        assert_eq!(result, "12345");
778    }
779
780    #[test]
781    fn test_fs_readfile() {
782        // Create a temporary file for testing
783        use std::fs;
784
785        let temp_file = "/tmp/mockforge_test_file.txt";
786        let test_content = "Hello, this is test content!";
787        fs::write(temp_file, test_content).unwrap();
788
789        // Test successful file reading
790        let template = format!(r#"{{{{fs.readFile "{}"}}}}"#, temp_file);
791        let result = expand_str(&template);
792        assert_eq!(result, test_content);
793
794        // Test with parentheses
795        let template = format!(r#"{{{{fs.readFile('{}')}}}}"#, temp_file);
796        let result = expand_str(&template);
797        assert_eq!(result, test_content);
798
799        // Test file not found
800        let template = r#"{{fs.readFile "/nonexistent/file.txt"}}"#;
801        let result = expand_str(template);
802        assert!(result.contains("fs.readFile error:"));
803
804        // Test empty path
805        let template = r#"{{fs.readFile ""}}"#;
806        let result = expand_str(template);
807        assert_eq!(result, "<fs.readFile: empty path>");
808
809        // Clean up
810        let _ = fs::remove_file(temp_file);
811    }
812}