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 self.path.split('?').next().unwrap_or(&self.path).to_string()
221 }
222
223 pub fn with_metadata(mut self, key: String, value: String) -> Self {
225 self.metadata.insert(key, value);
226 self
227 }
228
229 pub async fn mock_response_with_status_async(
238 &self,
239 context: &crate::ai_response::RequestContext,
240 ai_generator: Option<&dyn crate::openapi::response::AiGenerator>,
241 ) -> (u16, serde_json::Value) {
242 use crate::openapi::response::ResponseGenerator;
243
244 let status_code = self.find_first_available_status_code();
246
247 if let Some(ai_config) = &self.ai_config {
249 if ai_config.is_active() {
250 tracing::info!(
251 "Using AI-assisted response generation for {} {}",
252 self.method,
253 self.path
254 );
255
256 match ResponseGenerator::generate_ai_response(ai_config, context, ai_generator)
257 .await
258 {
259 Ok(response_body) => {
260 tracing::debug!(
261 "AI response generated successfully for {} {}: {:?}",
262 self.method,
263 self.path,
264 response_body
265 );
266 return (status_code, response_body);
267 }
268 Err(e) => {
269 tracing::warn!(
270 "AI response generation failed for {} {}: {}, falling back to standard generation",
271 self.method,
272 self.path,
273 e
274 );
275 }
277 }
278 }
279 }
280
281 let expand_tokens = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
283 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
284 .unwrap_or(false);
285
286 let mode = Some(self.response_selection_mode);
288 let selector = Some(self.response_selector.as_ref());
289
290 let persona_ref = self.persona.as_deref();
292
293 match ResponseGenerator::generate_response_with_expansion_and_mode_and_persona(
294 &self.spec,
295 &self.operation,
296 status_code,
297 Some("application/json"),
298 expand_tokens,
299 mode,
300 selector,
301 persona_ref,
302 ) {
303 Ok(response_body) => {
304 tracing::debug!(
305 "ResponseGenerator succeeded for {} {} with status {}: {:?}",
306 self.method,
307 self.path,
308 status_code,
309 response_body
310 );
311 (status_code, response_body)
312 }
313 Err(e) => {
314 tracing::debug!(
315 "ResponseGenerator failed for {} {}: {}, using fallback",
316 self.method,
317 self.path,
318 e
319 );
320 let response_body = serde_json::json!({
322 "message": format!("Mock response for {} {}", self.method, self.path),
323 "operation_id": self.operation.operation_id,
324 "status": status_code
325 });
326 (status_code, response_body)
327 }
328 }
329 }
330
331 pub fn mock_response_with_status(&self) -> (u16, serde_json::Value) {
336 self.mock_response_with_status_and_scenario(None)
337 }
338
339 pub fn mock_response_with_status_and_scenario(
351 &self,
352 scenario: Option<&str>,
353 ) -> (u16, serde_json::Value) {
354 self.mock_response_with_status_and_scenario_and_override(scenario, None)
355 }
356
357 pub fn mock_response_with_status_and_scenario_and_override(
363 &self,
364 scenario: Option<&str>,
365 status_override: Option<u16>,
366 ) -> (u16, serde_json::Value) {
367 let (status, body, _) =
368 self.mock_response_with_status_and_scenario_and_trace(scenario, status_override);
369 (status, body)
370 }
371
372 pub fn mock_response_with_status_and_scenario_and_trace(
376 &self,
377 scenario: Option<&str>,
378 status_override: Option<u16>,
379 ) -> (
380 u16,
381 serde_json::Value,
382 crate::reality_continuum::response_trace::ResponseGenerationTrace,
383 ) {
384 use crate::openapi::response_trace;
385 use crate::reality_continuum::response_trace::ResponseGenerationTrace;
386
387 let status_code = status_override
389 .filter(|code| self.has_response_for_status(*code))
390 .unwrap_or_else(|| self.find_first_available_status_code());
391
392 let expand_tokens = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
394 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
395 .unwrap_or(false);
396
397 let mode = Some(self.response_selection_mode);
399 let selector = Some(self.response_selector.as_ref());
400
401 match response_trace::generate_response_with_trace(
403 &self.spec,
404 &self.operation,
405 status_code,
406 Some("application/json"),
407 expand_tokens,
408 scenario,
409 mode,
410 selector,
411 None, ) {
413 Ok((response_body, trace)) => {
414 tracing::debug!(
415 "ResponseGenerator succeeded for {} {} with status {} and scenario {:?}: {:?}",
416 self.method,
417 self.path,
418 status_code,
419 scenario,
420 response_body
421 );
422 (status_code, response_body, trace)
423 }
424 Err(e) => {
425 tracing::debug!(
426 "ResponseGenerator failed for {} {}: {}, using fallback",
427 self.method,
428 self.path,
429 e
430 );
431 let response_body = serde_json::json!({
433 "message": format!("Mock response for {} {}", self.method, self.path),
434 "operation_id": self.operation.operation_id,
435 "status": status_code
436 });
437 let mut trace = ResponseGenerationTrace::new();
439 trace.set_final_payload(response_body.clone());
440 trace.add_metadata("fallback".to_string(), serde_json::json!(true));
441 trace.add_metadata("error".to_string(), serde_json::json!(e.to_string()));
442 (status_code, response_body, trace)
443 }
444 }
445 }
446
447 pub fn has_response_for_status(&self, code: u16) -> bool {
449 self.operation
450 .responses
451 .responses
452 .iter()
453 .any(|(status, _)| matches!(status, openapiv3::StatusCode::Code(c) if *c == code))
454 }
455
456 pub fn find_first_available_status_code(&self) -> u16 {
458 for (status, _) in &self.operation.responses.responses {
460 match status {
461 openapiv3::StatusCode::Code(code) => {
462 return *code;
463 }
464 openapiv3::StatusCode::Range(range) => {
465 match range {
467 2 => return 200, 3 => return 300, 4 => return 400, 5 => return 500, _ => continue, }
473 }
474 }
475 }
476
477 if self.operation.responses.default.is_some() {
479 return 200; }
481
482 200
484 }
485}
486
487#[derive(Debug, Clone)]
489pub struct OpenApiOperation {
490 pub method: String,
492 pub path: String,
494 pub operation: Operation,
496}
497
498impl OpenApiOperation {
499 pub fn new(method: String, path: String, operation: Operation) -> Self {
501 Self {
502 method,
503 path,
504 operation,
505 }
506 }
507}
508
509pub struct RouteGenerator;
511
512impl RouteGenerator {
513 pub fn generate_routes_from_path(
515 path: &str,
516 path_item: &ReferenceOr<PathItem>,
517 spec: &Arc<OpenApiSpec>,
518 ) -> Result<Vec<OpenApiRoute>> {
519 Self::generate_routes_from_path_with_persona(path, path_item, spec, None)
520 }
521
522 pub fn generate_routes_from_path_with_persona(
524 path: &str,
525 path_item: &ReferenceOr<PathItem>,
526 spec: &Arc<OpenApiSpec>,
527 persona: Option<Arc<Persona>>,
528 ) -> Result<Vec<OpenApiRoute>> {
529 let mut routes = Vec::new();
530
531 if let Some(item) = path_item.as_item() {
532 if let Some(op) = &item.get {
534 routes.push(OpenApiRoute::new_with_persona(
535 "GET".to_string(),
536 path.to_string(),
537 op.clone(),
538 spec.clone(),
539 persona.clone(),
540 ));
541 }
542 if let Some(op) = &item.post {
543 routes.push(OpenApiRoute::new_with_persona(
544 "POST".to_string(),
545 path.to_string(),
546 op.clone(),
547 spec.clone(),
548 persona.clone(),
549 ));
550 }
551 if let Some(op) = &item.put {
552 routes.push(OpenApiRoute::new_with_persona(
553 "PUT".to_string(),
554 path.to_string(),
555 op.clone(),
556 spec.clone(),
557 persona.clone(),
558 ));
559 }
560 if let Some(op) = &item.delete {
561 routes.push(OpenApiRoute::new_with_persona(
562 "DELETE".to_string(),
563 path.to_string(),
564 op.clone(),
565 spec.clone(),
566 persona.clone(),
567 ));
568 }
569 if let Some(op) = &item.patch {
570 routes.push(OpenApiRoute::new_with_persona(
571 "PATCH".to_string(),
572 path.to_string(),
573 op.clone(),
574 spec.clone(),
575 persona.clone(),
576 ));
577 }
578 if let Some(op) = &item.head {
579 routes.push(OpenApiRoute::new_with_persona(
580 "HEAD".to_string(),
581 path.to_string(),
582 op.clone(),
583 spec.clone(),
584 persona.clone(),
585 ));
586 }
587 if let Some(op) = &item.options {
588 routes.push(OpenApiRoute::new_with_persona(
589 "OPTIONS".to_string(),
590 path.to_string(),
591 op.clone(),
592 spec.clone(),
593 persona.clone(),
594 ));
595 }
596 if let Some(op) = &item.trace {
597 routes.push(OpenApiRoute::new_with_persona(
598 "TRACE".to_string(),
599 path.to_string(),
600 op.clone(),
601 spec.clone(),
602 persona.clone(),
603 ));
604 }
605 }
606
607 Ok(routes)
608 }
609}