1use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[serde(rename_all = "lowercase")]
13pub enum AiResponseMode {
14 Static,
16 Intelligent,
18 Hybrid,
20}
21
22impl Default for AiResponseMode {
23 fn default() -> Self {
24 Self::Static
25 }
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct AiResponseConfig {
32 #[serde(default)]
34 pub enabled: bool,
35
36 #[serde(default)]
38 pub mode: AiResponseMode,
39
40 pub prompt: Option<String>,
43
44 pub context: Option<String>,
46
47 #[serde(default = "default_temperature")]
49 pub temperature: f32,
50
51 #[serde(default = "default_max_tokens")]
53 pub max_tokens: usize,
54
55 pub schema: Option<Value>,
57
58 #[serde(default = "default_true")]
60 pub cache_enabled: bool,
61}
62
63fn default_temperature() -> f32 {
64 0.7
65}
66
67fn default_max_tokens() -> usize {
68 1024
69}
70
71fn default_true() -> bool {
72 true
73}
74
75impl Default for AiResponseConfig {
76 fn default() -> Self {
77 Self {
78 enabled: false,
79 mode: AiResponseMode::Static,
80 prompt: None,
81 context: None,
82 temperature: default_temperature(),
83 max_tokens: default_max_tokens(),
84 schema: None,
85 cache_enabled: true,
86 }
87 }
88}
89
90impl AiResponseConfig {
91 pub fn new(enabled: bool, mode: AiResponseMode, prompt: String) -> Self {
93 Self {
94 enabled,
95 mode,
96 prompt: Some(prompt),
97 ..Default::default()
98 }
99 }
100
101 pub fn is_active(&self) -> bool {
103 self.enabled && self.mode != AiResponseMode::Static && self.prompt.is_some()
104 }
105}
106
107#[derive(Debug, Clone, Default)]
109pub struct RequestContext {
110 pub method: String,
112 pub path: String,
114 pub path_params: HashMap<String, Value>,
116 pub query_params: HashMap<String, Value>,
118 pub headers: HashMap<String, Value>,
120 pub body: Option<Value>,
122 pub multipart_fields: HashMap<String, Value>,
124 pub multipart_files: HashMap<String, String>,
126}
127
128impl RequestContext {
129 pub fn new(method: String, path: String) -> Self {
131 Self {
132 method,
133 path,
134 ..Default::default()
135 }
136 }
137
138 pub fn with_path_params(mut self, params: HashMap<String, Value>) -> Self {
140 self.path_params = params;
141 self
142 }
143
144 pub fn with_query_params(mut self, params: HashMap<String, Value>) -> Self {
146 self.query_params = params;
147 self
148 }
149
150 pub fn with_headers(mut self, headers: HashMap<String, Value>) -> Self {
152 self.headers = headers;
153 self
154 }
155
156 pub fn with_body(mut self, body: Value) -> Self {
158 self.body = Some(body);
159 self
160 }
161
162 pub fn with_multipart_fields(mut self, fields: HashMap<String, Value>) -> Self {
164 self.multipart_fields = fields;
165 self
166 }
167
168 pub fn with_multipart_files(mut self, files: HashMap<String, String>) -> Self {
170 self.multipart_files = files;
171 self
172 }
173}
174
175#[deprecated(note = "Use mockforge_template_expansion::expand_prompt_template instead")]
188pub fn expand_prompt_template(template: &str, context: &RequestContext) -> String {
189 let mut result = template.to_string();
190
191 result = result
193 .replace("{{request.query.", "{{query.")
194 .replace("{{request.path.", "{{path.")
195 .replace("{{request.headers.", "{{headers.")
196 .replace("{{request.body.", "{{body.")
197 .replace("{{request.method}}", "{{method}}")
198 .replace("{{request.path}}", "{{path}}");
199
200 result = result.replace("{{method}}", &context.method);
202
203 result = result.replace("{{path}}", &context.path);
205
206 for (key, val) in &context.path_params {
208 let placeholder = format!("{{{{path.{key}}}}}");
209 let replacement = value_to_string(val);
210 result = result.replace(&placeholder, &replacement);
211 }
212
213 for (key, val) in &context.query_params {
215 let placeholder = format!("{{{{query.{key}}}}}");
216 let replacement = value_to_string(val);
217 result = result.replace(&placeholder, &replacement);
218 }
219
220 for (key, val) in &context.headers {
222 let placeholder = format!("{{{{headers.{key}}}}}");
223 let replacement = value_to_string(val);
224 result = result.replace(&placeholder, &replacement);
225 }
226
227 if let Some(body) = &context.body {
229 if let Some(obj) = body.as_object() {
230 for (key, val) in obj {
231 let placeholder = format!("{{{{body.{key}}}}}");
232 let replacement = value_to_string(val);
233 result = result.replace(&placeholder, &replacement);
234 }
235 }
236 }
237
238 for (key, val) in &context.multipart_fields {
240 let placeholder = format!("{{{{multipart.{key}}}}}");
241 let replacement = value_to_string(val);
242 result = result.replace(&placeholder, &replacement);
243 }
244
245 result
246}
247
248fn value_to_string(val: &serde_json::Value) -> String {
250 match val {
251 serde_json::Value::String(s) => s.clone(),
252 serde_json::Value::Number(n) => n.to_string(),
253 serde_json::Value::Bool(b) => b.to_string(),
254 serde_json::Value::Null => "null".to_string(),
255 _ => serde_json::to_string(val).unwrap_or_default(),
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use mockforge_template_expansion::{
263 expand_prompt_template, RequestContext as TemplateRequestContext,
264 };
265 use serde_json::json;
266
267 fn to_template_context(context: &RequestContext) -> TemplateRequestContext {
269 TemplateRequestContext {
270 method: context.method.clone(),
271 path: context.path.clone(),
272 path_params: context.path_params.clone(),
273 query_params: context.query_params.clone(),
274 headers: context.headers.clone(),
275 body: context.body.clone(),
276 multipart_fields: context.multipart_fields.clone(),
277 multipart_files: context.multipart_files.clone(),
278 }
279 }
280
281 #[test]
282 fn test_ai_response_config_default() {
283 let config = AiResponseConfig::default();
284 assert!(!config.enabled);
285 assert_eq!(config.mode, AiResponseMode::Static);
286 assert!(!config.is_active());
287 }
288
289 #[test]
290 fn test_ai_response_config_is_active() {
291 let config =
292 AiResponseConfig::new(true, AiResponseMode::Intelligent, "Test prompt".to_string());
293 assert!(config.is_active());
294
295 let config_disabled = AiResponseConfig {
296 enabled: false,
297 mode: AiResponseMode::Intelligent,
298 prompt: Some("Test".to_string()),
299 ..Default::default()
300 };
301 assert!(!config_disabled.is_active());
302 }
303
304 #[test]
305 fn test_request_context_builder() {
306 let mut path_params = HashMap::new();
307 path_params.insert("id".to_string(), json!("123"));
308
309 let context = RequestContext::new("POST".to_string(), "/users/123".to_string())
310 .with_path_params(path_params)
311 .with_body(json!({"name": "John"}));
312
313 assert_eq!(context.method, "POST");
314 assert_eq!(context.path, "/users/123");
315 assert_eq!(context.path_params.get("id"), Some(&json!("123")));
316 assert_eq!(context.body, Some(json!({"name": "John"})));
317 }
318
319 #[test]
320 fn test_expand_prompt_template_basic() {
321 let context = RequestContext::new("GET".to_string(), "/users".to_string());
322 let template = "Method: {{method}}, Path: {{path}}";
323 let template_context = to_template_context(&context);
324 let expanded = expand_prompt_template(template, &template_context);
325 assert_eq!(expanded, "Method: GET, Path: /users");
326 }
327
328 #[test]
329 fn test_expand_prompt_template_body() {
330 let body = json!({
331 "message": "Hello",
332 "user": "Alice"
333 });
334 let context = RequestContext::new("POST".to_string(), "/chat".to_string()).with_body(body);
335
336 let template = "User {{body.user}} says: {{body.message}}";
337 let template_context = to_template_context(&context);
338 let expanded = expand_prompt_template(template, &template_context);
339 assert_eq!(expanded, "User Alice says: Hello");
340 }
341
342 #[test]
343 fn test_expand_prompt_template_path_params() {
344 let mut path_params = HashMap::new();
345 path_params.insert("id".to_string(), json!("456"));
346 path_params.insert("name".to_string(), json!("test"));
347
348 let context = RequestContext::new("GET".to_string(), "/users/456".to_string())
349 .with_path_params(path_params);
350
351 let template = "Get user {{path.id}} with name {{path.name}}";
352 let template_context = to_template_context(&context);
353 let expanded = expand_prompt_template(template, &template_context);
354 assert_eq!(expanded, "Get user 456 with name test");
355 }
356
357 #[test]
358 fn test_expand_prompt_template_query_params() {
359 let mut query_params = HashMap::new();
360 query_params.insert("search".to_string(), json!("term"));
361 query_params.insert("limit".to_string(), json!(10));
362
363 let context = RequestContext::new("GET".to_string(), "/search".to_string())
364 .with_query_params(query_params);
365
366 let template = "Search for {{query.search}} with limit {{query.limit}}";
367 let template_context = to_template_context(&context);
368 let expanded = expand_prompt_template(template, &template_context);
369 assert_eq!(expanded, "Search for term with limit 10");
370 }
371
372 #[test]
373 fn test_expand_prompt_template_headers() {
374 let mut headers = HashMap::new();
375 headers.insert("user-agent".to_string(), json!("TestClient/1.0"));
376
377 let context =
378 RequestContext::new("GET".to_string(), "/api".to_string()).with_headers(headers);
379
380 let template = "Request from {{headers.user-agent}}";
381 let template_context = to_template_context(&context);
382 let expanded = expand_prompt_template(template, &template_context);
383 assert_eq!(expanded, "Request from TestClient/1.0");
384 }
385
386 #[test]
387 fn test_expand_prompt_template_complex() {
388 let mut path_params = HashMap::new();
389 path_params.insert("id".to_string(), json!("789"));
390
391 let mut query_params = HashMap::new();
392 query_params.insert("format".to_string(), json!("json"));
393
394 let body = json!({"action": "update", "value": 42});
395
396 let context = RequestContext::new("PUT".to_string(), "/api/items/789".to_string())
397 .with_path_params(path_params)
398 .with_query_params(query_params)
399 .with_body(body);
400
401 let template = "{{method}} item {{path.id}} with action {{body.action}} and value {{body.value}} in format {{query.format}}";
402 let template_context = to_template_context(&context);
403 let expanded = expand_prompt_template(template, &template_context);
404 assert_eq!(expanded, "PUT item 789 with action update and value 42 in format json");
405 }
406}