1use crate::intelligent_behavior::config::Persona;
7use crate::openapi::response_selection::{ResponseSelectionMode, ResponseSelector};
8use crate::{ai_response::AiResponseConfig, openapi::spec::OpenApiSpec, Result};
9use openapiv3::{Operation, PathItem, ReferenceOr};
10use std::collections::BTreeMap;
11use std::sync::Arc;
12
13fn extract_path_parameters(path_template: &str) -> Vec<String> {
15 let mut params = Vec::new();
16 let mut in_param = false;
17 let mut current_param = String::new();
18
19 for ch in path_template.chars() {
20 match ch {
21 '{' => {
22 in_param = true;
23 current_param.clear();
24 }
25 '}' => {
26 if in_param {
27 params.push(current_param.clone());
28 in_param = false;
29 }
30 }
31 ch if in_param => {
32 current_param.push(ch);
33 }
34 _ => {}
35 }
36 }
37
38 params
39}
40
41#[derive(Debug, Clone)]
43pub struct OpenApiRoute {
44 pub method: String,
46 pub path: String,
48 pub operation: Operation,
50 pub metadata: BTreeMap<String, String>,
52 pub parameters: Vec<String>,
54 pub spec: Arc<OpenApiSpec>,
56 pub ai_config: Option<AiResponseConfig>,
58 pub response_selection_mode: ResponseSelectionMode,
60 pub response_selector: Arc<ResponseSelector>,
62 pub persona: Option<Arc<Persona>>,
64}
65
66impl OpenApiRoute {
67 pub fn new(method: String, path: String, operation: Operation, spec: Arc<OpenApiSpec>) -> Self {
69 Self::new_with_persona(method, path, operation, spec, None)
70 }
71
72 pub fn new_with_persona(
74 method: String,
75 path: String,
76 operation: Operation,
77 spec: Arc<OpenApiSpec>,
78 persona: Option<Arc<Persona>>,
79 ) -> Self {
80 let parameters = extract_path_parameters(&path);
81
82 let ai_config = Self::parse_ai_config(&operation);
84
85 let response_selection_mode = Self::parse_response_selection_mode(&operation);
87 let response_selector = Arc::new(ResponseSelector::new(response_selection_mode));
88
89 Self {
90 method,
91 path,
92 operation,
93 metadata: BTreeMap::new(),
94 parameters,
95 spec,
96 ai_config,
97 response_selection_mode,
98 response_selector,
99 persona,
100 }
101 }
102
103 fn parse_ai_config(operation: &Operation) -> Option<AiResponseConfig> {
105 if let Some(ai_config_value) = operation.extensions.get("x-mockforge-ai") {
107 match serde_json::from_value::<AiResponseConfig>(ai_config_value.clone()) {
109 Ok(config) => {
110 if config.is_active() {
111 tracing::debug!(
112 "Parsed AI config for operation {}: mode={:?}, prompt={:?}",
113 operation.operation_id.as_deref().unwrap_or("unknown"),
114 config.mode,
115 config.prompt
116 );
117 return Some(config);
118 }
119 }
120 Err(e) => {
121 tracing::warn!(
122 "Failed to parse x-mockforge-ai extension for operation {}: {}",
123 operation.operation_id.as_deref().unwrap_or("unknown"),
124 e
125 );
126 }
127 }
128 }
129 None
130 }
131
132 fn parse_response_selection_mode(operation: &Operation) -> ResponseSelectionMode {
134 let op_id = operation.operation_id.as_deref().unwrap_or("unknown");
136
137 if let Ok(op_env_var) = std::env::var(format!(
139 "MOCKFORGE_RESPONSE_SELECTION_{}",
140 op_id.to_uppercase().replace('-', "_")
141 )) {
142 if let Some(mode) = ResponseSelectionMode::from_str(&op_env_var) {
143 tracing::debug!(
144 "Using response selection mode from env var for operation {}: {:?}",
145 op_id,
146 mode
147 );
148 return mode;
149 }
150 }
151
152 if let Ok(global_mode_str) = std::env::var("MOCKFORGE_RESPONSE_SELECTION_MODE") {
154 if let Some(mode) = ResponseSelectionMode::from_str(&global_mode_str) {
155 tracing::debug!("Using global response selection mode from env var: {:?}", mode);
156 return mode;
157 }
158 }
159
160 if let Some(selection_value) = operation.extensions.get("x-mockforge-response-selection") {
162 if let Some(mode_str) = selection_value.as_str() {
164 if let Some(mode) = ResponseSelectionMode::from_str(mode_str) {
165 tracing::debug!(
166 "Parsed response selection mode for operation {}: {:?}",
167 op_id,
168 mode
169 );
170 return mode;
171 }
172 }
173 if let Some(obj) = selection_value.as_object() {
175 if let Some(mode_str) = obj.get("mode").and_then(|v| v.as_str()) {
176 if let Some(mode) = ResponseSelectionMode::from_str(mode_str) {
177 tracing::debug!(
178 "Parsed response selection mode for operation {}: {:?}",
179 op_id,
180 mode
181 );
182 return mode;
183 }
184 }
185 }
186 tracing::warn!(
187 "Failed to parse x-mockforge-response-selection extension for operation {}",
188 op_id
189 );
190 }
191 ResponseSelectionMode::First
193 }
194
195 pub fn from_operation(
197 method: &str,
198 path: String,
199 operation: &Operation,
200 spec: Arc<OpenApiSpec>,
201 ) -> Self {
202 Self::from_operation_with_persona(method, path, operation, spec, None)
203 }
204
205 pub fn from_operation_with_persona(
207 method: &str,
208 path: String,
209 operation: &Operation,
210 spec: Arc<OpenApiSpec>,
211 persona: Option<Arc<Persona>>,
212 ) -> Self {
213 Self::new_with_persona(method.to_string(), path, operation.clone(), spec, persona)
214 }
215
216 pub fn axum_path(&self) -> String {
218 let path = self.path.split('?').next().unwrap_or(&self.path);
221
222 if path.contains("(") && path.contains("={") {
226 let mut result = String::with_capacity(path.len());
227 let mut chars = path.chars().peekable();
228
229 while let Some(ch) = chars.next() {
230 if ch == '(' {
231 let mut paren_content = String::new();
233 for c in chars.by_ref() {
234 if c == ')' {
235 break;
236 }
237 paren_content.push(c);
238 }
239 for part in paren_content.split(',') {
241 if let Some((_key, value)) = part.split_once('=') {
242 let param = value.trim_matches(|c| c == '\'' || c == '"');
243 result.push('/');
244 result.push_str(param);
245 }
246 }
247 } else {
248 result.push(ch);
249 }
250 }
251 return result;
252 }
253
254 path.to_string()
255 }
256
257 pub fn is_valid_axum_path(&self) -> bool {
262 let path = self.axum_path();
263 for segment in path.split('/') {
265 let brace_count = segment.matches('{').count();
266 if brace_count > 1 {
267 return false;
268 }
269 if brace_count == 1
272 && segment
273 != format!(
274 "{{{}}}",
275 segment
276 .trim_matches(|c: char| c != '{' && c != '}')
277 .trim_matches(|c| c == '{' || c == '}')
278 )
279 {
280 if !segment.starts_with('{') || !segment.ends_with('}') {
283 return false;
284 }
285 }
286 }
287 true
288 }
289
290 pub fn with_metadata(mut self, key: String, value: String) -> Self {
292 self.metadata.insert(key, value);
293 self
294 }
295
296 pub async fn mock_response_with_status_async(
305 &self,
306 context: &crate::ai_response::RequestContext,
307 ai_generator: Option<&dyn crate::openapi::response::AiGenerator>,
308 ) -> (u16, serde_json::Value) {
309 use crate::openapi::response::ResponseGenerator;
310
311 let status_code = self.find_first_available_status_code();
313
314 if let Some(ai_config) = &self.ai_config {
316 if ai_config.is_active() {
317 tracing::info!(
318 "Using AI-assisted response generation for {} {}",
319 self.method,
320 self.path
321 );
322
323 match ResponseGenerator::generate_ai_response(ai_config, context, ai_generator)
324 .await
325 {
326 Ok(response_body) => {
327 tracing::debug!(
328 "AI response generated successfully for {} {}: {:?}",
329 self.method,
330 self.path,
331 response_body
332 );
333 return (status_code, response_body);
334 }
335 Err(e) => {
336 tracing::warn!(
337 "AI response generation failed for {} {}: {}, falling back to standard generation",
338 self.method,
339 self.path,
340 e
341 );
342 }
344 }
345 }
346 }
347
348 let expand_tokens = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
350 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
351 .unwrap_or(false);
352
353 let mode = Some(self.response_selection_mode);
355 let selector = Some(self.response_selector.as_ref());
356
357 let persona_ref = self.persona.as_deref();
359
360 match ResponseGenerator::generate_response_with_expansion_and_mode_and_persona(
361 &self.spec,
362 &self.operation,
363 status_code,
364 Some("application/json"),
365 expand_tokens,
366 mode,
367 selector,
368 persona_ref,
369 ) {
370 Ok(response_body) => {
371 tracing::debug!(
372 "ResponseGenerator succeeded for {} {} with status {}: {:?}",
373 self.method,
374 self.path,
375 status_code,
376 response_body
377 );
378 (status_code, response_body)
379 }
380 Err(e) => {
381 tracing::debug!(
382 "ResponseGenerator failed for {} {}: {}, using fallback",
383 self.method,
384 self.path,
385 e
386 );
387 let response_body = serde_json::json!({
389 "message": format!("Mock response for {} {}", self.method, self.path),
390 "operation_id": self.operation.operation_id,
391 "status": status_code
392 });
393 (status_code, response_body)
394 }
395 }
396 }
397
398 pub fn mock_response_with_status(&self) -> (u16, serde_json::Value) {
403 self.mock_response_with_status_and_scenario(None)
404 }
405
406 pub fn mock_response_with_status_and_scenario(
418 &self,
419 scenario: Option<&str>,
420 ) -> (u16, serde_json::Value) {
421 self.mock_response_with_status_and_scenario_and_override(scenario, None)
422 }
423
424 pub fn mock_response_with_status_and_scenario_and_override(
430 &self,
431 scenario: Option<&str>,
432 status_override: Option<u16>,
433 ) -> (u16, serde_json::Value) {
434 let (status, body, _) =
435 self.mock_response_with_status_and_scenario_and_trace(scenario, status_override);
436 (status, body)
437 }
438
439 pub fn mock_response_with_status_and_scenario_and_trace(
443 &self,
444 scenario: Option<&str>,
445 status_override: Option<u16>,
446 ) -> (
447 u16,
448 serde_json::Value,
449 crate::reality_continuum::response_trace::ResponseGenerationTrace,
450 ) {
451 use crate::openapi::response_trace;
452 use crate::reality_continuum::response_trace::ResponseGenerationTrace;
453
454 let status_code = status_override
456 .filter(|code| self.has_response_for_status(*code))
457 .unwrap_or_else(|| self.find_first_available_status_code());
458
459 let expand_tokens = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
461 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
462 .unwrap_or(false);
463
464 let mode = Some(self.response_selection_mode);
466 let selector = Some(self.response_selector.as_ref());
467
468 match response_trace::generate_response_with_trace(
470 &self.spec,
471 &self.operation,
472 status_code,
473 Some("application/json"),
474 expand_tokens,
475 scenario,
476 mode,
477 selector,
478 None, ) {
480 Ok((response_body, trace)) => {
481 tracing::debug!(
482 "ResponseGenerator succeeded for {} {} with status {} and scenario {:?}: {:?}",
483 self.method,
484 self.path,
485 status_code,
486 scenario,
487 response_body
488 );
489 (status_code, response_body, trace)
490 }
491 Err(e) => {
492 tracing::debug!(
493 "ResponseGenerator failed for {} {}: {}, using fallback",
494 self.method,
495 self.path,
496 e
497 );
498 let response_body = serde_json::json!({
500 "message": format!("Mock response for {} {}", self.method, self.path),
501 "operation_id": self.operation.operation_id,
502 "status": status_code
503 });
504 let mut trace = ResponseGenerationTrace::new();
506 trace.set_final_payload(response_body.clone());
507 trace.add_metadata("fallback".to_string(), serde_json::json!(true));
508 trace.add_metadata("error".to_string(), serde_json::json!(e.to_string()));
509 (status_code, response_body, trace)
510 }
511 }
512 }
513
514 pub fn has_response_for_status(&self, code: u16) -> bool {
516 self.operation
517 .responses
518 .responses
519 .iter()
520 .any(|(status, _)| matches!(status, openapiv3::StatusCode::Code(c) if *c == code))
521 }
522
523 pub fn find_first_available_status_code(&self) -> u16 {
525 for (status, _) in &self.operation.responses.responses {
527 match status {
528 openapiv3::StatusCode::Code(code) => {
529 return *code;
530 }
531 openapiv3::StatusCode::Range(range) => {
532 match range {
534 2 => return 200, 3 => return 300, 4 => return 400, 5 => return 500, _ => continue, }
540 }
541 }
542 }
543
544 if self.operation.responses.default.is_some() {
546 return 200; }
548
549 200
551 }
552}
553
554#[derive(Debug, Clone)]
556pub struct OpenApiOperation {
557 pub method: String,
559 pub path: String,
561 pub operation: Operation,
563}
564
565impl OpenApiOperation {
566 pub fn new(method: String, path: String, operation: Operation) -> Self {
568 Self {
569 method,
570 path,
571 operation,
572 }
573 }
574}
575
576pub struct RouteGenerator;
578
579impl RouteGenerator {
580 pub fn generate_routes_from_path(
582 path: &str,
583 path_item: &ReferenceOr<PathItem>,
584 spec: &Arc<OpenApiSpec>,
585 ) -> Result<Vec<OpenApiRoute>> {
586 Self::generate_routes_from_path_with_persona(path, path_item, spec, None)
587 }
588
589 pub fn generate_routes_from_path_with_persona(
591 path: &str,
592 path_item: &ReferenceOr<PathItem>,
593 spec: &Arc<OpenApiSpec>,
594 persona: Option<Arc<Persona>>,
595 ) -> Result<Vec<OpenApiRoute>> {
596 let mut routes = Vec::new();
597
598 if let Some(item) = path_item.as_item() {
599 if let Some(op) = &item.get {
601 routes.push(OpenApiRoute::new_with_persona(
602 "GET".to_string(),
603 path.to_string(),
604 op.clone(),
605 spec.clone(),
606 persona.clone(),
607 ));
608 }
609 if let Some(op) = &item.post {
610 routes.push(OpenApiRoute::new_with_persona(
611 "POST".to_string(),
612 path.to_string(),
613 op.clone(),
614 spec.clone(),
615 persona.clone(),
616 ));
617 }
618 if let Some(op) = &item.put {
619 routes.push(OpenApiRoute::new_with_persona(
620 "PUT".to_string(),
621 path.to_string(),
622 op.clone(),
623 spec.clone(),
624 persona.clone(),
625 ));
626 }
627 if let Some(op) = &item.delete {
628 routes.push(OpenApiRoute::new_with_persona(
629 "DELETE".to_string(),
630 path.to_string(),
631 op.clone(),
632 spec.clone(),
633 persona.clone(),
634 ));
635 }
636 if let Some(op) = &item.patch {
637 routes.push(OpenApiRoute::new_with_persona(
638 "PATCH".to_string(),
639 path.to_string(),
640 op.clone(),
641 spec.clone(),
642 persona.clone(),
643 ));
644 }
645 if let Some(op) = &item.head {
646 routes.push(OpenApiRoute::new_with_persona(
647 "HEAD".to_string(),
648 path.to_string(),
649 op.clone(),
650 spec.clone(),
651 persona.clone(),
652 ));
653 }
654 if let Some(op) = &item.options {
655 routes.push(OpenApiRoute::new_with_persona(
656 "OPTIONS".to_string(),
657 path.to_string(),
658 op.clone(),
659 spec.clone(),
660 persona.clone(),
661 ));
662 }
663 if let Some(op) = &item.trace {
664 routes.push(OpenApiRoute::new_with_persona(
665 "TRACE".to_string(),
666 path.to_string(),
667 op.clone(),
668 spec.clone(),
669 persona.clone(),
670 ));
671 }
672 }
673
674 Ok(routes)
675 }
676}