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.clone()
220 }
221
222 pub fn with_metadata(mut self, key: String, value: String) -> Self {
224 self.metadata.insert(key, value);
225 self
226 }
227
228 pub async fn mock_response_with_status_async(
237 &self,
238 context: &crate::ai_response::RequestContext,
239 ai_generator: Option<&dyn crate::openapi::response::AiGenerator>,
240 ) -> (u16, serde_json::Value) {
241 use crate::openapi::response::ResponseGenerator;
242
243 let status_code = self.find_first_available_status_code();
245
246 if let Some(ai_config) = &self.ai_config {
248 if ai_config.is_active() {
249 tracing::info!(
250 "Using AI-assisted response generation for {} {}",
251 self.method,
252 self.path
253 );
254
255 match ResponseGenerator::generate_ai_response(ai_config, context, ai_generator)
256 .await
257 {
258 Ok(response_body) => {
259 tracing::debug!(
260 "AI response generated successfully for {} {}: {:?}",
261 self.method,
262 self.path,
263 response_body
264 );
265 return (status_code, response_body);
266 }
267 Err(e) => {
268 tracing::warn!(
269 "AI response generation failed for {} {}: {}, falling back to standard generation",
270 self.method,
271 self.path,
272 e
273 );
274 }
276 }
277 }
278 }
279
280 let expand_tokens = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
282 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
283 .unwrap_or(false);
284
285 let mode = Some(self.response_selection_mode);
287 let selector = Some(self.response_selector.as_ref());
288
289 let persona_ref = self.persona.as_deref();
291
292 match ResponseGenerator::generate_response_with_expansion_and_mode_and_persona(
293 &self.spec,
294 &self.operation,
295 status_code,
296 Some("application/json"),
297 expand_tokens,
298 mode,
299 selector,
300 persona_ref,
301 ) {
302 Ok(response_body) => {
303 tracing::debug!(
304 "ResponseGenerator succeeded for {} {} with status {}: {:?}",
305 self.method,
306 self.path,
307 status_code,
308 response_body
309 );
310 (status_code, response_body)
311 }
312 Err(e) => {
313 tracing::debug!(
314 "ResponseGenerator failed for {} {}: {}, using fallback",
315 self.method,
316 self.path,
317 e
318 );
319 let response_body = serde_json::json!({
321 "message": format!("Mock response for {} {}", self.method, self.path),
322 "operation_id": self.operation.operation_id,
323 "status": status_code
324 });
325 (status_code, response_body)
326 }
327 }
328 }
329
330 pub fn mock_response_with_status(&self) -> (u16, serde_json::Value) {
335 self.mock_response_with_status_and_scenario(None)
336 }
337
338 pub fn mock_response_with_status_and_scenario(
349 &self,
350 scenario: Option<&str>,
351 ) -> (u16, serde_json::Value) {
352 let (status, body, _) = self.mock_response_with_status_and_scenario_and_trace(scenario);
353 (status, body)
354 }
355
356 pub fn mock_response_with_status_and_scenario_and_trace(
360 &self,
361 scenario: Option<&str>,
362 ) -> (
363 u16,
364 serde_json::Value,
365 crate::reality_continuum::response_trace::ResponseGenerationTrace,
366 ) {
367 use crate::openapi::response_trace;
368 use crate::reality_continuum::response_trace::ResponseGenerationTrace;
369
370 let status_code = self.find_first_available_status_code();
372
373 let expand_tokens = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
375 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
376 .unwrap_or(false);
377
378 let mode = Some(self.response_selection_mode);
380 let selector = Some(self.response_selector.as_ref());
381
382 match response_trace::generate_response_with_trace(
384 &self.spec,
385 &self.operation,
386 status_code,
387 Some("application/json"),
388 expand_tokens,
389 scenario,
390 mode,
391 selector,
392 None, ) {
394 Ok((response_body, trace)) => {
395 tracing::debug!(
396 "ResponseGenerator succeeded for {} {} with status {} and scenario {:?}: {:?}",
397 self.method,
398 self.path,
399 status_code,
400 scenario,
401 response_body
402 );
403 (status_code, response_body, trace)
404 }
405 Err(e) => {
406 tracing::debug!(
407 "ResponseGenerator failed for {} {}: {}, using fallback",
408 self.method,
409 self.path,
410 e
411 );
412 let response_body = serde_json::json!({
414 "message": format!("Mock response for {} {}", self.method, self.path),
415 "operation_id": self.operation.operation_id,
416 "status": status_code
417 });
418 let mut trace = ResponseGenerationTrace::new();
420 trace.set_final_payload(response_body.clone());
421 trace.add_metadata("fallback".to_string(), serde_json::json!(true));
422 trace.add_metadata("error".to_string(), serde_json::json!(e.to_string()));
423 (status_code, response_body, trace)
424 }
425 }
426 }
427
428 fn find_first_available_status_code(&self) -> u16 {
430 for (status, _) in &self.operation.responses.responses {
432 match status {
433 openapiv3::StatusCode::Code(code) => {
434 return *code;
435 }
436 openapiv3::StatusCode::Range(range) => {
437 match range {
439 2 => return 200, 3 => return 300, 4 => return 400, 5 => return 500, _ => continue, }
445 }
446 }
447 }
448
449 if self.operation.responses.default.is_some() {
451 return 200; }
453
454 200
456 }
457}
458
459#[derive(Debug, Clone)]
461pub struct OpenApiOperation {
462 pub method: String,
464 pub path: String,
466 pub operation: Operation,
468}
469
470impl OpenApiOperation {
471 pub fn new(method: String, path: String, operation: Operation) -> Self {
473 Self {
474 method,
475 path,
476 operation,
477 }
478 }
479}
480
481pub struct RouteGenerator;
483
484impl RouteGenerator {
485 pub fn generate_routes_from_path(
487 path: &str,
488 path_item: &ReferenceOr<PathItem>,
489 spec: &Arc<OpenApiSpec>,
490 ) -> Result<Vec<OpenApiRoute>> {
491 Self::generate_routes_from_path_with_persona(path, path_item, spec, None)
492 }
493
494 pub fn generate_routes_from_path_with_persona(
496 path: &str,
497 path_item: &ReferenceOr<PathItem>,
498 spec: &Arc<OpenApiSpec>,
499 persona: Option<Arc<Persona>>,
500 ) -> Result<Vec<OpenApiRoute>> {
501 let mut routes = Vec::new();
502
503 if let Some(item) = path_item.as_item() {
504 if let Some(op) = &item.get {
506 routes.push(OpenApiRoute::new_with_persona(
507 "GET".to_string(),
508 path.to_string(),
509 op.clone(),
510 spec.clone(),
511 persona.clone(),
512 ));
513 }
514 if let Some(op) = &item.post {
515 routes.push(OpenApiRoute::new_with_persona(
516 "POST".to_string(),
517 path.to_string(),
518 op.clone(),
519 spec.clone(),
520 persona.clone(),
521 ));
522 }
523 if let Some(op) = &item.put {
524 routes.push(OpenApiRoute::new_with_persona(
525 "PUT".to_string(),
526 path.to_string(),
527 op.clone(),
528 spec.clone(),
529 persona.clone(),
530 ));
531 }
532 if let Some(op) = &item.delete {
533 routes.push(OpenApiRoute::new_with_persona(
534 "DELETE".to_string(),
535 path.to_string(),
536 op.clone(),
537 spec.clone(),
538 persona.clone(),
539 ));
540 }
541 if let Some(op) = &item.patch {
542 routes.push(OpenApiRoute::new_with_persona(
543 "PATCH".to_string(),
544 path.to_string(),
545 op.clone(),
546 spec.clone(),
547 persona.clone(),
548 ));
549 }
550 if let Some(op) = &item.head {
551 routes.push(OpenApiRoute::new_with_persona(
552 "HEAD".to_string(),
553 path.to_string(),
554 op.clone(),
555 spec.clone(),
556 persona.clone(),
557 ));
558 }
559 if let Some(op) = &item.options {
560 routes.push(OpenApiRoute::new_with_persona(
561 "OPTIONS".to_string(),
562 path.to_string(),
563 op.clone(),
564 spec.clone(),
565 persona.clone(),
566 ));
567 }
568 if let Some(op) = &item.trace {
569 routes.push(OpenApiRoute::new_with_persona(
570 "TRACE".to_string(),
571 path.to_string(),
572 op.clone(),
573 spec.clone(),
574 persona.clone(),
575 ));
576 }
577 }
578
579 Ok(routes)
580 }
581}