1use crate::openapi::response_selection::{ResponseSelectionMode, ResponseSelector};
7use crate::{ai_response::AiResponseConfig, openapi::spec::OpenApiSpec, Result};
8use openapiv3::{Operation, PathItem, ReferenceOr};
9use std::collections::BTreeMap;
10use std::sync::Arc;
11
12fn extract_path_parameters(path_template: &str) -> Vec<String> {
14 let mut params = Vec::new();
15 let mut in_param = false;
16 let mut current_param = String::new();
17
18 for ch in path_template.chars() {
19 match ch {
20 '{' => {
21 in_param = true;
22 current_param.clear();
23 }
24 '}' => {
25 if in_param {
26 params.push(current_param.clone());
27 in_param = false;
28 }
29 }
30 ch if in_param => {
31 current_param.push(ch);
32 }
33 _ => {}
34 }
35 }
36
37 params
38}
39
40#[derive(Debug, Clone)]
42pub struct OpenApiRoute {
43 pub method: String,
45 pub path: String,
47 pub operation: Operation,
49 pub metadata: BTreeMap<String, String>,
51 pub parameters: Vec<String>,
53 pub spec: Arc<OpenApiSpec>,
55 pub ai_config: Option<AiResponseConfig>,
57 pub response_selection_mode: ResponseSelectionMode,
59 pub response_selector: Arc<ResponseSelector>,
61}
62
63impl OpenApiRoute {
64 pub fn new(method: String, path: String, operation: Operation, spec: Arc<OpenApiSpec>) -> Self {
66 let parameters = extract_path_parameters(&path);
67
68 let ai_config = Self::parse_ai_config(&operation);
70
71 let response_selection_mode = Self::parse_response_selection_mode(&operation);
73 let response_selector = Arc::new(ResponseSelector::new(response_selection_mode));
74
75 Self {
76 method,
77 path,
78 operation,
79 metadata: BTreeMap::new(),
80 parameters,
81 spec,
82 ai_config,
83 response_selection_mode,
84 response_selector,
85 }
86 }
87
88 fn parse_ai_config(operation: &Operation) -> Option<AiResponseConfig> {
90 if let Some(ai_config_value) = operation.extensions.get("x-mockforge-ai") {
92 match serde_json::from_value::<AiResponseConfig>(ai_config_value.clone()) {
94 Ok(config) => {
95 if config.is_active() {
96 tracing::debug!(
97 "Parsed AI config for operation {}: mode={:?}, prompt={:?}",
98 operation.operation_id.as_deref().unwrap_or("unknown"),
99 config.mode,
100 config.prompt
101 );
102 return Some(config);
103 }
104 }
105 Err(e) => {
106 tracing::warn!(
107 "Failed to parse x-mockforge-ai extension for operation {}: {}",
108 operation.operation_id.as_deref().unwrap_or("unknown"),
109 e
110 );
111 }
112 }
113 }
114 None
115 }
116
117 fn parse_response_selection_mode(operation: &Operation) -> ResponseSelectionMode {
119 let op_id = operation.operation_id.as_deref().unwrap_or("unknown");
121
122 if let Some(op_env_var) = std::env::var(format!(
124 "MOCKFORGE_RESPONSE_SELECTION_{}",
125 op_id.to_uppercase().replace('-', "_")
126 ))
127 .ok()
128 {
129 if let Some(mode) = ResponseSelectionMode::from_str(&op_env_var) {
130 tracing::debug!(
131 "Using response selection mode from env var for operation {}: {:?}",
132 op_id,
133 mode
134 );
135 return mode;
136 }
137 }
138
139 if let Some(global_mode_str) = std::env::var("MOCKFORGE_RESPONSE_SELECTION_MODE").ok() {
141 if let Some(mode) = ResponseSelectionMode::from_str(&global_mode_str) {
142 tracing::debug!("Using global response selection mode from env var: {:?}", mode);
143 return mode;
144 }
145 }
146
147 if let Some(selection_value) = operation.extensions.get("x-mockforge-response-selection") {
149 if let Some(mode_str) = selection_value.as_str() {
151 if let Some(mode) = ResponseSelectionMode::from_str(mode_str) {
152 tracing::debug!(
153 "Parsed response selection mode for operation {}: {:?}",
154 op_id,
155 mode
156 );
157 return mode;
158 }
159 }
160 if let Some(obj) = selection_value.as_object() {
162 if let Some(mode_str) = obj.get("mode").and_then(|v| v.as_str()) {
163 if let Some(mode) = ResponseSelectionMode::from_str(mode_str) {
164 tracing::debug!(
165 "Parsed response selection mode for operation {}: {:?}",
166 op_id,
167 mode
168 );
169 return mode;
170 }
171 }
172 }
173 tracing::warn!(
174 "Failed to parse x-mockforge-response-selection extension for operation {}",
175 op_id
176 );
177 }
178 ResponseSelectionMode::First
180 }
181
182 pub fn from_operation(
184 method: &str,
185 path: String,
186 operation: &Operation,
187 spec: Arc<OpenApiSpec>,
188 ) -> Self {
189 Self::new(method.to_string(), path, operation.clone(), spec)
190 }
191
192 pub fn axum_path(&self) -> String {
194 self.path.clone()
196 }
197
198 pub fn with_metadata(mut self, key: String, value: String) -> Self {
200 self.metadata.insert(key, value);
201 self
202 }
203
204 pub async fn mock_response_with_status_async(
213 &self,
214 context: &crate::ai_response::RequestContext,
215 ai_generator: Option<&dyn crate::openapi::response::AiGenerator>,
216 ) -> (u16, serde_json::Value) {
217 use crate::openapi::response::ResponseGenerator;
218
219 let status_code = self.find_first_available_status_code();
221
222 if let Some(ai_config) = &self.ai_config {
224 if ai_config.is_active() {
225 tracing::info!(
226 "Using AI-assisted response generation for {} {}",
227 self.method,
228 self.path
229 );
230
231 match ResponseGenerator::generate_ai_response(ai_config, context, ai_generator)
232 .await
233 {
234 Ok(response_body) => {
235 tracing::debug!(
236 "AI response generated successfully for {} {}: {:?}",
237 self.method,
238 self.path,
239 response_body
240 );
241 return (status_code, response_body);
242 }
243 Err(e) => {
244 tracing::warn!(
245 "AI response generation failed for {} {}: {}, falling back to standard generation",
246 self.method,
247 self.path,
248 e
249 );
250 }
252 }
253 }
254 }
255
256 let expand_tokens = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
258 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
259 .unwrap_or(false);
260
261 let mode = Some(self.response_selection_mode);
263 let selector = Some(self.response_selector.as_ref());
264
265 match ResponseGenerator::generate_response_with_expansion_and_mode(
266 &self.spec,
267 &self.operation,
268 status_code,
269 Some("application/json"),
270 expand_tokens,
271 mode,
272 selector,
273 ) {
274 Ok(response_body) => {
275 tracing::debug!(
276 "ResponseGenerator succeeded for {} {} with status {}: {:?}",
277 self.method,
278 self.path,
279 status_code,
280 response_body
281 );
282 (status_code, response_body)
283 }
284 Err(e) => {
285 tracing::debug!(
286 "ResponseGenerator failed for {} {}: {}, using fallback",
287 self.method,
288 self.path,
289 e
290 );
291 let response_body = serde_json::json!({
293 "message": format!("Mock response for {} {}", self.method, self.path),
294 "operation_id": self.operation.operation_id,
295 "status": status_code
296 });
297 (status_code, response_body)
298 }
299 }
300 }
301
302 pub fn mock_response_with_status(&self) -> (u16, serde_json::Value) {
307 self.mock_response_with_status_and_scenario(None)
308 }
309
310 pub fn mock_response_with_status_and_scenario(
321 &self,
322 scenario: Option<&str>,
323 ) -> (u16, serde_json::Value) {
324 use crate::openapi::response::ResponseGenerator;
325
326 let status_code = self.find_first_available_status_code();
328
329 let expand_tokens = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
331 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
332 .unwrap_or(false);
333
334 let mode = Some(self.response_selection_mode);
336 let selector = Some(self.response_selector.as_ref());
337
338 match ResponseGenerator::generate_response_with_scenario_and_mode(
339 &self.spec,
340 &self.operation,
341 status_code,
342 Some("application/json"),
343 expand_tokens,
344 scenario,
345 mode,
346 selector,
347 ) {
348 Ok(response_body) => {
349 tracing::debug!(
350 "ResponseGenerator succeeded for {} {} with status {} and scenario {:?}: {:?}",
351 self.method,
352 self.path,
353 status_code,
354 scenario,
355 response_body
356 );
357 (status_code, response_body)
358 }
359 Err(e) => {
360 tracing::debug!(
361 "ResponseGenerator failed for {} {}: {}, using fallback",
362 self.method,
363 self.path,
364 e
365 );
366 let response_body = serde_json::json!({
368 "message": format!("Mock response for {} {}", self.method, self.path),
369 "operation_id": self.operation.operation_id,
370 "status": status_code
371 });
372 (status_code, response_body)
373 }
374 }
375 }
376
377 fn find_first_available_status_code(&self) -> u16 {
379 for (status, _) in &self.operation.responses.responses {
381 match status {
382 openapiv3::StatusCode::Code(code) => {
383 return *code;
384 }
385 openapiv3::StatusCode::Range(range) => {
386 match range {
388 2 => return 200, 3 => return 300, 4 => return 400, 5 => return 500, _ => continue, }
394 }
395 }
396 }
397
398 if self.operation.responses.default.is_some() {
400 return 200; }
402
403 200
405 }
406}
407
408#[derive(Debug, Clone)]
410pub struct OpenApiOperation {
411 pub method: String,
413 pub path: String,
415 pub operation: Operation,
417}
418
419impl OpenApiOperation {
420 pub fn new(method: String, path: String, operation: Operation) -> Self {
422 Self {
423 method,
424 path,
425 operation,
426 }
427 }
428}
429
430pub struct RouteGenerator;
432
433impl RouteGenerator {
434 pub fn generate_routes_from_path(
436 path: &str,
437 path_item: &ReferenceOr<PathItem>,
438 spec: &Arc<OpenApiSpec>,
439 ) -> Result<Vec<OpenApiRoute>> {
440 let mut routes = Vec::new();
441
442 if let Some(item) = path_item.as_item() {
443 if let Some(op) = &item.get {
445 routes.push(OpenApiRoute::new(
446 "GET".to_string(),
447 path.to_string(),
448 op.clone(),
449 spec.clone(),
450 ));
451 }
452 if let Some(op) = &item.post {
453 routes.push(OpenApiRoute::new(
454 "POST".to_string(),
455 path.to_string(),
456 op.clone(),
457 spec.clone(),
458 ));
459 }
460 if let Some(op) = &item.put {
461 routes.push(OpenApiRoute::new(
462 "PUT".to_string(),
463 path.to_string(),
464 op.clone(),
465 spec.clone(),
466 ));
467 }
468 if let Some(op) = &item.delete {
469 routes.push(OpenApiRoute::new(
470 "DELETE".to_string(),
471 path.to_string(),
472 op.clone(),
473 spec.clone(),
474 ));
475 }
476 if let Some(op) = &item.patch {
477 routes.push(OpenApiRoute::new(
478 "PATCH".to_string(),
479 path.to_string(),
480 op.clone(),
481 spec.clone(),
482 ));
483 }
484 if let Some(op) = &item.head {
485 routes.push(OpenApiRoute::new(
486 "HEAD".to_string(),
487 path.to_string(),
488 op.clone(),
489 spec.clone(),
490 ));
491 }
492 if let Some(op) = &item.options {
493 routes.push(OpenApiRoute::new(
494 "OPTIONS".to_string(),
495 path.to_string(),
496 op.clone(),
497 spec.clone(),
498 ));
499 }
500 if let Some(op) = &item.trace {
501 routes.push(OpenApiRoute::new(
502 "TRACE".to_string(),
503 path.to_string(),
504 op.clone(),
505 spec.clone(),
506 ));
507 }
508 }
509
510 Ok(routes)
511 }
512}