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