mockforge_template_expansion/
lib.rs

1//! Template expansion utilities for request context variables
2//!
3//! This crate provides Send-safe template expansion that does NOT use `rng()`.
4//! It is completely isolated from the templating module in mockforge-core
5//! to avoid Send issues in async contexts.
6//!
7//! The key difference from `mockforge-core::templating` is that this crate
8//! only performs simple string replacements based on request context, and
9//! does not use any random number generation or other non-Send types.
10//!
11//! **Important**: This crate does NOT depend on `mockforge-core` to avoid
12//! bringing `rng()` into scope. `RequestContext` is duplicated here.
13
14use serde_json::Value;
15use std::collections::HashMap;
16
17/// Request context for prompt template expansion
18///
19/// This is a duplicate of `mockforge_core::ai_response::RequestContext`
20/// to avoid depending on `mockforge-core` which has `rng()` in scope.
21#[derive(Debug, Clone, Default)]
22pub struct RequestContext {
23    /// HTTP method (GET, POST, etc.)
24    pub method: String,
25    /// Request path
26    pub path: String,
27    /// Path parameters
28    pub path_params: HashMap<String, Value>,
29    /// Query parameters
30    pub query_params: HashMap<String, Value>,
31    /// Request headers
32    pub headers: HashMap<String, Value>,
33    /// Request body (if JSON)
34    pub body: Option<Value>,
35    /// Multipart form fields (for multipart/form-data requests)
36    pub multipart_fields: HashMap<String, Value>,
37    /// Multipart file uploads (filename -> file path)
38    pub multipart_files: HashMap<String, String>,
39}
40
41impl RequestContext {
42    /// Create a new request context
43    #[must_use]
44    pub fn new(method: String, path: String) -> Self {
45        Self {
46            method,
47            path,
48            ..Default::default()
49        }
50    }
51
52    /// Set path parameters
53    #[must_use]
54    pub fn with_path_params(mut self, params: HashMap<String, Value>) -> Self {
55        self.path_params = params;
56        self
57    }
58
59    /// Set query parameters
60    #[must_use]
61    pub fn with_query_params(mut self, params: HashMap<String, Value>) -> Self {
62        self.query_params = params;
63        self
64    }
65
66    /// Set headers
67    #[must_use]
68    pub fn with_headers(mut self, headers: HashMap<String, Value>) -> Self {
69        self.headers = headers;
70        self
71    }
72
73    /// Set body
74    #[must_use]
75    pub fn with_body(mut self, body: Value) -> Self {
76        self.body = Some(body);
77        self
78    }
79
80    /// Set multipart form fields
81    #[must_use]
82    pub fn with_multipart_fields(mut self, fields: HashMap<String, Value>) -> Self {
83        self.multipart_fields = fields;
84        self
85    }
86
87    /// Set multipart file uploads
88    #[must_use]
89    pub fn with_multipart_files(mut self, files: HashMap<String, String>) -> Self {
90        self.multipart_files = files;
91        self
92    }
93}
94
95/// Expand template variables in a prompt string using request context
96///
97/// This function is Send-safe and does not use `rng()` or any non-Send types.
98/// It only performs simple string replacements based on the request context.
99///
100/// # Arguments
101/// * `template` - Template string with variables like `{{method}}`, `{{path}}`, `{{query.name}}`, etc.
102/// * `context` - Request context containing method, path, query params, headers, body, etc.
103///
104/// # Returns
105/// String with all template variables replaced with actual values from context
106///
107/// # Example
108/// ```
109/// use mockforge_template_expansion::{expand_prompt_template, RequestContext};
110/// use serde_json::json;
111/// use std::collections::HashMap;
112///
113/// let mut query_params = HashMap::new();
114/// query_params.insert("search".to_string(), json!("term"));
115///
116/// let context = RequestContext::new("GET".to_string(), "/api/search".to_string())
117///     .with_query_params(query_params);
118///
119/// let template = "Search for {{query.search}} on path {{path}}";
120/// let expanded = expand_prompt_template(template, &context);
121/// assert_eq!(expanded, "Search for term on path /api/search");
122/// ```
123#[must_use]
124pub fn expand_prompt_template(template: &str, context: &RequestContext) -> String {
125    let mut result = template.to_string();
126
127    // Replace {{method}}
128    result = result.replace("{{method}}", &context.method);
129
130    // Replace {{path}}
131    result = result.replace("{{path}}", &context.path);
132
133    // Replace {{body.*}} variables
134    if let Some(body) = &context.body {
135        result = expand_json_variables(&result, "body", body);
136    }
137
138    // Replace {{path.*}} variables
139    result = expand_map_variables(&result, "path", &context.path_params);
140
141    // Replace {{query.*}} variables
142    result = expand_map_variables(&result, "query", &context.query_params);
143
144    // Replace {{headers.*}} variables
145    result = expand_map_variables(&result, "headers", &context.headers);
146
147    // Replace {{multipart.*}} variables for form fields
148    result = expand_map_variables(&result, "multipart", &context.multipart_fields);
149
150    result
151}
152
153/// Expand template variables from a JSON value
154///
155/// This helper function extracts values from a JSON object and replaces
156/// template placeholders like `{{body.field}}` with the actual field value.
157fn expand_json_variables(template: &str, prefix: &str, value: &Value) -> String {
158    let mut result = template.to_string();
159
160    // Handle object fields
161    if let Some(obj) = value.as_object() {
162        for (key, val) in obj {
163            let placeholder = format!("{{{{{prefix}.{key}}}}}");
164            let replacement = match val {
165                Value::String(s) => s.clone(),
166                Value::Number(n) => n.to_string(),
167                Value::Bool(b) => b.to_string(),
168                Value::Null => "null".to_string(),
169                _ => serde_json::to_string(val).unwrap_or_default(),
170            };
171            result = result.replace(&placeholder, &replacement);
172        }
173    }
174
175    result
176}
177
178/// Expand template variables from a `HashMap`
179///
180/// This helper function extracts values from a `HashMap` and replaces
181/// template placeholders like `{{query.name}}` with the actual value.
182fn expand_map_variables(template: &str, prefix: &str, map: &HashMap<String, Value>) -> String {
183    let mut result = template.to_string();
184
185    for (key, val) in map {
186        let placeholder = format!("{{{{{prefix}.{key}}}}}");
187        let replacement = match val {
188            Value::String(s) => s.clone(),
189            Value::Number(n) => n.to_string(),
190            Value::Bool(b) => b.to_string(),
191            Value::Null => "null".to_string(),
192            _ => serde_json::to_string(val).unwrap_or_default(),
193        };
194        result = result.replace(&placeholder, &replacement);
195    }
196
197    result
198}
199
200/// Expand template variables in a JSON value recursively using request context
201///
202/// This function is Send-safe and does not use `rng()` or any non-Send types.
203/// It only uses `expand_prompt_template` which performs simple string replacements.
204///
205/// # Arguments
206/// * `value` - JSON value to process
207/// * `context` - Request context for template variable expansion
208///
209/// # Returns
210/// New JSON value with all template tokens expanded
211///
212/// # Example
213/// ```
214/// use mockforge_template_expansion::{expand_templates_in_json, RequestContext};
215/// use serde_json::json;
216///
217/// let context = RequestContext::new("GET".to_string(), "/api/users".to_string());
218/// let value = json!({
219///     "message": "Request to {{path}}",
220///     "method": "{{method}}"
221/// });
222///
223/// let expanded = expand_templates_in_json(value, &context);
224/// assert_eq!(expanded["message"], "Request to /api/users");
225/// assert_eq!(expanded["method"], "GET");
226/// ```
227#[must_use]
228pub fn expand_templates_in_json(value: Value, context: &RequestContext) -> Value {
229    match value {
230        Value::String(s) => {
231            // Normalize {{request.query.name}} to {{query.name}} format for compatibility
232            let normalized = s
233                .replace("{{request.query.", "{{query.")
234                .replace("{{request.path.", "{{path.")
235                .replace("{{request.headers.", "{{headers.")
236                .replace("{{request.body.", "{{body.")
237                .replace("{{request.method}}", "{{method}}")
238                .replace("{{request.path}}", "{{path}}");
239            // Use expand_prompt_template which is Send-safe and doesn't use rng()
240            Value::String(expand_prompt_template(&normalized, context))
241        }
242        Value::Array(arr) => {
243            Value::Array(arr.into_iter().map(|v| expand_templates_in_json(v, context)).collect())
244        }
245        Value::Object(obj) => Value::Object(
246            obj.into_iter()
247                .map(|(k, v)| (k, expand_templates_in_json(v, context)))
248                .collect(),
249        ),
250        _ => value,
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    use serde_json::json;
258    use std::collections::HashMap;
259
260    // ==================== RequestContext Tests ====================
261
262    #[test]
263    fn test_request_context_new() {
264        let ctx = RequestContext::new("POST".to_string(), "/api/test".to_string());
265        assert_eq!(ctx.method, "POST");
266        assert_eq!(ctx.path, "/api/test");
267        assert!(ctx.path_params.is_empty());
268        assert!(ctx.query_params.is_empty());
269        assert!(ctx.headers.is_empty());
270        assert!(ctx.body.is_none());
271        assert!(ctx.multipart_fields.is_empty());
272        assert!(ctx.multipart_files.is_empty());
273    }
274
275    #[test]
276    fn test_request_context_default() {
277        let ctx = RequestContext::default();
278        assert_eq!(ctx.method, "");
279        assert_eq!(ctx.path, "");
280        assert!(ctx.path_params.is_empty());
281        assert!(ctx.query_params.is_empty());
282        assert!(ctx.headers.is_empty());
283        assert!(ctx.body.is_none());
284        assert!(ctx.multipart_fields.is_empty());
285        assert!(ctx.multipart_files.is_empty());
286    }
287
288    #[test]
289    fn test_request_context_with_path_params() {
290        let mut params = HashMap::new();
291        params.insert("id".to_string(), json!("123"));
292        params.insert("name".to_string(), json!("test"));
293
294        let ctx = RequestContext::new("GET".to_string(), "/users".to_string())
295            .with_path_params(params.clone());
296
297        assert_eq!(ctx.path_params.len(), 2);
298        assert_eq!(ctx.path_params.get("id"), Some(&json!("123")));
299        assert_eq!(ctx.path_params.get("name"), Some(&json!("test")));
300    }
301
302    #[test]
303    fn test_request_context_with_query_params() {
304        let mut params = HashMap::new();
305        params.insert("page".to_string(), json!(1));
306        params.insert("limit".to_string(), json!(10));
307
308        let ctx =
309            RequestContext::new("GET".to_string(), "/items".to_string()).with_query_params(params);
310
311        assert_eq!(ctx.query_params.len(), 2);
312        assert_eq!(ctx.query_params.get("page"), Some(&json!(1)));
313    }
314
315    #[test]
316    fn test_request_context_with_headers() {
317        let mut headers = HashMap::new();
318        headers.insert("content-type".to_string(), json!("application/json"));
319        headers.insert("authorization".to_string(), json!("Bearer token123"));
320
321        let ctx = RequestContext::new("POST".to_string(), "/api".to_string()).with_headers(headers);
322
323        assert_eq!(ctx.headers.len(), 2);
324        assert_eq!(ctx.headers.get("content-type"), Some(&json!("application/json")));
325    }
326
327    #[test]
328    fn test_request_context_with_body() {
329        let body = json!({"key": "value", "count": 42});
330        let ctx =
331            RequestContext::new("POST".to_string(), "/data".to_string()).with_body(body.clone());
332
333        assert_eq!(ctx.body, Some(body));
334    }
335
336    #[test]
337    fn test_request_context_with_multipart_fields() {
338        let mut fields = HashMap::new();
339        fields.insert("username".to_string(), json!("testuser"));
340        fields.insert("email".to_string(), json!("test@example.com"));
341
342        let ctx = RequestContext::new("POST".to_string(), "/upload".to_string())
343            .with_multipart_fields(fields);
344
345        assert_eq!(ctx.multipart_fields.len(), 2);
346        assert_eq!(ctx.multipart_fields.get("username"), Some(&json!("testuser")));
347    }
348
349    #[test]
350    fn test_request_context_with_multipart_files() {
351        let mut files = HashMap::new();
352        files.insert("document".to_string(), "/tmp/doc.pdf".to_string());
353        files.insert("image".to_string(), "/tmp/photo.jpg".to_string());
354
355        let ctx = RequestContext::new("POST".to_string(), "/upload".to_string())
356            .with_multipart_files(files);
357
358        assert_eq!(ctx.multipart_files.len(), 2);
359        assert_eq!(ctx.multipart_files.get("document"), Some(&"/tmp/doc.pdf".to_string()));
360    }
361
362    #[test]
363    fn test_request_context_builder_chain() {
364        let mut path_params = HashMap::new();
365        path_params.insert("id".to_string(), json!("456"));
366
367        let mut query_params = HashMap::new();
368        query_params.insert("verbose".to_string(), json!(true));
369
370        let mut headers = HashMap::new();
371        headers.insert("x-custom".to_string(), json!("value"));
372
373        let mut multipart_fields = HashMap::new();
374        multipart_fields.insert("field1".to_string(), json!("data"));
375
376        let mut multipart_files = HashMap::new();
377        multipart_files.insert("file1".to_string(), "/path/to/file".to_string());
378
379        let ctx = RequestContext::new("PUT".to_string(), "/resource/456".to_string())
380            .with_path_params(path_params)
381            .with_query_params(query_params)
382            .with_headers(headers)
383            .with_body(json!({"update": true}))
384            .with_multipart_fields(multipart_fields)
385            .with_multipart_files(multipart_files);
386
387        assert_eq!(ctx.method, "PUT");
388        assert_eq!(ctx.path, "/resource/456");
389        assert_eq!(ctx.path_params.len(), 1);
390        assert_eq!(ctx.query_params.len(), 1);
391        assert_eq!(ctx.headers.len(), 1);
392        assert!(ctx.body.is_some());
393        assert_eq!(ctx.multipart_fields.len(), 1);
394        assert_eq!(ctx.multipart_files.len(), 1);
395    }
396
397    // ==================== expand_prompt_template Tests ====================
398
399    #[test]
400    fn test_expand_prompt_template_basic() {
401        let context = RequestContext::new("GET".to_string(), "/users".to_string());
402        let template = "Method: {{method}}, Path: {{path}}";
403        let expanded = expand_prompt_template(template, &context);
404        assert_eq!(expanded, "Method: GET, Path: /users");
405    }
406
407    #[test]
408    fn test_expand_prompt_template_empty_template() {
409        let context = RequestContext::new("GET".to_string(), "/test".to_string());
410        let expanded = expand_prompt_template("", &context);
411        assert_eq!(expanded, "");
412    }
413
414    #[test]
415    fn test_expand_prompt_template_no_variables() {
416        let context = RequestContext::new("GET".to_string(), "/test".to_string());
417        let template = "This is a plain string with no variables";
418        let expanded = expand_prompt_template(template, &context);
419        assert_eq!(expanded, template);
420    }
421
422    #[test]
423    fn test_expand_prompt_template_missing_variable() {
424        let context = RequestContext::new("GET".to_string(), "/test".to_string());
425        let template = "Value is {{query.nonexistent}}";
426        let expanded = expand_prompt_template(template, &context);
427        // Missing variables should remain in the string
428        assert_eq!(expanded, "Value is {{query.nonexistent}}");
429    }
430
431    #[test]
432    fn test_expand_prompt_template_multiple_occurrences() {
433        let context = RequestContext::new("GET".to_string(), "/api".to_string());
434        let template = "{{method}} to {{path}}, again {{method}} to {{path}}";
435        let expanded = expand_prompt_template(template, &context);
436        assert_eq!(expanded, "GET to /api, again GET to /api");
437    }
438
439    #[test]
440    fn test_expand_prompt_template_body() {
441        let body = json!({
442            "message": "Hello",
443            "user": "Alice"
444        });
445        let context = RequestContext::new("POST".to_string(), "/chat".to_string()).with_body(body);
446
447        let template = "User {{body.user}} says: {{body.message}}";
448        let expanded = expand_prompt_template(template, &context);
449        assert_eq!(expanded, "User Alice says: Hello");
450    }
451
452    #[test]
453    fn test_expand_prompt_template_body_with_number() {
454        let body = json!({
455            "count": 42,
456            "price": 19.99
457        });
458        let context = RequestContext::new("POST".to_string(), "/order".to_string()).with_body(body);
459
460        let template = "Count: {{body.count}}, Price: {{body.price}}";
461        let expanded = expand_prompt_template(template, &context);
462        assert_eq!(expanded, "Count: 42, Price: 19.99");
463    }
464
465    #[test]
466    fn test_expand_prompt_template_body_with_boolean() {
467        let body = json!({
468            "active": true,
469            "deleted": false
470        });
471        let context =
472            RequestContext::new("POST".to_string(), "/status".to_string()).with_body(body);
473
474        let template = "Active: {{body.active}}, Deleted: {{body.deleted}}";
475        let expanded = expand_prompt_template(template, &context);
476        assert_eq!(expanded, "Active: true, Deleted: false");
477    }
478
479    #[test]
480    fn test_expand_prompt_template_body_with_null() {
481        let body = json!({
482            "value": null
483        });
484        let context = RequestContext::new("POST".to_string(), "/data".to_string()).with_body(body);
485
486        let template = "Value: {{body.value}}";
487        let expanded = expand_prompt_template(template, &context);
488        assert_eq!(expanded, "Value: null");
489    }
490
491    #[test]
492    fn test_expand_prompt_template_body_with_nested_object() {
493        let body = json!({
494            "nested": {"inner": "value"}
495        });
496        let context = RequestContext::new("POST".to_string(), "/data".to_string()).with_body(body);
497
498        let template = "Nested: {{body.nested}}";
499        let expanded = expand_prompt_template(template, &context);
500        assert_eq!(expanded, r#"Nested: {"inner":"value"}"#);
501    }
502
503    #[test]
504    fn test_expand_prompt_template_body_with_array() {
505        let body = json!({
506            "items": [1, 2, 3]
507        });
508        let context = RequestContext::new("POST".to_string(), "/data".to_string()).with_body(body);
509
510        let template = "Items: {{body.items}}";
511        let expanded = expand_prompt_template(template, &context);
512        assert_eq!(expanded, "Items: [1,2,3]");
513    }
514
515    #[test]
516    fn test_expand_prompt_template_no_body() {
517        let context = RequestContext::new("GET".to_string(), "/test".to_string());
518        let template = "Body field: {{body.field}}";
519        let expanded = expand_prompt_template(template, &context);
520        // Body is None, so placeholder remains
521        assert_eq!(expanded, "Body field: {{body.field}}");
522    }
523
524    #[test]
525    fn test_expand_prompt_template_path_params() {
526        let mut path_params = HashMap::new();
527        path_params.insert("id".to_string(), json!("456"));
528        path_params.insert("name".to_string(), json!("test"));
529
530        let context = RequestContext::new("GET".to_string(), "/users/456".to_string())
531            .with_path_params(path_params);
532
533        let template = "Get user {{path.id}} with name {{path.name}}";
534        let expanded = expand_prompt_template(template, &context);
535        assert_eq!(expanded, "Get user 456 with name test");
536    }
537
538    #[test]
539    fn test_expand_prompt_template_query_params() {
540        let mut query_params = HashMap::new();
541        query_params.insert("search".to_string(), json!("term"));
542        query_params.insert("limit".to_string(), json!(10));
543
544        let context = RequestContext::new("GET".to_string(), "/search".to_string())
545            .with_query_params(query_params);
546
547        let template = "Search for {{query.search}} with limit {{query.limit}}";
548        let expanded = expand_prompt_template(template, &context);
549        assert_eq!(expanded, "Search for term with limit 10");
550    }
551
552    #[test]
553    fn test_expand_prompt_template_query_params_boolean() {
554        let mut query_params = HashMap::new();
555        query_params.insert("verbose".to_string(), json!(true));
556        query_params.insert("debug".to_string(), json!(false));
557
558        let context = RequestContext::new("GET".to_string(), "/api".to_string())
559            .with_query_params(query_params);
560
561        let template = "Verbose: {{query.verbose}}, Debug: {{query.debug}}";
562        let expanded = expand_prompt_template(template, &context);
563        assert_eq!(expanded, "Verbose: true, Debug: false");
564    }
565
566    #[test]
567    fn test_expand_prompt_template_query_params_null() {
568        let mut query_params = HashMap::new();
569        query_params.insert("filter".to_string(), json!(null));
570
571        let context = RequestContext::new("GET".to_string(), "/api".to_string())
572            .with_query_params(query_params);
573
574        let template = "Filter: {{query.filter}}";
575        let expanded = expand_prompt_template(template, &context);
576        assert_eq!(expanded, "Filter: null");
577    }
578
579    #[test]
580    fn test_expand_prompt_template_query_params_array() {
581        let mut query_params = HashMap::new();
582        query_params.insert("tags".to_string(), json!(["a", "b", "c"]));
583
584        let context = RequestContext::new("GET".to_string(), "/api".to_string())
585            .with_query_params(query_params);
586
587        let template = "Tags: {{query.tags}}";
588        let expanded = expand_prompt_template(template, &context);
589        assert_eq!(expanded, r#"Tags: ["a","b","c"]"#);
590    }
591
592    #[test]
593    fn test_expand_prompt_template_headers() {
594        let mut headers = HashMap::new();
595        headers.insert("user-agent".to_string(), json!("TestClient/1.0"));
596
597        let context =
598            RequestContext::new("GET".to_string(), "/api".to_string()).with_headers(headers);
599
600        let template = "Request from {{headers.user-agent}}";
601        let expanded = expand_prompt_template(template, &context);
602        assert_eq!(expanded, "Request from TestClient/1.0");
603    }
604
605    #[test]
606    fn test_expand_prompt_template_multipart_fields() {
607        let mut multipart_fields = HashMap::new();
608        multipart_fields.insert("username".to_string(), json!("testuser"));
609        multipart_fields.insert("description".to_string(), json!("A test file"));
610
611        let context = RequestContext::new("POST".to_string(), "/upload".to_string())
612            .with_multipart_fields(multipart_fields);
613
614        let template = "User: {{multipart.username}}, Desc: {{multipart.description}}";
615        let expanded = expand_prompt_template(template, &context);
616        assert_eq!(expanded, "User: testuser, Desc: A test file");
617    }
618
619    #[test]
620    fn test_expand_prompt_template_complex() {
621        let mut path_params = HashMap::new();
622        path_params.insert("id".to_string(), json!("789"));
623
624        let mut query_params = HashMap::new();
625        query_params.insert("format".to_string(), json!("json"));
626
627        let body = json!({"action": "update", "value": 42});
628
629        let context = RequestContext::new("PUT".to_string(), "/api/items/789".to_string())
630            .with_path_params(path_params)
631            .with_query_params(query_params)
632            .with_body(body);
633
634        let template = "{{method}} item {{path.id}} with action {{body.action}} and value {{body.value}} in format {{query.format}}";
635        let expanded = expand_prompt_template(template, &context);
636        assert_eq!(expanded, "PUT item 789 with action update and value 42 in format json");
637    }
638
639    #[test]
640    fn test_expand_prompt_template_empty_context() {
641        let context = RequestContext::default();
642        let template = "Method: {{method}}, Path: {{path}}";
643        let expanded = expand_prompt_template(template, &context);
644        assert_eq!(expanded, "Method: , Path: ");
645    }
646
647    // ==================== expand_templates_in_json Tests ====================
648
649    #[test]
650    fn test_expand_templates_in_json() {
651        let context = RequestContext::new("GET".to_string(), "/api/users".to_string());
652        let value = json!({
653            "message": "Request to {{path}}",
654            "method": "{{method}}",
655            "nested": {
656                "path": "{{path}}"
657            },
658            "array": ["{{method}}", "{{path}}"]
659        });
660
661        let expanded = expand_templates_in_json(value, &context);
662        assert_eq!(expanded["message"], "Request to /api/users");
663        assert_eq!(expanded["method"], "GET");
664        assert_eq!(expanded["nested"]["path"], "/api/users");
665        assert_eq!(expanded["array"][0], "GET");
666        assert_eq!(expanded["array"][1], "/api/users");
667    }
668
669    #[test]
670    fn test_expand_templates_in_json_primitives_unchanged() {
671        let context = RequestContext::new("GET".to_string(), "/test".to_string());
672
673        // Numbers should remain unchanged
674        let num_value = json!(42);
675        let expanded_num = expand_templates_in_json(num_value, &context);
676        assert_eq!(expanded_num, json!(42));
677
678        // Booleans should remain unchanged
679        let bool_value = json!(true);
680        let expanded_bool = expand_templates_in_json(bool_value, &context);
681        assert_eq!(expanded_bool, json!(true));
682
683        // Null should remain unchanged
684        let null_value = json!(null);
685        let expanded_null = expand_templates_in_json(null_value, &context);
686        assert_eq!(expanded_null, json!(null));
687
688        // Float should remain unchanged
689        let float_value = json!(3.125);
690        let expanded_float = expand_templates_in_json(float_value, &context);
691        assert_eq!(expanded_float, json!(3.125));
692    }
693
694    #[test]
695    fn test_expand_templates_in_json_empty_string() {
696        let context = RequestContext::new("GET".to_string(), "/test".to_string());
697        let value = json!("");
698        let expanded = expand_templates_in_json(value, &context);
699        assert_eq!(expanded, json!(""));
700    }
701
702    #[test]
703    fn test_expand_templates_in_json_empty_array() {
704        let context = RequestContext::new("GET".to_string(), "/test".to_string());
705        let value = json!([]);
706        let expanded = expand_templates_in_json(value, &context);
707        assert_eq!(expanded, json!([]));
708    }
709
710    #[test]
711    fn test_expand_templates_in_json_empty_object() {
712        let context = RequestContext::new("GET".to_string(), "/test".to_string());
713        let value = json!({});
714        let expanded = expand_templates_in_json(value, &context);
715        assert_eq!(expanded, json!({}));
716    }
717
718    #[test]
719    fn test_expand_templates_in_json_deeply_nested() {
720        let context = RequestContext::new("POST".to_string(), "/deep".to_string());
721        let value = json!({
722            "level1": {
723                "level2": {
724                    "level3": {
725                        "method": "{{method}}",
726                        "path": "{{path}}"
727                    }
728                }
729            }
730        });
731
732        let expanded = expand_templates_in_json(value, &context);
733        assert_eq!(expanded["level1"]["level2"]["level3"]["method"], "POST");
734        assert_eq!(expanded["level1"]["level2"]["level3"]["path"], "/deep");
735    }
736
737    #[test]
738    fn test_expand_templates_in_json_mixed_array() {
739        let context = RequestContext::new("DELETE".to_string(), "/resource".to_string());
740        let value = json!([
741            "{{method}}",
742            42,
743            true,
744            null,
745            {"nested": "{{path}}"},
746            ["{{method}}", 123]
747        ]);
748
749        let expanded = expand_templates_in_json(value, &context);
750        assert_eq!(expanded[0], "DELETE");
751        assert_eq!(expanded[1], 42);
752        assert_eq!(expanded[2], true);
753        assert_eq!(expanded[3], json!(null));
754        assert_eq!(expanded[4]["nested"], "/resource");
755        assert_eq!(expanded[5][0], "DELETE");
756        assert_eq!(expanded[5][1], 123);
757    }
758
759    #[test]
760    fn test_expand_templates_in_json_normalize_request_prefix() {
761        let context = RequestContext::new("POST".to_string(), "/api/data".to_string());
762        let value = json!({
763            "message": "{{request.method}} {{request.path}}",
764            "query": "{{request.query.name}}"
765        });
766
767        let expanded = expand_templates_in_json(value, &context);
768        assert_eq!(expanded["message"], "POST /api/data");
769        // Note: query.name won't be expanded since it's not in context, but normalization should work
770    }
771
772    #[test]
773    fn test_expand_templates_in_json_normalize_all_request_prefixes() {
774        let mut query_params = HashMap::new();
775        query_params.insert("search".to_string(), json!("test"));
776
777        let mut path_params = HashMap::new();
778        path_params.insert("id".to_string(), json!("123"));
779
780        let mut headers = HashMap::new();
781        headers.insert("auth".to_string(), json!("token"));
782
783        let body = json!({"field": "value"});
784
785        let context = RequestContext::new("GET".to_string(), "/items/123".to_string())
786            .with_query_params(query_params)
787            .with_path_params(path_params)
788            .with_headers(headers)
789            .with_body(body);
790
791        let value = json!({
792            "method": "{{request.method}}",
793            "path": "{{request.path}}",
794            "query_search": "{{request.query.search}}",
795            "path_id": "{{request.path.id}}",
796            "header_auth": "{{request.headers.auth}}",
797            "body_field": "{{request.body.field}}"
798        });
799
800        let expanded = expand_templates_in_json(value, &context);
801        assert_eq!(expanded["method"], "GET");
802        assert_eq!(expanded["path"], "/items/123");
803        assert_eq!(expanded["query_search"], "test");
804        assert_eq!(expanded["path_id"], "123");
805        assert_eq!(expanded["header_auth"], "token");
806        assert_eq!(expanded["body_field"], "value");
807    }
808
809    #[test]
810    fn test_expand_templates_in_json_string_without_template() {
811        let context = RequestContext::new("GET".to_string(), "/test".to_string());
812        let value = json!("plain string without any templates");
813        let expanded = expand_templates_in_json(value, &context);
814        assert_eq!(expanded, json!("plain string without any templates"));
815    }
816
817    // ==================== Edge Case Tests ====================
818
819    #[test]
820    fn test_special_characters_in_values() {
821        let mut query_params = HashMap::new();
822        query_params
823            .insert("special".to_string(), json!("value with \"quotes\" and \\backslashes"));
824
825        let context = RequestContext::new("GET".to_string(), "/test?foo=bar&baz=qux".to_string())
826            .with_query_params(query_params);
827
828        let template = "Path: {{path}}, Special: {{query.special}}";
829        let expanded = expand_prompt_template(template, &context);
830        assert!(expanded.contains("/test?foo=bar&baz=qux"));
831        assert!(expanded.contains("value with \"quotes\" and \\backslashes"));
832    }
833
834    #[test]
835    fn test_unicode_in_values() {
836        let body = json!({
837            "message": "Hello δΈ–η•Œ! 🌍",
838            "emoji": "πŸš€βœ¨"
839        });
840        let context =
841            RequestContext::new("POST".to_string(), "/unicode".to_string()).with_body(body);
842
843        let template = "Message: {{body.message}}, Emoji: {{body.emoji}}";
844        let expanded = expand_prompt_template(template, &context);
845        assert_eq!(expanded, "Message: Hello δΈ–η•Œ! 🌍, Emoji: πŸš€βœ¨");
846    }
847
848    #[test]
849    fn test_whitespace_handling() {
850        let context =
851            RequestContext::new("  GET  ".to_string(), "  /path with spaces  ".to_string());
852        let template = "Method: '{{method}}', Path: '{{path}}'";
853        let expanded = expand_prompt_template(template, &context);
854        assert_eq!(expanded, "Method: '  GET  ', Path: '  /path with spaces  '");
855    }
856
857    #[test]
858    fn test_empty_string_values() {
859        let mut query_params = HashMap::new();
860        query_params.insert("empty".to_string(), json!(""));
861
862        let context = RequestContext::new("GET".to_string(), "/test".to_string())
863            .with_query_params(query_params);
864
865        let template = "Empty value: '{{query.empty}}'";
866        let expanded = expand_prompt_template(template, &context);
867        assert_eq!(expanded, "Empty value: ''");
868    }
869
870    #[test]
871    fn test_request_context_debug() {
872        let ctx = RequestContext::new("GET".to_string(), "/test".to_string());
873        let debug_str = format!("{:?}", ctx);
874        assert!(debug_str.contains("RequestContext"));
875        assert!(debug_str.contains("GET"));
876        assert!(debug_str.contains("/test"));
877    }
878
879    #[test]
880    fn test_request_context_clone() {
881        let mut query_params = HashMap::new();
882        query_params.insert("key".to_string(), json!("value"));
883
884        let ctx = RequestContext::new("POST".to_string(), "/clone-test".to_string())
885            .with_query_params(query_params)
886            .with_body(json!({"data": 123}));
887
888        let cloned = ctx.clone();
889        assert_eq!(cloned.method, ctx.method);
890        assert_eq!(cloned.path, ctx.path);
891        assert_eq!(cloned.query_params, ctx.query_params);
892        assert_eq!(cloned.body, ctx.body);
893    }
894
895    #[test]
896    fn test_expand_prompt_template_body_non_object() {
897        // Test when body is a string (not an object)
898        let context = RequestContext::new("POST".to_string(), "/data".to_string())
899            .with_body(json!("just a string"));
900
901        let template = "Body: {{body}}, method: {{method}}";
902        let expanded = expand_prompt_template(template, &context);
903        // Body placeholders that don't match object fields should remain
904        assert_eq!(expanded, "Body: {{body}}, method: POST");
905    }
906
907    #[test]
908    fn test_expand_prompt_template_body_array_top_level() {
909        // Test when body is an array at the top level (not an object)
910        let context = RequestContext::new("POST".to_string(), "/data".to_string())
911            .with_body(json!([1, 2, 3]));
912
913        let template = "Array body: {{body.item}}, path: {{path}}";
914        let expanded = expand_prompt_template(template, &context);
915        // Since body is not an object, the placeholder should remain
916        assert_eq!(expanded, "Array body: {{body.item}}, path: /data");
917    }
918
919    #[test]
920    fn test_expand_prompt_template_body_primitive() {
921        // Test when body is a primitive value (number)
922        let context =
923            RequestContext::new("POST".to_string(), "/data".to_string()).with_body(json!(42));
924
925        let template = "Number body: {{body.value}}, method: {{method}}";
926        let expanded = expand_prompt_template(template, &context);
927        // Since body is not an object, the placeholder should remain
928        assert_eq!(expanded, "Number body: {{body.value}}, method: POST");
929    }
930
931    #[test]
932    fn test_expand_json_variables_non_object() {
933        // Direct test of expand_json_variables with non-object values
934        let template = "Value: {{test.field}}, other: {{other}}";
935
936        // Test with string
937        let result = expand_json_variables(template, "test", &json!("string value"));
938        assert_eq!(result, "Value: {{test.field}}, other: {{other}}");
939
940        // Test with number
941        let result = expand_json_variables(template, "test", &json!(123));
942        assert_eq!(result, "Value: {{test.field}}, other: {{other}}");
943
944        // Test with boolean
945        let result = expand_json_variables(template, "test", &json!(true));
946        assert_eq!(result, "Value: {{test.field}}, other: {{other}}");
947
948        // Test with null
949        let result = expand_json_variables(template, "test", &json!(null));
950        assert_eq!(result, "Value: {{test.field}}, other: {{other}}");
951
952        // Test with array
953        let result = expand_json_variables(template, "test", &json!([1, 2, 3]));
954        assert_eq!(result, "Value: {{test.field}}, other: {{other}}");
955    }
956
957    #[test]
958    fn test_expand_map_variables_empty_map() {
959        let template = "Query: {{query.param}}, path: {{path.id}}";
960        let empty_map = HashMap::new();
961
962        let result = expand_map_variables(template, "query", &empty_map);
963        assert_eq!(result, "Query: {{query.param}}, path: {{path.id}}");
964    }
965
966    #[test]
967    fn test_expand_map_variables_complex_types() {
968        let mut map = HashMap::new();
969        map.insert("nested".to_string(), json!({"inner": "value"}));
970        map.insert("array".to_string(), json!([1, 2, 3]));
971
972        let template = "Nested: {{test.nested}}, Array: {{test.array}}";
973        let result = expand_map_variables(template, "test", &map);
974        assert_eq!(result, r#"Nested: {"inner":"value"}, Array: [1,2,3]"#);
975    }
976
977    #[test]
978    fn test_expand_templates_in_json_with_request_body_prefix() {
979        let body = json!({"field": "value"});
980        let context = RequestContext::new("POST".to_string(), "/api".to_string()).with_body(body);
981
982        let value = json!({"msg": "{{request.body.field}}"});
983        let expanded = expand_templates_in_json(value, &context);
984        assert_eq!(expanded["msg"], "value");
985    }
986
987    #[test]
988    fn test_key_with_special_chars_in_placeholder() {
989        // Test that keys with special characters work correctly
990        let mut query_params = HashMap::new();
991        query_params.insert("user-id".to_string(), json!("12345"));
992        query_params.insert("session_token".to_string(), json!("abc123"));
993
994        let context = RequestContext::new("GET".to_string(), "/api".to_string())
995            .with_query_params(query_params);
996
997        let template = "User: {{query.user-id}}, Token: {{query.session_token}}";
998        let expanded = expand_prompt_template(template, &context);
999        assert_eq!(expanded, "User: 12345, Token: abc123");
1000    }
1001
1002    #[test]
1003    fn test_large_number_values() {
1004        let mut query_params = HashMap::new();
1005        query_params.insert("big".to_string(), json!(9999999999i64));
1006        query_params.insert("float".to_string(), json!(1.23456789));
1007
1008        let context = RequestContext::new("GET".to_string(), "/api".to_string())
1009            .with_query_params(query_params);
1010
1011        let template = "Big: {{query.big}}, Float: {{query.float}}";
1012        let expanded = expand_prompt_template(template, &context);
1013        assert!(expanded.contains("9999999999"));
1014        assert!(expanded.contains("1.23456789"));
1015    }
1016
1017    #[test]
1018    fn test_multiple_template_variables_same_name_different_prefix() {
1019        let mut path_params = HashMap::new();
1020        path_params.insert("id".to_string(), json!("path-123"));
1021
1022        let mut query_params = HashMap::new();
1023        query_params.insert("id".to_string(), json!("query-456"));
1024
1025        let body = json!({"id": "body-789"});
1026
1027        let context = RequestContext::new("POST".to_string(), "/resource".to_string())
1028            .with_path_params(path_params)
1029            .with_query_params(query_params)
1030            .with_body(body);
1031
1032        let template = "Path ID: {{path.id}}, Query ID: {{query.id}}, Body ID: {{body.id}}";
1033        let expanded = expand_prompt_template(template, &context);
1034        assert_eq!(expanded, "Path ID: path-123, Query ID: query-456, Body ID: body-789");
1035    }
1036
1037    #[test]
1038    fn test_partial_placeholder_should_not_expand() {
1039        let context = RequestContext::new("GET".to_string(), "/test".to_string());
1040
1041        // Test incomplete placeholders
1042        let template = "{{method} {{method}} {method}} {{metho {{method";
1043        let expanded = expand_prompt_template(template, &context);
1044        // Only {{method}} should be expanded
1045        assert!(expanded.contains("GET"));
1046        assert!(expanded.contains("{{method}"));
1047    }
1048}