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("()") {
224 let path = path.replace("()", "");
225 return path;
226 }
227
228 if path.contains('(') && path.contains('=') {
233 let mut result = String::with_capacity(path.len());
234 let mut chars = path.chars().peekable();
235
236 while let Some(ch) = chars.next() {
237 if ch == '(' {
238 let mut paren_content = String::new();
240 for c in chars.by_ref() {
241 if c == ')' {
242 break;
243 }
244 paren_content.push(c);
245 }
246 for part in paren_content.split(',') {
248 if let Some((_key, value)) = part.split_once('=') {
249 let param = value.trim_matches(|c| c == '\'' || c == '"');
250 result.push('/');
251 result.push_str(param);
252 }
253 }
254 } else {
255 result.push(ch);
256 }
257 }
258 return result;
259 }
260
261 path.to_string()
262 }
263
264 pub fn is_valid_axum_path(&self) -> bool {
269 let path = self.axum_path();
270 if path.contains('(') || path.contains(')') {
272 return false;
273 }
274 for segment in path.split('/') {
276 let brace_count = segment.matches('{').count();
277 if brace_count > 1 {
278 return false;
279 }
280 if brace_count == 1
283 && segment
284 != format!(
285 "{{{}}}",
286 segment
287 .trim_matches(|c: char| c != '{' && c != '}')
288 .trim_matches(|c| c == '{' || c == '}')
289 )
290 {
291 if !segment.starts_with('{') || !segment.ends_with('}') {
294 return false;
295 }
296 }
297 }
298 true
299 }
300
301 pub fn with_metadata(mut self, key: String, value: String) -> Self {
303 self.metadata.insert(key, value);
304 self
305 }
306
307 pub async fn mock_response_with_status_async(
316 &self,
317 context: &crate::ai_response::RequestContext,
318 ai_generator: Option<&dyn crate::openapi::response::AiGenerator>,
319 ) -> (u16, serde_json::Value) {
320 use crate::openapi::response::ResponseGenerator;
321
322 let status_code = self.find_first_available_status_code();
324
325 if let Some(ai_config) = &self.ai_config {
327 if ai_config.is_active() {
328 tracing::info!(
329 "Using AI-assisted response generation for {} {}",
330 self.method,
331 self.path
332 );
333
334 match ResponseGenerator::generate_ai_response(ai_config, context, ai_generator)
335 .await
336 {
337 Ok(response_body) => {
338 tracing::debug!(
339 "AI response generated successfully for {} {}: {:?}",
340 self.method,
341 self.path,
342 response_body
343 );
344 return (status_code, response_body);
345 }
346 Err(e) => {
347 tracing::warn!(
348 "AI response generation failed for {} {}: {}, falling back to standard generation",
349 self.method,
350 self.path,
351 e
352 );
353 }
355 }
356 }
357 }
358
359 let expand_tokens = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
361 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
362 .unwrap_or(false);
363
364 let mode = Some(self.response_selection_mode);
366 let selector = Some(self.response_selector.as_ref());
367
368 let persona_ref = self.persona.as_deref();
370
371 match ResponseGenerator::generate_response_with_expansion_and_mode_and_persona(
372 &self.spec,
373 &self.operation,
374 status_code,
375 Some("application/json"),
376 expand_tokens,
377 mode,
378 selector,
379 persona_ref,
380 ) {
381 Ok(response_body) => {
382 tracing::debug!(
383 "ResponseGenerator succeeded for {} {} with status {}: {:?}",
384 self.method,
385 self.path,
386 status_code,
387 response_body
388 );
389 (status_code, response_body)
390 }
391 Err(e) => {
392 tracing::debug!(
393 "ResponseGenerator failed for {} {}: {}, using fallback",
394 self.method,
395 self.path,
396 e
397 );
398 let response_body = serde_json::json!({
400 "message": format!("Mock response for {} {}", self.method, self.path),
401 "operation_id": self.operation.operation_id,
402 "status": status_code
403 });
404 (status_code, response_body)
405 }
406 }
407 }
408
409 pub fn mock_response_with_status(&self) -> (u16, serde_json::Value) {
414 self.mock_response_with_status_and_scenario(None)
415 }
416
417 pub fn mock_response_with_status_and_scenario(
429 &self,
430 scenario: Option<&str>,
431 ) -> (u16, serde_json::Value) {
432 self.mock_response_with_status_and_scenario_and_override(scenario, None)
433 }
434
435 pub fn mock_response_with_status_and_scenario_and_override(
441 &self,
442 scenario: Option<&str>,
443 status_override: Option<u16>,
444 ) -> (u16, serde_json::Value) {
445 let (status, body, _) =
446 self.mock_response_with_status_and_scenario_and_trace(scenario, status_override);
447 (status, body)
448 }
449
450 pub fn mock_response_with_status_and_scenario_and_trace(
454 &self,
455 scenario: Option<&str>,
456 status_override: Option<u16>,
457 ) -> (
458 u16,
459 serde_json::Value,
460 crate::reality_continuum::response_trace::ResponseGenerationTrace,
461 ) {
462 use crate::openapi::response_trace;
463 use crate::reality_continuum::response_trace::ResponseGenerationTrace;
464
465 let status_code = status_override
467 .filter(|code| self.has_response_for_status(*code))
468 .unwrap_or_else(|| self.find_first_available_status_code());
469
470 let expand_tokens = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
472 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
473 .unwrap_or(false);
474
475 let mode = Some(self.response_selection_mode);
477 let selector = Some(self.response_selector.as_ref());
478
479 match response_trace::generate_response_with_trace(
481 &self.spec,
482 &self.operation,
483 status_code,
484 Some("application/json"),
485 expand_tokens,
486 scenario,
487 mode,
488 selector,
489 None, ) {
491 Ok((response_body, trace)) => {
492 tracing::debug!(
493 "ResponseGenerator succeeded for {} {} with status {} and scenario {:?}: {:?}",
494 self.method,
495 self.path,
496 status_code,
497 scenario,
498 response_body
499 );
500 (status_code, response_body, trace)
501 }
502 Err(e) => {
503 tracing::debug!(
504 "ResponseGenerator failed for {} {}: {}, using fallback",
505 self.method,
506 self.path,
507 e
508 );
509 let response_body = serde_json::json!({
511 "message": format!("Mock response for {} {}", self.method, self.path),
512 "operation_id": self.operation.operation_id,
513 "status": status_code
514 });
515 let mut trace = ResponseGenerationTrace::new();
517 trace.set_final_payload(response_body.clone());
518 trace.add_metadata("fallback".to_string(), serde_json::json!(true));
519 trace.add_metadata("error".to_string(), serde_json::json!(e.to_string()));
520 (status_code, response_body, trace)
521 }
522 }
523 }
524
525 pub fn has_response_for_status(&self, code: u16) -> bool {
527 self.operation
528 .responses
529 .responses
530 .iter()
531 .any(|(status, _)| matches!(status, openapiv3::StatusCode::Code(c) if *c == code))
532 }
533
534 pub fn find_first_available_status_code(&self) -> u16 {
536 for (status, _) in &self.operation.responses.responses {
538 match status {
539 openapiv3::StatusCode::Code(code) => {
540 return *code;
541 }
542 openapiv3::StatusCode::Range(range) => {
543 match range {
545 2 => return 200, 3 => return 300, 4 => return 400, 5 => return 500, _ => continue, }
551 }
552 }
553 }
554
555 if self.operation.responses.default.is_some() {
557 return 200; }
559
560 200
562 }
563}
564
565#[derive(Debug, Clone)]
567pub struct OpenApiOperation {
568 pub method: String,
570 pub path: String,
572 pub operation: Operation,
574}
575
576impl OpenApiOperation {
577 pub fn new(method: String, path: String, operation: Operation) -> Self {
579 Self {
580 method,
581 path,
582 operation,
583 }
584 }
585}
586
587pub struct RouteGenerator;
589
590impl RouteGenerator {
591 pub fn generate_routes_from_path(
593 path: &str,
594 path_item: &ReferenceOr<PathItem>,
595 spec: &Arc<OpenApiSpec>,
596 ) -> Result<Vec<OpenApiRoute>> {
597 Self::generate_routes_from_path_with_persona(path, path_item, spec, None)
598 }
599
600 pub fn generate_routes_from_path_with_persona(
602 path: &str,
603 path_item: &ReferenceOr<PathItem>,
604 spec: &Arc<OpenApiSpec>,
605 persona: Option<Arc<Persona>>,
606 ) -> Result<Vec<OpenApiRoute>> {
607 let mut routes = Vec::new();
608
609 if let Some(item) = path_item.as_item() {
610 if let Some(op) = &item.get {
612 routes.push(OpenApiRoute::new_with_persona(
613 "GET".to_string(),
614 path.to_string(),
615 op.clone(),
616 spec.clone(),
617 persona.clone(),
618 ));
619 }
620 if let Some(op) = &item.post {
621 routes.push(OpenApiRoute::new_with_persona(
622 "POST".to_string(),
623 path.to_string(),
624 op.clone(),
625 spec.clone(),
626 persona.clone(),
627 ));
628 }
629 if let Some(op) = &item.put {
630 routes.push(OpenApiRoute::new_with_persona(
631 "PUT".to_string(),
632 path.to_string(),
633 op.clone(),
634 spec.clone(),
635 persona.clone(),
636 ));
637 }
638 if let Some(op) = &item.delete {
639 routes.push(OpenApiRoute::new_with_persona(
640 "DELETE".to_string(),
641 path.to_string(),
642 op.clone(),
643 spec.clone(),
644 persona.clone(),
645 ));
646 }
647 if let Some(op) = &item.patch {
648 routes.push(OpenApiRoute::new_with_persona(
649 "PATCH".to_string(),
650 path.to_string(),
651 op.clone(),
652 spec.clone(),
653 persona.clone(),
654 ));
655 }
656 if let Some(op) = &item.head {
657 routes.push(OpenApiRoute::new_with_persona(
658 "HEAD".to_string(),
659 path.to_string(),
660 op.clone(),
661 spec.clone(),
662 persona.clone(),
663 ));
664 }
665 if let Some(op) = &item.options {
666 routes.push(OpenApiRoute::new_with_persona(
667 "OPTIONS".to_string(),
668 path.to_string(),
669 op.clone(),
670 spec.clone(),
671 persona.clone(),
672 ));
673 }
674 if let Some(op) = &item.trace {
675 routes.push(OpenApiRoute::new_with_persona(
676 "TRACE".to_string(),
677 path.to_string(),
678 op.clone(),
679 spec.clone(),
680 persona.clone(),
681 ));
682 }
683 }
684
685 Ok(routes)
686 }
687}