Skip to main content

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::{thread_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    // Early return if no template tokens present (common case optimization)
270    if !input.contains("{{") {
271        return input.to_string();
272    }
273
274    let mut out = input.to_string();
275
276    // Basic replacements first (fast paths) - only if tokens are present
277    if out.contains("{{uuid}}") {
278        out = out.replace("{{uuid}}", &uuid::Uuid::new_v4().to_string());
279    }
280
281    // Only get current time if we need it (for {{now}} or time offsets)
282    let needs_time = out.contains("{{now}}") || NOW_OFFSET_RE.is_match(&out);
283    let current_time = if needs_time {
284        if let Some(clock) = &context.virtual_clock {
285            Some(clock.now())
286        } else {
287            Some(Utc::now())
288        }
289    } else {
290        None
291    };
292
293    if let Some(time) = current_time {
294        if out.contains("{{now}}") {
295            out = out.replace("{{now}}", &time.to_rfc3339());
296        }
297        // now±Nd (days), now±Nh (hours), now±Nm (minutes), now±Ns (seconds)
298        if NOW_OFFSET_RE.is_match(&out) {
299            out = replace_now_offset_with_time(&out, time);
300        }
301    }
302
303    // Randoms - only process if tokens are present
304    if out.contains("{{rand.int}}") {
305        let n: i64 = thread_rng().random_range(0..=1_000_000);
306        out = out.replace("{{rand.int}}", &n.to_string());
307    }
308    if out.contains("{{rand.float}}") {
309        let n: f64 = thread_rng().random();
310        out = out.replace("{{rand.float}}", &format!("{:.6}", n));
311    }
312    if RANDINT_RE.is_match(&out) {
313        out = replace_randint_ranges(&out);
314    }
315
316    // Response function tokens (new response() syntax)
317    if out.contains("response(") {
318        out = replace_response_function(&out, context.chain_context.as_ref());
319    }
320
321    // Environment variables (check before chain context to allow env vars in chain expressions)
322    if out.contains("{{") {
323        if let Some(env_ctx) = context.env_context.as_ref() {
324            out = replace_env_tokens(&out, env_ctx);
325        }
326    }
327
328    // Chain context variables
329    if out.contains("{{chain.") {
330        out = replace_chain_tokens(&out, context.chain_context.as_ref());
331    }
332
333    // Faker tokens (can be disabled for determinism)
334    // Cache the environment variable check using OnceCell for better performance
335    static FAKER_ENABLED: Lazy<bool> = Lazy::new(|| {
336        std::env::var("MOCKFORGE_FAKE_TOKENS")
337            .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
338            .unwrap_or(true)
339    });
340    if *FAKER_ENABLED && out.contains("{{faker.") {
341        out = replace_faker_tokens(&out);
342    }
343
344    // File system tokens
345    if out.contains("{{fs.readFile") {
346        out = replace_fs_tokens(&out);
347    }
348
349    // Encryption tokens
350    if out.contains("{{encrypt") || out.contains("{{decrypt") || out.contains("{{secure") {
351        out = replace_encryption_tokens(&out);
352    }
353
354    out
355}
356
357// Provider wiring (optional)
358static FAKER_PROVIDER: OnceCell<Arc<dyn FakerProvider + Send + Sync>> = OnceCell::new();
359
360/// Provider trait for generating fake data in templates
361///
362/// Implement this trait to customize how fake data is generated for template tokens
363/// like `{{faker.email}}`, `{{faker.name}}`, etc.
364pub trait FakerProvider {
365    /// Generate a random UUID
366    fn uuid(&self) -> String {
367        uuid::Uuid::new_v4().to_string()
368    }
369    /// Generate a fake email address
370    fn email(&self) -> String {
371        format!("user{}@example.com", thread_rng().random_range(1000..=9999))
372    }
373    /// Generate a fake person name
374    fn name(&self) -> String {
375        "Alex Smith".to_string()
376    }
377    /// Generate a fake street address
378    fn address(&self) -> String {
379        "1 Main St".to_string()
380    }
381    /// Generate a fake phone number
382    fn phone(&self) -> String {
383        "+1-555-0100".to_string()
384    }
385    /// Generate a fake company name
386    fn company(&self) -> String {
387        "Example Inc".to_string()
388    }
389    /// Generate a fake URL
390    fn url(&self) -> String {
391        "https://example.com".to_string()
392    }
393    /// Generate a fake IP address
394    fn ip(&self) -> String {
395        "192.168.1.1".to_string()
396    }
397    /// Generate a fake color name
398    fn color(&self) -> String {
399        "blue".to_string()
400    }
401    /// Generate a fake word
402    fn word(&self) -> String {
403        "word".to_string()
404    }
405    /// Generate a fake sentence
406    fn sentence(&self) -> String {
407        "A sample sentence.".to_string()
408    }
409    /// Generate a fake paragraph
410    fn paragraph(&self) -> String {
411        "A sample paragraph.".to_string()
412    }
413}
414
415/// Register a custom faker provider for template token generation
416///
417/// This allows you to replace the default faker implementation with a custom one
418/// that can generate more realistic or customized fake data.
419///
420/// # Arguments
421/// * `provider` - Custom faker provider implementation
422pub fn register_faker_provider(provider: Arc<dyn FakerProvider + Send + Sync>) {
423    let _ = FAKER_PROVIDER.set(provider);
424}
425
426fn replace_randint_ranges(input: &str) -> String {
427    // Supports {{randInt a b}} and {{rand.int a b}}
428    let mut s = input.to_string();
429    loop {
430        let mat = RANDINT_RE.captures(&s);
431        if let Some(caps) = mat {
432            let a: i64 = caps.get(1).map(|m| m.as_str().parse().unwrap_or(0)).unwrap_or(0);
433            let b: i64 = caps.get(2).map(|m| m.as_str().parse().unwrap_or(100)).unwrap_or(100);
434            let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
435            let n: i64 = thread_rng().random_range(lo..=hi);
436            s = RANDINT_RE.replace(&s, n.to_string()).to_string();
437        } else {
438            break;
439        }
440    }
441    s
442}
443
444fn replace_now_offset_with_time(input: &str, current_time: chrono::DateTime<Utc>) -> String {
445    // {{ now+1d }}, {{now-2h}}, {{now+30m}}, {{now-10s}}
446    NOW_OFFSET_RE
447        .replace_all(input, |caps: &regex::Captures| {
448            let sign = caps.get(1).map(|m| m.as_str()).unwrap_or("+");
449            let amount: i64 = caps.get(2).map(|m| m.as_str().parse().unwrap_or(0)).unwrap_or(0);
450            let unit = caps.get(3).map(|m| m.as_str()).unwrap_or("d");
451            let dur = match unit {
452                "s" => ChronoDuration::seconds(amount),
453                "m" => ChronoDuration::minutes(amount),
454                "h" => ChronoDuration::hours(amount),
455                _ => ChronoDuration::days(amount),
456            };
457            let ts = if sign == "+" {
458                current_time + dur
459            } else {
460                current_time - dur
461            };
462            ts.to_rfc3339()
463        })
464        .to_string()
465}
466
467/// Replace environment variable tokens in a template string
468fn replace_env_tokens(input: &str, env_context: &EnvironmentTemplatingContext) -> String {
469    ENV_TOKEN_RE
470        .replace_all(input, |caps: &regex::Captures| {
471            let var_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
472
473            // Skip built-in tokens (uuid, now, rand.*, faker.*, chain.*, encrypt.*, decrypt.*, secure.*)
474            if matches!(var_name, "uuid" | "now")
475                || var_name.starts_with("rand.")
476                || var_name.starts_with("faker.")
477                || var_name.starts_with("chain.")
478                || var_name.starts_with("encrypt")
479                || var_name.starts_with("decrypt")
480                || var_name.starts_with("secure")
481            {
482                return caps.get(0).map(|m| m.as_str().to_string()).unwrap_or_default();
483            }
484
485            // Look up the variable in environment context
486            match env_context.get_variable(var_name) {
487                Some(value) => value.clone(),
488                None => format!("{{{{{}}}}}", var_name), // Keep original if not found
489            }
490        })
491        .to_string()
492}
493
494/// Replace chain context tokens in a template string
495fn replace_chain_tokens(input: &str, chain_context: Option<&ChainTemplatingContext>) -> String {
496    if let Some(context) = chain_context {
497        CHAIN_TOKEN_RE
498            .replace_all(input, |caps: &regex::Captures| {
499                let path = caps.get(1).map(|m| m.as_str()).unwrap_or("");
500
501                match context.extract_value(path) {
502                    Some(Value::String(s)) => s,
503                    Some(Value::Number(n)) => n.to_string(),
504                    Some(Value::Bool(b)) => b.to_string(),
505                    Some(val) => serde_json::to_string(&val).unwrap_or_else(|_| "null".to_string()),
506                    None => "null".to_string(), // Return null for missing values instead of empty string
507                }
508            })
509            .to_string()
510    } else {
511        // No chain context available, return input unchanged
512        input.to_string()
513    }
514}
515
516/// Replace response function tokens (new response() syntax)
517fn replace_response_function(
518    input: &str,
519    chain_context: Option<&ChainTemplatingContext>,
520) -> String {
521    // Match response('request_id', 'jsonpath') - handle both single and double quotes
522    if let Some(context) = chain_context {
523        let result = RESPONSE_FN_RE
524            .replace_all(input, |caps: &regex::Captures| {
525                let request_id = caps.get(1).map(|m| m.as_str()).unwrap_or("");
526                let json_path = caps.get(2).map(|m| m.as_str()).unwrap_or("");
527
528                // Build the full path like "request_id.json_path"
529                let full_path = if json_path.is_empty() {
530                    request_id.to_string()
531                } else {
532                    format!("{}.{}", request_id, json_path)
533                };
534
535                match context.extract_value(&full_path) {
536                    Some(Value::String(s)) => s,
537                    Some(Value::Number(n)) => n.to_string(),
538                    Some(Value::Bool(b)) => b.to_string(),
539                    Some(val) => serde_json::to_string(&val).unwrap_or_else(|_| "null".to_string()),
540                    None => "null".to_string(), // Return null for missing values
541                }
542            })
543            .to_string();
544
545        result
546    } else {
547        // No chain context available, return input unchanged
548        input.to_string()
549    }
550}
551
552/// Replace encryption tokens in a template string
553fn replace_encryption_tokens(input: &str) -> String {
554    // Key store is initialized at startup
555    let key_store = init_key_store();
556
557    // Default key ID for templating
558    let default_key_id = "mockforge_default";
559
560    let mut out = input.to_string();
561
562    // Process encrypt tokens
563    out = ENCRYPT_RE
564        .replace_all(&out, |caps: &regex::Captures| {
565            let key_id = caps.get(1).map(|m| m.as_str()).unwrap_or(default_key_id);
566            let plaintext = caps.get(2).map(|m| m.as_str()).unwrap_or("");
567
568            match key_store.get_key(key_id) {
569                Some(key) => match key.encrypt(plaintext, None) {
570                    Ok(ciphertext) => ciphertext,
571                    Err(_) => "<encryption_error>".to_string(),
572                },
573                None => {
574                    // Create a default key if none exists
575                    let password = std::env::var("MOCKFORGE_ENCRYPTION_KEY")
576                        .unwrap_or_else(|_| "mockforge_default_encryption_key_2024".to_string());
577                    match crate::encryption::EncryptionKey::from_password_pbkdf2(
578                        &password,
579                        None,
580                        crate::encryption::EncryptionAlgorithm::Aes256Gcm,
581                    ) {
582                        Ok(key) => match key.encrypt(plaintext, None) {
583                            Ok(ciphertext) => ciphertext,
584                            Err(_) => "<encryption_error>".to_string(),
585                        },
586                        Err(_) => "<key_creation_error>".to_string(),
587                    }
588                }
589            }
590        })
591        .to_string();
592
593    // Process secure tokens (ChaCha20-Poly1305)
594    out = SECURE_RE
595        .replace_all(&out, |caps: &regex::Captures| {
596            let key_id = caps.get(1).map(|m| m.as_str()).unwrap_or(default_key_id);
597            let plaintext = caps.get(2).map(|m| m.as_str()).unwrap_or("");
598
599            match key_store.get_key(key_id) {
600                Some(key) => {
601                    // Use ChaCha20-Poly1305 for secure() function
602                    match key.encrypt_chacha20(plaintext, None) {
603                        Ok(ciphertext) => ciphertext,
604                        Err(_) => "<encryption_error>".to_string(),
605                    }
606                }
607                None => {
608                    // Create a default key if none exists
609                    let password = std::env::var("MOCKFORGE_ENCRYPTION_KEY")
610                        .unwrap_or_else(|_| "mockforge_default_encryption_key_2024".to_string());
611                    match crate::encryption::EncryptionKey::from_password_pbkdf2(
612                        &password,
613                        None,
614                        crate::encryption::EncryptionAlgorithm::ChaCha20Poly1305,
615                    ) {
616                        Ok(key) => match key.encrypt_chacha20(plaintext, None) {
617                            Ok(ciphertext) => ciphertext,
618                            Err(_) => "<encryption_error>".to_string(),
619                        },
620                        Err(_) => "<key_creation_error>".to_string(),
621                    }
622                }
623            }
624        })
625        .to_string();
626
627    // Process decrypt tokens
628    out = DECRYPT_RE
629        .replace_all(&out, |caps: &regex::Captures| {
630            let key_id = caps.get(1).map(|m| m.as_str()).unwrap_or(default_key_id);
631            let ciphertext = caps.get(2).map(|m| m.as_str()).unwrap_or("");
632
633            match key_store.get_key(key_id) {
634                Some(key) => match key.decrypt(ciphertext, None) {
635                    Ok(plaintext) => plaintext,
636                    Err(_) => "<decryption_error>".to_string(),
637                },
638                None => {
639                    // Create a default key if none exists
640                    let password = std::env::var("MOCKFORGE_ENCRYPTION_KEY")
641                        .unwrap_or_else(|_| "mockforge_default_encryption_key_2024".to_string());
642                    match crate::encryption::EncryptionKey::from_password_pbkdf2(
643                        &password,
644                        None,
645                        crate::encryption::EncryptionAlgorithm::Aes256Gcm,
646                    ) {
647                        Ok(key) => match key.decrypt(ciphertext, None) {
648                            Ok(plaintext) => plaintext,
649                            Err(_) => "<decryption_error>".to_string(),
650                        },
651                        Err(_) => "<key_creation_error>".to_string(),
652                    }
653                }
654            }
655        })
656        .to_string();
657
658    out
659}
660
661/// Replace file system tokens in a template string
662fn replace_fs_tokens(input: &str) -> String {
663    // Handle {{fs.readFile "path/to/file"}} or {{fs.readFile('path/to/file')}}
664    FS_READFILE_RE
665        .replace_all(input, |caps: &regex::Captures| {
666            let file_path = caps.get(1).or_else(|| caps.get(2)).map(|m| m.as_str()).unwrap_or("");
667
668            if file_path.is_empty() {
669                return "<fs.readFile: empty path>".to_string();
670            }
671
672            match std::fs::read_to_string(file_path) {
673                Ok(content) => content,
674                Err(e) => format!("<fs.readFile error: {}>", e),
675            }
676        })
677        .to_string()
678}
679
680fn replace_faker_tokens(input: &str) -> String {
681    // If a provider is registered (e.g., from mockforge-data), use it; else fallback
682    if let Some(provider) = FAKER_PROVIDER.get() {
683        return replace_with_provider(input, provider.as_ref());
684    }
685    replace_with_fallback(input)
686}
687
688fn replace_with_provider(input: &str, p: &dyn FakerProvider) -> String {
689    let mut out = input.to_string();
690    let map = [
691        ("{{faker.uuid}}", p.uuid()),
692        ("{{faker.email}}", p.email()),
693        ("{{faker.name}}", p.name()),
694        ("{{faker.address}}", p.address()),
695        ("{{faker.phone}}", p.phone()),
696        ("{{faker.company}}", p.company()),
697        ("{{faker.url}}", p.url()),
698        ("{{faker.ip}}", p.ip()),
699        ("{{faker.color}}", p.color()),
700        ("{{faker.word}}", p.word()),
701        ("{{faker.sentence}}", p.sentence()),
702        ("{{faker.paragraph}}", p.paragraph()),
703    ];
704    for (pat, val) in map {
705        if out.contains(pat) {
706            out = out.replace(pat, &val);
707        }
708    }
709    out
710}
711
712fn replace_with_fallback(input: &str) -> String {
713    let mut out = input.to_string();
714    if out.contains("{{faker.uuid}}") {
715        out = out.replace("{{faker.uuid}}", &uuid::Uuid::new_v4().to_string());
716    }
717    if out.contains("{{faker.email}}") {
718        let user: String =
719            (0..8).map(|_| (b'a' + (thread_rng().random::<u8>() % 26)) as char).collect();
720        let dom: String =
721            (0..6).map(|_| (b'a' + (thread_rng().random::<u8>() % 26)) as char).collect();
722        out = out.replace("{{faker.email}}", &format!("{}@{}.example", user, dom));
723    }
724    if out.contains("{{faker.name}}") {
725        let firsts = ["Alex", "Sam", "Taylor", "Jordan", "Casey", "Riley"];
726        let lasts = ["Smith", "Lee", "Patel", "Garcia", "Kim", "Brown"];
727        let fi = thread_rng().random::<u8>() as usize % firsts.len();
728        let li = thread_rng().random::<u8>() as usize % lasts.len();
729        out = out.replace("{{faker.name}}", &format!("{} {}", firsts[fi], lasts[li]));
730    }
731    out
732}
733
734#[cfg(test)]
735mod tests {
736    use super::*;
737    use crate::request_chaining::{ChainContext, ChainResponse, ChainTemplatingContext};
738    use serde_json::json;
739
740    #[test]
741    fn test_expand_str_with_context() {
742        let chain_context = ChainTemplatingContext::new(ChainContext::new());
743        let context = TemplatingContext::with_chain(chain_context);
744        let result = expand_str_with_context("{{uuid}}", &context);
745        assert!(!result.is_empty());
746    }
747
748    #[test]
749    fn test_replace_env_tokens() {
750        let mut vars = HashMap::new();
751        vars.insert("api_key".to_string(), "secret123".to_string());
752        let env_context = EnvironmentTemplatingContext::new(vars);
753        let result = replace_env_tokens("{{api_key}}", &env_context);
754        assert_eq!(result, "secret123");
755    }
756
757    #[test]
758    fn test_replace_chain_tokens() {
759        let chain_ctx = ChainContext::new();
760        let template_ctx = ChainTemplatingContext::new(chain_ctx);
761        let context = Some(&template_ctx);
762        // Note: This test would need a proper response stored in the chain context
763        let result = replace_chain_tokens("{{chain.test.body}}", context);
764        assert_eq!(result, "null");
765    }
766
767    #[test]
768    fn test_response_function() {
769        // Test with no chain context
770        let result = replace_response_function(r#"response('login', 'body.user_id')"#, None);
771        assert_eq!(result, r#"response('login', 'body.user_id')"#);
772
773        // Test with chain context but no matching response
774        let chain_ctx = ChainContext::new();
775        let template_ctx = ChainTemplatingContext::new(chain_ctx);
776        let context = Some(&template_ctx);
777        let result = replace_response_function(r#"response('login', 'body.user_id')"#, context);
778        assert_eq!(result, "null");
779
780        // Test with stored response
781        let mut chain_ctx = ChainContext::new();
782        let response = ChainResponse {
783            status: 200,
784            headers: HashMap::new(),
785            body: Some(json!({"user_id": 12345})),
786            duration_ms: 150,
787            executed_at: "2023-01-01T00:00:00Z".to_string(),
788            error: None,
789        };
790        chain_ctx.store_response("login".to_string(), response);
791        let template_ctx = ChainTemplatingContext::new(chain_ctx);
792        let context = Some(&template_ctx);
793
794        let result = replace_response_function(r#"response('login', 'user_id')"#, context);
795        assert_eq!(result, "12345");
796    }
797
798    #[test]
799    fn test_fs_readfile() {
800        // Create a temporary file for testing
801        use std::fs;
802
803        let temp_file = "/tmp/mockforge_test_file.txt";
804        let test_content = "Hello, this is test content!";
805        fs::write(temp_file, test_content).unwrap();
806
807        // Test successful file reading
808        let template = format!(r#"{{{{fs.readFile "{}"}}}}"#, temp_file);
809        let result = expand_str(&template);
810        assert_eq!(result, test_content);
811
812        // Test with parentheses
813        let template = format!(r#"{{{{fs.readFile('{}')}}}}"#, temp_file);
814        let result = expand_str(&template);
815        assert_eq!(result, test_content);
816
817        // Test file not found
818        let template = r#"{{fs.readFile "/nonexistent/file.txt"}}"#;
819        let result = expand_str(template);
820        assert!(result.contains("fs.readFile error:"));
821
822        // Test empty path
823        let template = r#"{{fs.readFile ""}}"#;
824        let result = expand_str(template);
825        assert_eq!(result, "<fs.readFile: empty path>");
826
827        // Clean up
828        let _ = fs::remove_file(temp_file);
829    }
830}