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(
350 &self,
351 scenario: Option<&str>,
352 ) -> (u16, serde_json::Value) {
353 let (status, body, _) = self.mock_response_with_status_and_scenario_and_trace(scenario);
354 (status, body)
355 }
356
357 pub fn mock_response_with_status_and_scenario_and_trace(
361 &self,
362 scenario: Option<&str>,
363 ) -> (
364 u16,
365 serde_json::Value,
366 crate::reality_continuum::response_trace::ResponseGenerationTrace,
367 ) {
368 use crate::openapi::response_trace;
369 use crate::reality_continuum::response_trace::ResponseGenerationTrace;
370
371 let status_code = self.find_first_available_status_code();
373
374 let expand_tokens = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
376 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
377 .unwrap_or(false);
378
379 let mode = Some(self.response_selection_mode);
381 let selector = Some(self.response_selector.as_ref());
382
383 match response_trace::generate_response_with_trace(
385 &self.spec,
386 &self.operation,
387 status_code,
388 Some("application/json"),
389 expand_tokens,
390 scenario,
391 mode,
392 selector,
393 None, ) {
395 Ok((response_body, trace)) => {
396 tracing::debug!(
397 "ResponseGenerator succeeded for {} {} with status {} and scenario {:?}: {:?}",
398 self.method,
399 self.path,
400 status_code,
401 scenario,
402 response_body
403 );
404 (status_code, response_body, trace)
405 }
406 Err(e) => {
407 tracing::debug!(
408 "ResponseGenerator failed for {} {}: {}, using fallback",
409 self.method,
410 self.path,
411 e
412 );
413 let response_body = serde_json::json!({
415 "message": format!("Mock response for {} {}", self.method, self.path),
416 "operation_id": self.operation.operation_id,
417 "status": status_code
418 });
419 let mut trace = ResponseGenerationTrace::new();
421 trace.set_final_payload(response_body.clone());
422 trace.add_metadata("fallback".to_string(), serde_json::json!(true));
423 trace.add_metadata("error".to_string(), serde_json::json!(e.to_string()));
424 (status_code, response_body, trace)
425 }
426 }
427 }
428
429 fn find_first_available_status_code(&self) -> u16 {
431 for (status, _) in &self.operation.responses.responses {
433 match status {
434 openapiv3::StatusCode::Code(code) => {
435 return *code;
436 }
437 openapiv3::StatusCode::Range(range) => {
438 match range {
440 2 => return 200, 3 => return 300, 4 => return 400, 5 => return 500, _ => continue, }
446 }
447 }
448 }
449
450 if self.operation.responses.default.is_some() {
452 return 200; }
454
455 200
457 }
458}
459
460#[derive(Debug, Clone)]
462pub struct OpenApiOperation {
463 pub method: String,
465 pub path: String,
467 pub operation: Operation,
469}
470
471impl OpenApiOperation {
472 pub fn new(method: String, path: String, operation: Operation) -> Self {
474 Self {
475 method,
476 path,
477 operation,
478 }
479 }
480}
481
482pub struct RouteGenerator;
484
485impl RouteGenerator {
486 pub fn generate_routes_from_path(
488 path: &str,
489 path_item: &ReferenceOr<PathItem>,
490 spec: &Arc<OpenApiSpec>,
491 ) -> Result<Vec<OpenApiRoute>> {
492 Self::generate_routes_from_path_with_persona(path, path_item, spec, None)
493 }
494
495 pub fn generate_routes_from_path_with_persona(
497 path: &str,
498 path_item: &ReferenceOr<PathItem>,
499 spec: &Arc<OpenApiSpec>,
500 persona: Option<Arc<Persona>>,
501 ) -> Result<Vec<OpenApiRoute>> {
502 let mut routes = Vec::new();
503
504 if let Some(item) = path_item.as_item() {
505 if let Some(op) = &item.get {
507 routes.push(OpenApiRoute::new_with_persona(
508 "GET".to_string(),
509 path.to_string(),
510 op.clone(),
511 spec.clone(),
512 persona.clone(),
513 ));
514 }
515 if let Some(op) = &item.post {
516 routes.push(OpenApiRoute::new_with_persona(
517 "POST".to_string(),
518 path.to_string(),
519 op.clone(),
520 spec.clone(),
521 persona.clone(),
522 ));
523 }
524 if let Some(op) = &item.put {
525 routes.push(OpenApiRoute::new_with_persona(
526 "PUT".to_string(),
527 path.to_string(),
528 op.clone(),
529 spec.clone(),
530 persona.clone(),
531 ));
532 }
533 if let Some(op) = &item.delete {
534 routes.push(OpenApiRoute::new_with_persona(
535 "DELETE".to_string(),
536 path.to_string(),
537 op.clone(),
538 spec.clone(),
539 persona.clone(),
540 ));
541 }
542 if let Some(op) = &item.patch {
543 routes.push(OpenApiRoute::new_with_persona(
544 "PATCH".to_string(),
545 path.to_string(),
546 op.clone(),
547 spec.clone(),
548 persona.clone(),
549 ));
550 }
551 if let Some(op) = &item.head {
552 routes.push(OpenApiRoute::new_with_persona(
553 "HEAD".to_string(),
554 path.to_string(),
555 op.clone(),
556 spec.clone(),
557 persona.clone(),
558 ));
559 }
560 if let Some(op) = &item.options {
561 routes.push(OpenApiRoute::new_with_persona(
562 "OPTIONS".to_string(),
563 path.to_string(),
564 op.clone(),
565 spec.clone(),
566 persona.clone(),
567 ));
568 }
569 if let Some(op) = &item.trace {
570 routes.push(OpenApiRoute::new_with_persona(
571 "TRACE".to_string(),
572 path.to_string(),
573 op.clone(),
574 spec.clone(),
575 persona.clone(),
576 ));
577 }
578 }
579
580 Ok(routes)
581 }
582}