1use crate::openapi::response_selection::{ResponseSelectionMode, ResponseSelector};
7use crate::{ai_response::AiResponseConfig, openapi::spec::OpenApiSpec, Result};
8use crate::intelligent_behavior::config::Persona;
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 Some(op_env_var) = std::env::var(format!(
139 "MOCKFORGE_RESPONSE_SELECTION_{}",
140 op_id.to_uppercase().replace('-', "_")
141 ))
142 .ok()
143 {
144 if let Some(mode) = ResponseSelectionMode::from_str(&op_env_var) {
145 tracing::debug!(
146 "Using response selection mode from env var for operation {}: {:?}",
147 op_id,
148 mode
149 );
150 return mode;
151 }
152 }
153
154 if let Some(global_mode_str) = std::env::var("MOCKFORGE_RESPONSE_SELECTION_MODE").ok() {
156 if let Some(mode) = ResponseSelectionMode::from_str(&global_mode_str) {
157 tracing::debug!("Using global response selection mode from env var: {:?}", mode);
158 return mode;
159 }
160 }
161
162 if let Some(selection_value) = operation.extensions.get("x-mockforge-response-selection") {
164 if let Some(mode_str) = selection_value.as_str() {
166 if let Some(mode) = ResponseSelectionMode::from_str(mode_str) {
167 tracing::debug!(
168 "Parsed response selection mode for operation {}: {:?}",
169 op_id,
170 mode
171 );
172 return mode;
173 }
174 }
175 if let Some(obj) = selection_value.as_object() {
177 if let Some(mode_str) = obj.get("mode").and_then(|v| v.as_str()) {
178 if let Some(mode) = ResponseSelectionMode::from_str(mode_str) {
179 tracing::debug!(
180 "Parsed response selection mode for operation {}: {:?}",
181 op_id,
182 mode
183 );
184 return mode;
185 }
186 }
187 }
188 tracing::warn!(
189 "Failed to parse x-mockforge-response-selection extension for operation {}",
190 op_id
191 );
192 }
193 ResponseSelectionMode::First
195 }
196
197 pub fn from_operation(
199 method: &str,
200 path: String,
201 operation: &Operation,
202 spec: Arc<OpenApiSpec>,
203 ) -> Self {
204 Self::from_operation_with_persona(method, path, operation, spec, None)
205 }
206
207 pub fn from_operation_with_persona(
209 method: &str,
210 path: String,
211 operation: &Operation,
212 spec: Arc<OpenApiSpec>,
213 persona: Option<Arc<Persona>>,
214 ) -> Self {
215 Self::new_with_persona(method.to_string(), path, operation.clone(), spec, persona)
216 }
217
218 pub fn axum_path(&self) -> String {
220 self.path.clone()
222 }
223
224 pub fn with_metadata(mut self, key: String, value: String) -> Self {
226 self.metadata.insert(key, value);
227 self
228 }
229
230 pub async fn mock_response_with_status_async(
239 &self,
240 context: &crate::ai_response::RequestContext,
241 ai_generator: Option<&dyn crate::openapi::response::AiGenerator>,
242 ) -> (u16, serde_json::Value) {
243 use crate::openapi::response::ResponseGenerator;
244
245 let status_code = self.find_first_available_status_code();
247
248 if let Some(ai_config) = &self.ai_config {
250 if ai_config.is_active() {
251 tracing::info!(
252 "Using AI-assisted response generation for {} {}",
253 self.method,
254 self.path
255 );
256
257 match ResponseGenerator::generate_ai_response(ai_config, context, ai_generator)
258 .await
259 {
260 Ok(response_body) => {
261 tracing::debug!(
262 "AI response generated successfully for {} {}: {:?}",
263 self.method,
264 self.path,
265 response_body
266 );
267 return (status_code, response_body);
268 }
269 Err(e) => {
270 tracing::warn!(
271 "AI response generation failed for {} {}: {}, falling back to standard generation",
272 self.method,
273 self.path,
274 e
275 );
276 }
278 }
279 }
280 }
281
282 let expand_tokens = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
284 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
285 .unwrap_or(false);
286
287 let mode = Some(self.response_selection_mode);
289 let selector = Some(self.response_selector.as_ref());
290
291 let persona_ref = self.persona.as_deref();
293
294 match ResponseGenerator::generate_response_with_expansion_and_mode_and_persona(
295 &self.spec,
296 &self.operation,
297 status_code,
298 Some("application/json"),
299 expand_tokens,
300 mode,
301 selector,
302 persona_ref,
303 ) {
304 Ok(response_body) => {
305 tracing::debug!(
306 "ResponseGenerator succeeded for {} {} with status {}: {:?}",
307 self.method,
308 self.path,
309 status_code,
310 response_body
311 );
312 (status_code, response_body)
313 }
314 Err(e) => {
315 tracing::debug!(
316 "ResponseGenerator failed for {} {}: {}, using fallback",
317 self.method,
318 self.path,
319 e
320 );
321 let response_body = serde_json::json!({
323 "message": format!("Mock response for {} {}", self.method, self.path),
324 "operation_id": self.operation.operation_id,
325 "status": status_code
326 });
327 (status_code, response_body)
328 }
329 }
330 }
331
332 pub fn mock_response_with_status(&self) -> (u16, serde_json::Value) {
337 self.mock_response_with_status_and_scenario(None)
338 }
339
340 pub fn mock_response_with_status_and_scenario(
351 &self,
352 scenario: Option<&str>,
353 ) -> (u16, serde_json::Value) {
354 use crate::openapi::response::ResponseGenerator;
355
356 let status_code = self.find_first_available_status_code();
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 match ResponseGenerator::generate_response_with_scenario_and_mode(
369 &self.spec,
370 &self.operation,
371 status_code,
372 Some("application/json"),
373 expand_tokens,
374 scenario,
375 mode,
376 selector,
377 ) {
378 Ok(response_body) => {
379 tracing::debug!(
380 "ResponseGenerator succeeded for {} {} with status {} and scenario {:?}: {:?}",
381 self.method,
382 self.path,
383 status_code,
384 scenario,
385 response_body
386 );
387 (status_code, response_body)
388 }
389 Err(e) => {
390 tracing::debug!(
391 "ResponseGenerator failed for {} {}: {}, using fallback",
392 self.method,
393 self.path,
394 e
395 );
396 let response_body = serde_json::json!({
398 "message": format!("Mock response for {} {}", self.method, self.path),
399 "operation_id": self.operation.operation_id,
400 "status": status_code
401 });
402 (status_code, response_body)
403 }
404 }
405 }
406
407 fn find_first_available_status_code(&self) -> u16 {
409 for (status, _) in &self.operation.responses.responses {
411 match status {
412 openapiv3::StatusCode::Code(code) => {
413 return *code;
414 }
415 openapiv3::StatusCode::Range(range) => {
416 match range {
418 2 => return 200, 3 => return 300, 4 => return 400, 5 => return 500, _ => continue, }
424 }
425 }
426 }
427
428 if self.operation.responses.default.is_some() {
430 return 200; }
432
433 200
435 }
436}
437
438#[derive(Debug, Clone)]
440pub struct OpenApiOperation {
441 pub method: String,
443 pub path: String,
445 pub operation: Operation,
447}
448
449impl OpenApiOperation {
450 pub fn new(method: String, path: String, operation: Operation) -> Self {
452 Self {
453 method,
454 path,
455 operation,
456 }
457 }
458}
459
460pub struct RouteGenerator;
462
463impl RouteGenerator {
464 pub fn generate_routes_from_path(
466 path: &str,
467 path_item: &ReferenceOr<PathItem>,
468 spec: &Arc<OpenApiSpec>,
469 ) -> Result<Vec<OpenApiRoute>> {
470 Self::generate_routes_from_path_with_persona(path, path_item, spec, None)
471 }
472
473 pub fn generate_routes_from_path_with_persona(
475 path: &str,
476 path_item: &ReferenceOr<PathItem>,
477 spec: &Arc<OpenApiSpec>,
478 persona: Option<Arc<Persona>>,
479 ) -> Result<Vec<OpenApiRoute>> {
480 let mut routes = Vec::new();
481
482 if let Some(item) = path_item.as_item() {
483 if let Some(op) = &item.get {
485 routes.push(OpenApiRoute::new_with_persona(
486 "GET".to_string(),
487 path.to_string(),
488 op.clone(),
489 spec.clone(),
490 persona.clone(),
491 ));
492 }
493 if let Some(op) = &item.post {
494 routes.push(OpenApiRoute::new_with_persona(
495 "POST".to_string(),
496 path.to_string(),
497 op.clone(),
498 spec.clone(),
499 persona.clone(),
500 ));
501 }
502 if let Some(op) = &item.put {
503 routes.push(OpenApiRoute::new_with_persona(
504 "PUT".to_string(),
505 path.to_string(),
506 op.clone(),
507 spec.clone(),
508 persona.clone(),
509 ));
510 }
511 if let Some(op) = &item.delete {
512 routes.push(OpenApiRoute::new_with_persona(
513 "DELETE".to_string(),
514 path.to_string(),
515 op.clone(),
516 spec.clone(),
517 persona.clone(),
518 ));
519 }
520 if let Some(op) = &item.patch {
521 routes.push(OpenApiRoute::new_with_persona(
522 "PATCH".to_string(),
523 path.to_string(),
524 op.clone(),
525 spec.clone(),
526 persona.clone(),
527 ));
528 }
529 if let Some(op) = &item.head {
530 routes.push(OpenApiRoute::new_with_persona(
531 "HEAD".to_string(),
532 path.to_string(),
533 op.clone(),
534 spec.clone(),
535 persona.clone(),
536 ));
537 }
538 if let Some(op) = &item.options {
539 routes.push(OpenApiRoute::new_with_persona(
540 "OPTIONS".to_string(),
541 path.to_string(),
542 op.clone(),
543 spec.clone(),
544 persona.clone(),
545 ));
546 }
547 if let Some(op) = &item.trace {
548 routes.push(OpenApiRoute::new_with_persona(
549 "TRACE".to_string(),
550 path.to_string(),
551 op.clone(),
552 spec.clone(),
553 persona.clone(),
554 ));
555 }
556 }
557
558 Ok(routes)
559 }
560}