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 use crate::openapi::response::ResponseGenerator;
226
227 let status_code = self.find_first_available_status_code();
229
230 let expand_tokens = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
233 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
234 .unwrap_or(false);
235
236 match ResponseGenerator::generate_response_with_expansion(
237 &self.spec,
238 &self.operation,
239 status_code,
240 Some("application/json"),
241 expand_tokens,
242 ) {
243 Ok(response_body) => {
244 tracing::debug!(
245 "ResponseGenerator succeeded for {} {} with status {}: {:?}",
246 self.method,
247 self.path,
248 status_code,
249 response_body
250 );
251 (status_code, response_body)
252 }
253 Err(e) => {
254 tracing::debug!(
255 "ResponseGenerator failed for {} {}: {}, using fallback",
256 self.method,
257 self.path,
258 e
259 );
260 let response_body = serde_json::json!({
262 "message": format!("Mock response for {} {}", self.method, self.path),
263 "operation_id": self.operation.operation_id,
264 "status": status_code
265 });
266 (status_code, response_body)
267 }
268 }
269 }
270
271 fn find_first_available_status_code(&self) -> u16 {
273 for (status, _) in &self.operation.responses.responses {
275 match status {
276 openapiv3::StatusCode::Code(code) => {
277 return *code;
278 }
279 openapiv3::StatusCode::Range(range) => {
280 match range {
282 2 => return 200, 3 => return 300, 4 => return 400, 5 => return 500, _ => continue, }
288 }
289 }
290 }
291
292 if self.operation.responses.default.is_some() {
294 return 200; }
296
297 200
299 }
300}
301
302#[derive(Debug, Clone)]
304pub struct OpenApiOperation {
305 pub method: String,
307 pub path: String,
309 pub operation: Operation,
311}
312
313impl OpenApiOperation {
314 pub fn new(method: String, path: String, operation: Operation) -> Self {
316 Self {
317 method,
318 path,
319 operation,
320 }
321 }
322}
323
324pub struct RouteGenerator;
326
327impl RouteGenerator {
328 pub fn generate_routes_from_path(
330 path: &str,
331 path_item: &ReferenceOr<PathItem>,
332 spec: &Arc<OpenApiSpec>,
333 ) -> Result<Vec<OpenApiRoute>> {
334 let mut routes = Vec::new();
335
336 if let Some(item) = path_item.as_item() {
337 if let Some(op) = &item.get {
339 routes.push(OpenApiRoute::new(
340 "GET".to_string(),
341 path.to_string(),
342 op.clone(),
343 spec.clone(),
344 ));
345 }
346 if let Some(op) = &item.post {
347 routes.push(OpenApiRoute::new(
348 "POST".to_string(),
349 path.to_string(),
350 op.clone(),
351 spec.clone(),
352 ));
353 }
354 if let Some(op) = &item.put {
355 routes.push(OpenApiRoute::new(
356 "PUT".to_string(),
357 path.to_string(),
358 op.clone(),
359 spec.clone(),
360 ));
361 }
362 if let Some(op) = &item.delete {
363 routes.push(OpenApiRoute::new(
364 "DELETE".to_string(),
365 path.to_string(),
366 op.clone(),
367 spec.clone(),
368 ));
369 }
370 if let Some(op) = &item.patch {
371 routes.push(OpenApiRoute::new(
372 "PATCH".to_string(),
373 path.to_string(),
374 op.clone(),
375 spec.clone(),
376 ));
377 }
378 if let Some(op) = &item.head {
379 routes.push(OpenApiRoute::new(
380 "HEAD".to_string(),
381 path.to_string(),
382 op.clone(),
383 spec.clone(),
384 ));
385 }
386 if let Some(op) = &item.options {
387 routes.push(OpenApiRoute::new(
388 "OPTIONS".to_string(),
389 path.to_string(),
390 op.clone(),
391 spec.clone(),
392 ));
393 }
394 if let Some(op) = &item.trace {
395 routes.push(OpenApiRoute::new(
396 "TRACE".to_string(),
397 path.to_string(),
398 op.clone(),
399 spec.clone(),
400 ));
401 }
402 }
403
404 Ok(routes)
405 }
406}