mockforge_core/
templating.rs

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