1use crate::{ai_response::AiResponseConfig, openapi::spec::OpenApiSpec, Result};
7use openapiv3::{Operation, PathItem, ReferenceOr};
8use std::collections::BTreeMap;
9use std::sync::Arc;
10
11fn extract_path_parameters(path_template: &str) -> Vec<String> {
13 let mut params = Vec::new();
14 let mut in_param = false;
15 let mut current_param = String::new();
16
17 for ch in path_template.chars() {
18 match ch {
19 '{' => {
20 in_param = true;
21 current_param.clear();
22 }
23 '}' => {
24 if in_param {
25 params.push(current_param.clone());
26 in_param = false;
27 }
28 }
29 ch if in_param => {
30 current_param.push(ch);
31 }
32 _ => {}
33 }
34 }
35
36 params
37}
38
39#[derive(Debug, Clone)]
41pub struct OpenApiRoute {
42 pub method: String,
44 pub path: String,
46 pub operation: Operation,
48 pub metadata: BTreeMap<String, String>,
50 pub parameters: Vec<String>,
52 pub spec: Arc<OpenApiSpec>,
54 pub ai_config: Option<AiResponseConfig>,
56}
57
58impl OpenApiRoute {
59 pub fn new(method: String, path: String, operation: Operation, spec: Arc<OpenApiSpec>) -> Self {
61 let parameters = extract_path_parameters(&path);
62
63 let ai_config = Self::parse_ai_config(&operation);
65
66 Self {
67 method,
68 path,
69 operation,
70 metadata: BTreeMap::new(),
71 parameters,
72 spec,
73 ai_config,
74 }
75 }
76
77 fn parse_ai_config(operation: &Operation) -> Option<AiResponseConfig> {
79 if let Some(ai_config_value) = operation.extensions.get("x-mockforge-ai") {
81 match serde_json::from_value::<AiResponseConfig>(ai_config_value.clone()) {
83 Ok(config) => {
84 if config.is_active() {
85 tracing::debug!(
86 "Parsed AI config for operation {}: mode={:?}, prompt={:?}",
87 operation.operation_id.as_deref().unwrap_or("unknown"),
88 config.mode,
89 config.prompt
90 );
91 return Some(config);
92 }
93 }
94 Err(e) => {
95 tracing::warn!(
96 "Failed to parse x-mockforge-ai extension for operation {}: {}",
97 operation.operation_id.as_deref().unwrap_or("unknown"),
98 e
99 );
100 }
101 }
102 }
103 None
104 }
105
106 pub fn from_operation(
108 method: &str,
109 path: String,
110 operation: &Operation,
111 spec: Arc<OpenApiSpec>,
112 ) -> Self {
113 Self::new(method.to_string(), path, operation.clone(), spec)
114 }
115
116 pub fn axum_path(&self) -> String {
118 self.path.clone()
120 }
121
122 pub fn with_metadata(mut self, key: String, value: String) -> Self {
124 self.metadata.insert(key, value);
125 self
126 }
127
128 pub async fn mock_response_with_status_async(
137 &self,
138 context: &crate::ai_response::RequestContext,
139 ai_generator: Option<&dyn crate::openapi::response::AiGenerator>,
140 ) -> (u16, serde_json::Value) {
141 use crate::openapi::response::ResponseGenerator;
142
143 let status_code = self.find_first_available_status_code();
145
146 if let Some(ai_config) = &self.ai_config {
148 if ai_config.is_active() {
149 tracing::info!(
150 "Using AI-assisted response generation for {} {}",
151 self.method,
152 self.path
153 );
154
155 match ResponseGenerator::generate_ai_response(ai_config, context, ai_generator)
156 .await
157 {
158 Ok(response_body) => {
159 tracing::debug!(
160 "AI response generated successfully for {} {}: {:?}",
161 self.method,
162 self.path,
163 response_body
164 );
165 return (status_code, response_body);
166 }
167 Err(e) => {
168 tracing::warn!(
169 "AI response generation failed for {} {}: {}, falling back to standard generation",
170 self.method,
171 self.path,
172 e
173 );
174 }
176 }
177 }
178 }
179
180 let expand_tokens = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
182 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
183 .unwrap_or(false);
184
185 match ResponseGenerator::generate_response_with_expansion(
186 &self.spec,
187 &self.operation,
188 status_code,
189 Some("application/json"),
190 expand_tokens,
191 ) {
192 Ok(response_body) => {
193 tracing::debug!(
194 "ResponseGenerator succeeded for {} {} with status {}: {:?}",
195 self.method,
196 self.path,
197 status_code,
198 response_body
199 );
200 (status_code, response_body)
201 }
202 Err(e) => {
203 tracing::debug!(
204 "ResponseGenerator failed for {} {}: {}, using fallback",
205 self.method,
206 self.path,
207 e
208 );
209 let response_body = serde_json::json!({
211 "message": format!("Mock response for {} {}", self.method, self.path),
212 "operation_id": self.operation.operation_id,
213 "status": status_code
214 });
215 (status_code, response_body)
216 }
217 }
218 }
219
220 pub fn mock_response_with_status(&self) -> (u16, serde_json::Value) {
225 self.mock_response_with_status_and_scenario(None)
226 }
227
228 pub fn mock_response_with_status_and_scenario(
239 &self,
240 scenario: Option<&str>,
241 ) -> (u16, serde_json::Value) {
242 use crate::openapi::response::ResponseGenerator;
243
244 let status_code = self.find_first_available_status_code();
246
247 let expand_tokens = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
249 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
250 .unwrap_or(false);
251
252 match ResponseGenerator::generate_response_with_scenario(
253 &self.spec,
254 &self.operation,
255 status_code,
256 Some("application/json"),
257 expand_tokens,
258 scenario,
259 ) {
260 Ok(response_body) => {
261 tracing::debug!(
262 "ResponseGenerator succeeded for {} {} with status {} and scenario {:?}: {:?}",
263 self.method,
264 self.path,
265 status_code,
266 scenario,
267 response_body
268 );
269 (status_code, response_body)
270 }
271 Err(e) => {
272 tracing::debug!(
273 "ResponseGenerator failed for {} {}: {}, using fallback",
274 self.method,
275 self.path,
276 e
277 );
278 let response_body = serde_json::json!({
280 "message": format!("Mock response for {} {}", self.method, self.path),
281 "operation_id": self.operation.operation_id,
282 "status": status_code
283 });
284 (status_code, response_body)
285 }
286 }
287 }
288
289 fn find_first_available_status_code(&self) -> u16 {
291 for (status, _) in &self.operation.responses.responses {
293 match status {
294 openapiv3::StatusCode::Code(code) => {
295 return *code;
296 }
297 openapiv3::StatusCode::Range(range) => {
298 match range {
300 2 => return 200, 3 => return 300, 4 => return 400, 5 => return 500, _ => continue, }
306 }
307 }
308 }
309
310 if self.operation.responses.default.is_some() {
312 return 200; }
314
315 200
317 }
318}
319
320#[derive(Debug, Clone)]
322pub struct OpenApiOperation {
323 pub method: String,
325 pub path: String,
327 pub operation: Operation,
329}
330
331impl OpenApiOperation {
332 pub fn new(method: String, path: String, operation: Operation) -> Self {
334 Self {
335 method,
336 path,
337 operation,
338 }
339 }
340}
341
342pub struct RouteGenerator;
344
345impl RouteGenerator {
346 pub fn generate_routes_from_path(
348 path: &str,
349 path_item: &ReferenceOr<PathItem>,
350 spec: &Arc<OpenApiSpec>,
351 ) -> Result<Vec<OpenApiRoute>> {
352 let mut routes = Vec::new();
353
354 if let Some(item) = path_item.as_item() {
355 if let Some(op) = &item.get {
357 routes.push(OpenApiRoute::new(
358 "GET".to_string(),
359 path.to_string(),
360 op.clone(),
361 spec.clone(),
362 ));
363 }
364 if let Some(op) = &item.post {
365 routes.push(OpenApiRoute::new(
366 "POST".to_string(),
367 path.to_string(),
368 op.clone(),
369 spec.clone(),
370 ));
371 }
372 if let Some(op) = &item.put {
373 routes.push(OpenApiRoute::new(
374 "PUT".to_string(),
375 path.to_string(),
376 op.clone(),
377 spec.clone(),
378 ));
379 }
380 if let Some(op) = &item.delete {
381 routes.push(OpenApiRoute::new(
382 "DELETE".to_string(),
383 path.to_string(),
384 op.clone(),
385 spec.clone(),
386 ));
387 }
388 if let Some(op) = &item.patch {
389 routes.push(OpenApiRoute::new(
390 "PATCH".to_string(),
391 path.to_string(),
392 op.clone(),
393 spec.clone(),
394 ));
395 }
396 if let Some(op) = &item.head {
397 routes.push(OpenApiRoute::new(
398 "HEAD".to_string(),
399 path.to_string(),
400 op.clone(),
401 spec.clone(),
402 ));
403 }
404 if let Some(op) = &item.options {
405 routes.push(OpenApiRoute::new(
406 "OPTIONS".to_string(),
407 path.to_string(),
408 op.clone(),
409 spec.clone(),
410 ));
411 }
412 if let Some(op) = &item.trace {
413 routes.push(OpenApiRoute::new(
414 "TRACE".to_string(),
415 path.to_string(),
416 op.clone(),
417 spec.clone(),
418 ));
419 }
420 }
421
422 Ok(routes)
423 }
424}