mockforge_core/
openapi_routes.rs

1//! OpenAPI-based route generation for MockForge
2//!
3//! This module has been refactored into sub-modules for better organization:
4//! - registry: OpenAPI route registry and management
5//! - validation: Request/response validation logic
6//! - generation: Route generation from OpenAPI specs
7//! - builder: Axum router building from OpenAPI specs
8
9// Re-export sub-modules for backward compatibility
10pub mod builder;
11pub mod generation;
12pub mod registry;
13pub mod validation;
14
15// Re-export commonly used types
16pub use builder::*;
17pub use generation::*;
18pub use validation::*;
19
20// Legacy types and functions for backward compatibility
21use crate::ai_response::RequestContext;
22use crate::openapi::response::AiGenerator;
23use crate::openapi::{OpenApiOperation, OpenApiRoute, OpenApiSchema, OpenApiSpec};
24use crate::templating::expand_tokens as core_expand_tokens;
25use crate::{latency::LatencyInjector, overrides::Overrides, Error, Result};
26use axum::extract::{Path as AxumPath, RawQuery};
27use axum::http::HeaderMap;
28use axum::response::IntoResponse;
29use axum::routing::*;
30use axum::{Json, Router};
31use chrono::Utc;
32use once_cell::sync::Lazy;
33use openapiv3::ParameterSchemaOrContent;
34use serde_json::{json, Map, Value};
35use std::collections::VecDeque;
36use std::sync::{Arc, Mutex};
37use tracing;
38
39/// OpenAPI route registry that manages generated routes
40#[derive(Debug, Clone)]
41pub struct OpenApiRouteRegistry {
42    /// The OpenAPI specification
43    spec: Arc<OpenApiSpec>,
44    /// Generated routes
45    routes: Vec<OpenApiRoute>,
46    /// Validation options
47    options: ValidationOptions,
48}
49
50/// Validation mode for request/response validation
51#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)]
52pub enum ValidationMode {
53    /// Validation is disabled (no checks performed)
54    Disabled,
55    /// Validation warnings are logged but do not fail requests
56    #[default]
57    Warn,
58    /// Validation failures return error responses
59    Enforce,
60}
61
62/// Options for configuring validation behavior
63#[derive(Debug, Clone)]
64pub struct ValidationOptions {
65    /// Validation mode for incoming requests
66    pub request_mode: ValidationMode,
67    /// Whether to aggregate multiple validation errors into a single response
68    pub aggregate_errors: bool,
69    /// Whether to validate outgoing responses against schemas
70    pub validate_responses: bool,
71    /// Per-operation validation mode overrides (operation ID -> mode)
72    pub overrides: std::collections::HashMap<String, ValidationMode>,
73    /// Skip validation for request paths starting with any of these prefixes
74    pub admin_skip_prefixes: Vec<String>,
75    /// Expand templating tokens in responses/examples after generation
76    pub response_template_expand: bool,
77    /// HTTP status code to return for validation failures (e.g., 400 or 422)
78    pub validation_status: Option<u16>,
79}
80
81impl Default for ValidationOptions {
82    fn default() -> Self {
83        Self {
84            request_mode: ValidationMode::Enforce,
85            aggregate_errors: true,
86            validate_responses: false,
87            overrides: std::collections::HashMap::new(),
88            admin_skip_prefixes: Vec::new(),
89            response_template_expand: false,
90            validation_status: None,
91        }
92    }
93}
94
95impl OpenApiRouteRegistry {
96    /// Create a new registry from an OpenAPI spec
97    pub fn new(spec: OpenApiSpec) -> Self {
98        Self::new_with_env(spec)
99    }
100
101    /// Create a new registry from an OpenAPI spec with environment-based validation options
102    ///
103    /// Options are read from environment variables:
104    /// - `MOCKFORGE_REQUEST_VALIDATION`: "off"/"warn"/"enforce" (default: "enforce")
105    /// - `MOCKFORGE_AGGREGATE_ERRORS`: "1"/"true" to aggregate errors (default: true)
106    /// - `MOCKFORGE_RESPONSE_VALIDATION`: "1"/"true" to validate responses (default: false)
107    /// - `MOCKFORGE_RESPONSE_TEMPLATE_EXPAND`: "1"/"true" to expand templates (default: false)
108    /// - `MOCKFORGE_VALIDATION_STATUS`: HTTP status code for validation failures (optional)
109    pub fn new_with_env(spec: OpenApiSpec) -> Self {
110        tracing::debug!("Creating OpenAPI route registry");
111        let spec = Arc::new(spec);
112        let routes = Self::generate_routes(&spec);
113        let options = ValidationOptions {
114            request_mode: match std::env::var("MOCKFORGE_REQUEST_VALIDATION")
115                .unwrap_or_else(|_| "enforce".into())
116                .to_ascii_lowercase()
117                .as_str()
118            {
119                "off" | "disable" | "disabled" => ValidationMode::Disabled,
120                "warn" | "warning" => ValidationMode::Warn,
121                _ => ValidationMode::Enforce,
122            },
123            aggregate_errors: std::env::var("MOCKFORGE_AGGREGATE_ERRORS")
124                .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
125                .unwrap_or(true),
126            validate_responses: std::env::var("MOCKFORGE_RESPONSE_VALIDATION")
127                .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
128                .unwrap_or(false),
129            overrides: std::collections::HashMap::new(),
130            admin_skip_prefixes: Vec::new(),
131            response_template_expand: std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
132                .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
133                .unwrap_or(false),
134            validation_status: std::env::var("MOCKFORGE_VALIDATION_STATUS")
135                .ok()
136                .and_then(|s| s.parse::<u16>().ok()),
137        };
138        Self {
139            spec,
140            routes,
141            options,
142        }
143    }
144
145    /// Construct with explicit options
146    pub fn new_with_options(spec: OpenApiSpec, options: ValidationOptions) -> Self {
147        tracing::debug!("Creating OpenAPI route registry with custom options");
148        let spec = Arc::new(spec);
149        let routes = Self::generate_routes(&spec);
150        Self {
151            spec,
152            routes,
153            options,
154        }
155    }
156
157    /// Clone this registry for validation purposes (creates an independent copy)
158    ///
159    /// This is useful when you need a separate registry instance for validation
160    /// that won't interfere with the main registry's state.
161    pub fn clone_for_validation(&self) -> Self {
162        OpenApiRouteRegistry {
163            spec: self.spec.clone(),
164            routes: self.routes.clone(),
165            options: self.options.clone(),
166        }
167    }
168
169    /// Generate routes from the OpenAPI specification
170    fn generate_routes(spec: &Arc<OpenApiSpec>) -> Vec<OpenApiRoute> {
171        let mut routes = Vec::new();
172
173        let all_paths_ops = spec.all_paths_and_operations();
174        tracing::debug!("Generating routes from OpenAPI spec with {} paths", all_paths_ops.len());
175
176        for (path, operations) in all_paths_ops {
177            tracing::debug!("Processing path: {}", path);
178            for (method, operation) in operations {
179                routes.push(OpenApiRoute::from_operation(
180                    &method,
181                    path.clone(),
182                    &operation,
183                    spec.clone(),
184                ));
185            }
186        }
187
188        tracing::debug!("Generated {} total routes from OpenAPI spec", routes.len());
189        routes
190    }
191
192    /// Get all routes
193    pub fn routes(&self) -> &[OpenApiRoute] {
194        &self.routes
195    }
196
197    /// Get the OpenAPI specification
198    pub fn spec(&self) -> &OpenApiSpec {
199        &self.spec
200    }
201
202    /// Build an Axum router from the OpenAPI spec (simplified)
203    pub fn build_router(self) -> Router {
204        let mut router = Router::new();
205        tracing::debug!("Building router from {} routes", self.routes.len());
206
207        // Create individual routes for each operation
208        for route in &self.routes {
209            tracing::debug!("Adding route: {} {}", route.method, route.path);
210            let axum_path = route.axum_path();
211            let operation = route.operation.clone();
212            let method = route.method.clone();
213            let path_template = route.path.clone();
214            let validator = self.clone_for_validation();
215            let route_clone = route.clone();
216
217            // Handler: validate path/query/header/cookie/body, then return mock
218            let handler = move |AxumPath(path_params): AxumPath<
219                std::collections::HashMap<String, String>,
220            >,
221                                RawQuery(raw_query): RawQuery,
222                                headers: HeaderMap,
223                                body: axum::body::Bytes| async move {
224                tracing::debug!("Handling OpenAPI request: {} {}", method, path_template);
225
226                // Determine scenario from header or environment variable
227                // Header takes precedence over environment variable
228                let scenario = headers
229                    .get("X-Mockforge-Scenario")
230                    .and_then(|v| v.to_str().ok())
231                    .map(|s| s.to_string())
232                    .or_else(|| std::env::var("MOCKFORGE_HTTP_SCENARIO").ok());
233
234                // Generate mock response for this request with scenario support
235                let (selected_status, mock_response) =
236                    route_clone.mock_response_with_status_and_scenario(scenario.as_deref());
237                // Admin routes are mounted separately; no validation skip needed here.
238                // Build params maps
239                let mut path_map = serde_json::Map::new();
240                for (k, v) in path_params {
241                    path_map.insert(k, Value::String(v));
242                }
243
244                // Query
245                let mut query_map = Map::new();
246                if let Some(q) = raw_query {
247                    for (k, v) in url::form_urlencoded::parse(q.as_bytes()) {
248                        query_map.insert(k.to_string(), Value::String(v.to_string()));
249                    }
250                }
251
252                // Headers: only capture those declared on this operation
253                let mut header_map = Map::new();
254                for p_ref in &operation.parameters {
255                    if let Some(openapiv3::Parameter::Header { parameter_data, .. }) =
256                        p_ref.as_item()
257                    {
258                        let name_lc = parameter_data.name.to_ascii_lowercase();
259                        if let Ok(hn) = axum::http::HeaderName::from_bytes(name_lc.as_bytes()) {
260                            if let Some(val) = headers.get(hn) {
261                                if let Ok(s) = val.to_str() {
262                                    header_map.insert(
263                                        parameter_data.name.clone(),
264                                        Value::String(s.to_string()),
265                                    );
266                                }
267                            }
268                        }
269                    }
270                }
271
272                // Cookies: parse Cookie header
273                let mut cookie_map = Map::new();
274                if let Some(val) = headers.get(axum::http::header::COOKIE) {
275                    if let Ok(s) = val.to_str() {
276                        for part in s.split(';') {
277                            let part = part.trim();
278                            if let Some((k, v)) = part.split_once('=') {
279                                cookie_map.insert(k.to_string(), Value::String(v.to_string()));
280                            }
281                        }
282                    }
283                }
284
285                // Body: try JSON when present
286                let body_json: Option<Value> = if !body.is_empty() {
287                    serde_json::from_slice(&body).ok()
288                } else {
289                    None
290                };
291
292                if let Err(e) = validator.validate_request_with_all(
293                    &path_template,
294                    &method,
295                    &path_map,
296                    &query_map,
297                    &header_map,
298                    &cookie_map,
299                    body_json.as_ref(),
300                ) {
301                    // Choose status: prefer options.validation_status, fallback to env, else 400
302                    let status_code = validator.options.validation_status.unwrap_or_else(|| {
303                        std::env::var("MOCKFORGE_VALIDATION_STATUS")
304                            .ok()
305                            .and_then(|s| s.parse::<u16>().ok())
306                            .unwrap_or(400)
307                    });
308
309                    let payload = if status_code == 422 {
310                        // For 422 responses, use enhanced schema validation with detailed errors
311                        // Note: We need to extract parameters from the request context
312                        // For now, using empty maps as placeholders
313                        let empty_params = serde_json::Map::new();
314                        generate_enhanced_422_response(
315                            &validator,
316                            &path_template,
317                            &method,
318                            body_json.as_ref(),
319                            &empty_params, // path_params
320                            &empty_params, // query_params
321                            &empty_params, // header_params
322                            &empty_params, // cookie_params
323                        )
324                    } else {
325                        // For other status codes, use generic error format
326                        let msg = format!("{}", e);
327                        let detail_val = serde_json::from_str::<serde_json::Value>(&msg)
328                            .unwrap_or(serde_json::json!(msg));
329                        json!({
330                            "error": "request validation failed",
331                            "detail": detail_val,
332                            "method": method,
333                            "path": path_template,
334                            "timestamp": Utc::now().to_rfc3339(),
335                        })
336                    };
337
338                    record_validation_error(&payload);
339                    let status = axum::http::StatusCode::from_u16(status_code)
340                        .unwrap_or(axum::http::StatusCode::BAD_REQUEST);
341
342                    // Serialize payload with fallback for serialization errors
343                    let body_bytes = serde_json::to_vec(&payload)
344                        .unwrap_or_else(|_| br#"{"error":"Serialization failed"}"#.to_vec());
345
346                    return axum::http::Response::builder()
347                        .status(status)
348                        .header(axum::http::header::CONTENT_TYPE, "application/json")
349                        .body(axum::body::Body::from(body_bytes))
350                        .expect("Response builder should create valid response with valid headers and body");
351                }
352
353                // Expand tokens in the response if enabled (options or env)
354                let mut final_response = mock_response.clone();
355                let env_expand = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
356                    .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
357                    .unwrap_or(false);
358                let expand = validator.options.response_template_expand || env_expand;
359                if expand {
360                    final_response = core_expand_tokens(&final_response);
361                }
362
363                // Optional response validation
364                if validator.options.validate_responses {
365                    // Find the first 2xx response in the operation
366                    if let Some((status_code, _response)) = operation
367                        .responses
368                        .responses
369                        .iter()
370                        .filter_map(|(status, resp)| match status {
371                            openapiv3::StatusCode::Code(code) if *code >= 200 && *code < 300 => {
372                                resp.as_item().map(|r| ((*code), r))
373                            }
374                            openapiv3::StatusCode::Range(range)
375                                if *range >= 200 && *range < 300 =>
376                            {
377                                resp.as_item().map(|r| (200, r))
378                            }
379                            _ => None,
380                        })
381                        .next()
382                    {
383                        // Basic response validation - check if response is valid JSON
384                        if serde_json::from_value::<serde_json::Value>(final_response.clone())
385                            .is_err()
386                        {
387                            tracing::warn!(
388                                "Response validation failed: invalid JSON for status {}",
389                                status_code
390                            );
391                        }
392                    }
393                }
394
395                // Return the mock response with the correct status code
396                let mut response = Json(final_response).into_response();
397                *response.status_mut() = axum::http::StatusCode::from_u16(selected_status)
398                    .unwrap_or(axum::http::StatusCode::OK);
399                response
400            };
401
402            // Register the handler based on HTTP method
403            router = match route.method.as_str() {
404                "GET" => router.route(&axum_path, get(handler)),
405                "POST" => router.route(&axum_path, post(handler)),
406                "PUT" => router.route(&axum_path, put(handler)),
407                "DELETE" => router.route(&axum_path, delete(handler)),
408                "PATCH" => router.route(&axum_path, patch(handler)),
409                "HEAD" => router.route(&axum_path, head(handler)),
410                "OPTIONS" => router.route(&axum_path, options(handler)),
411                _ => router, // Skip unknown methods
412            };
413        }
414
415        // Add OpenAPI documentation endpoint
416        let spec_json = serde_json::to_value(&self.spec.spec).unwrap_or(Value::Null);
417        router = router.route("/openapi.json", get(move || async move { Json(spec_json) }));
418
419        router
420    }
421
422    /// Build an Axum router from the OpenAPI spec with latency injection support
423    pub fn build_router_with_latency(self, latency_injector: LatencyInjector) -> Router {
424        self.build_router_with_injectors(latency_injector, None)
425    }
426
427    /// Build an Axum router from the OpenAPI spec with both latency and failure injection support
428    pub fn build_router_with_injectors(
429        self,
430        latency_injector: LatencyInjector,
431        failure_injector: Option<crate::FailureInjector>,
432    ) -> Router {
433        self.build_router_with_injectors_and_overrides(
434            latency_injector,
435            failure_injector,
436            None,
437            false,
438        )
439    }
440
441    /// Build an Axum router from the OpenAPI spec with latency, failure injection, and overrides support
442    pub fn build_router_with_injectors_and_overrides(
443        self,
444        latency_injector: LatencyInjector,
445        failure_injector: Option<crate::FailureInjector>,
446        overrides: Option<Overrides>,
447        overrides_enabled: bool,
448    ) -> Router {
449        let mut router = Router::new();
450
451        // Create individual routes for each operation
452        for route in &self.routes {
453            let axum_path = route.axum_path();
454            let operation = route.operation.clone();
455            let method = route.method.clone();
456            let method_str = method.clone();
457            let method_for_router = method_str.clone();
458            let path_template = route.path.clone();
459            let validator = self.clone_for_validation();
460            let route_clone = route.clone();
461            let injector = latency_injector.clone();
462            let failure_injector = failure_injector.clone();
463            let route_overrides = overrides.clone();
464
465            // Extract tags from operation for latency and failure injection
466            let mut operation_tags = operation.tags.clone();
467            if let Some(operation_id) = &operation.operation_id {
468                operation_tags.push(operation_id.clone());
469            }
470
471            // Handler: inject latency, validate path/query/header/cookie/body, then return mock
472            let handler = move |AxumPath(path_params): AxumPath<
473                std::collections::HashMap<String, String>,
474            >,
475                                RawQuery(raw_query): RawQuery,
476                                headers: HeaderMap,
477                                body: axum::body::Bytes| async move {
478                // Check for failure injection first
479                if let Some(ref failure_injector) = failure_injector {
480                    if let Some((status_code, error_message)) =
481                        failure_injector.process_request(&operation_tags)
482                    {
483                        return (
484                            axum::http::StatusCode::from_u16(status_code)
485                                .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR),
486                            axum::Json(serde_json::json!({
487                                "error": error_message,
488                                "injected_failure": true
489                            })),
490                        );
491                    }
492                }
493
494                // Inject latency before processing the request
495                if let Err(e) = injector.inject_latency(&operation_tags).await {
496                    tracing::warn!("Failed to inject latency: {}", e);
497                }
498
499                // Determine scenario from header or environment variable
500                // Header takes precedence over environment variable
501                let scenario = headers
502                    .get("X-Mockforge-Scenario")
503                    .and_then(|v| v.to_str().ok())
504                    .map(|s| s.to_string())
505                    .or_else(|| std::env::var("MOCKFORGE_HTTP_SCENARIO").ok());
506
507                // Admin routes are mounted separately; no validation skip needed here.
508                // Build params maps
509                let mut path_map = Map::new();
510                for (k, v) in path_params {
511                    path_map.insert(k, Value::String(v));
512                }
513
514                // Query
515                let mut query_map = Map::new();
516                if let Some(q) = raw_query {
517                    for (k, v) in url::form_urlencoded::parse(q.as_bytes()) {
518                        query_map.insert(k.to_string(), Value::String(v.to_string()));
519                    }
520                }
521
522                // Headers: only capture those declared on this operation
523                let mut header_map = Map::new();
524                for p_ref in &operation.parameters {
525                    if let Some(openapiv3::Parameter::Header { parameter_data, .. }) =
526                        p_ref.as_item()
527                    {
528                        let name_lc = parameter_data.name.to_ascii_lowercase();
529                        if let Ok(hn) = axum::http::HeaderName::from_bytes(name_lc.as_bytes()) {
530                            if let Some(val) = headers.get(hn) {
531                                if let Ok(s) = val.to_str() {
532                                    header_map.insert(
533                                        parameter_data.name.clone(),
534                                        Value::String(s.to_string()),
535                                    );
536                                }
537                            }
538                        }
539                    }
540                }
541
542                // Cookies: parse Cookie header
543                let mut cookie_map = Map::new();
544                if let Some(val) = headers.get(axum::http::header::COOKIE) {
545                    if let Ok(s) = val.to_str() {
546                        for part in s.split(';') {
547                            let part = part.trim();
548                            if let Some((k, v)) = part.split_once('=') {
549                                cookie_map.insert(k.to_string(), Value::String(v.to_string()));
550                            }
551                        }
552                    }
553                }
554
555                // Body: try JSON when present
556                let body_json: Option<Value> = if !body.is_empty() {
557                    serde_json::from_slice(&body).ok()
558                } else {
559                    None
560                };
561
562                if let Err(e) = validator.validate_request_with_all(
563                    &path_template,
564                    &method_str,
565                    &path_map,
566                    &query_map,
567                    &header_map,
568                    &cookie_map,
569                    body_json.as_ref(),
570                ) {
571                    let msg = format!("{}", e);
572                    let detail_val = serde_json::from_str::<serde_json::Value>(&msg)
573                        .unwrap_or(serde_json::json!(msg));
574                    let payload = serde_json::json!({
575                        "error": "request validation failed",
576                        "detail": detail_val,
577                        "method": method_str,
578                        "path": path_template,
579                        "timestamp": Utc::now().to_rfc3339(),
580                    });
581                    record_validation_error(&payload);
582                    // Choose status: prefer options.validation_status, fallback to env, else 400
583                    let status_code = validator.options.validation_status.unwrap_or_else(|| {
584                        std::env::var("MOCKFORGE_VALIDATION_STATUS")
585                            .ok()
586                            .and_then(|s| s.parse::<u16>().ok())
587                            .unwrap_or(400)
588                    });
589                    return (
590                        axum::http::StatusCode::from_u16(status_code)
591                            .unwrap_or(axum::http::StatusCode::BAD_REQUEST),
592                        Json(payload),
593                    );
594                }
595
596                // Generate mock response with scenario support
597                let (selected_status, mock_response) =
598                    route_clone.mock_response_with_status_and_scenario(scenario.as_deref());
599
600                // Expand templating tokens in response if enabled (options or env)
601                let mut response = mock_response.clone();
602                let env_expand = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
603                    .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
604                    .unwrap_or(false);
605                let expand = validator.options.response_template_expand || env_expand;
606                if expand {
607                    response = core_expand_tokens(&response);
608                }
609
610                // Apply overrides if provided and enabled
611                if let Some(ref overrides) = route_overrides {
612                    if overrides_enabled {
613                        // Extract tags from operation for override matching
614                        let operation_tags =
615                            operation.operation_id.clone().map(|id| vec![id]).unwrap_or_default();
616                        overrides.apply(
617                            &operation.operation_id.unwrap_or_default(),
618                            &operation_tags,
619                            &path_template,
620                            &mut response,
621                        );
622                    }
623                }
624
625                // Return the mock response
626                (
627                    axum::http::StatusCode::from_u16(selected_status)
628                        .unwrap_or(axum::http::StatusCode::OK),
629                    Json(response),
630                )
631            };
632
633            // Add route to router based on HTTP method
634            router = match method_for_router.as_str() {
635                "GET" => router.route(&axum_path, get(handler)),
636                "POST" => router.route(&axum_path, post(handler)),
637                "PUT" => router.route(&axum_path, put(handler)),
638                "PATCH" => router.route(&axum_path, patch(handler)),
639                "DELETE" => router.route(&axum_path, delete(handler)),
640                "HEAD" => router.route(&axum_path, head(handler)),
641                "OPTIONS" => router.route(&axum_path, options(handler)),
642                _ => router.route(&axum_path, get(handler)), // Default to GET for unknown methods
643            };
644        }
645
646        // Add OpenAPI documentation endpoint
647        let spec_json = serde_json::to_value(&self.spec.spec).unwrap_or(Value::Null);
648        router = router.route("/openapi.json", get(move || async move { Json(spec_json) }));
649
650        router
651    }
652
653    /// Get route by path and method
654    pub fn get_route(&self, path: &str, method: &str) -> Option<&OpenApiRoute> {
655        self.routes.iter().find(|route| route.path == path && route.method == method)
656    }
657
658    /// Get all routes for a specific path
659    pub fn get_routes_for_path(&self, path: &str) -> Vec<&OpenApiRoute> {
660        self.routes.iter().filter(|route| route.path == path).collect()
661    }
662
663    /// Validate request against OpenAPI spec (legacy body-only)
664    pub fn validate_request(&self, path: &str, method: &str, body: Option<&Value>) -> Result<()> {
665        self.validate_request_with(path, method, &Map::new(), &Map::new(), body)
666    }
667
668    /// Validate request against OpenAPI spec with path/query params
669    pub fn validate_request_with(
670        &self,
671        path: &str,
672        method: &str,
673        path_params: &Map<String, Value>,
674        query_params: &Map<String, Value>,
675        body: Option<&Value>,
676    ) -> Result<()> {
677        self.validate_request_with_all(
678            path,
679            method,
680            path_params,
681            query_params,
682            &Map::new(),
683            &Map::new(),
684            body,
685        )
686    }
687
688    /// Validate request against OpenAPI spec with path/query/header/cookie params
689    #[allow(clippy::too_many_arguments)]
690    pub fn validate_request_with_all(
691        &self,
692        path: &str,
693        method: &str,
694        path_params: &Map<String, Value>,
695        query_params: &Map<String, Value>,
696        header_params: &Map<String, Value>,
697        cookie_params: &Map<String, Value>,
698        body: Option<&Value>,
699    ) -> Result<()> {
700        // Skip validation for any configured admin prefixes
701        for pref in &self.options.admin_skip_prefixes {
702            if !pref.is_empty() && path.starts_with(pref) {
703                return Ok(());
704            }
705        }
706        // Runtime env overrides
707        let env_mode = std::env::var("MOCKFORGE_REQUEST_VALIDATION").ok().map(|v| {
708            match v.to_ascii_lowercase().as_str() {
709                "off" | "disable" | "disabled" => ValidationMode::Disabled,
710                "warn" | "warning" => ValidationMode::Warn,
711                _ => ValidationMode::Enforce,
712            }
713        });
714        let aggregate = std::env::var("MOCKFORGE_AGGREGATE_ERRORS")
715            .ok()
716            .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
717            .unwrap_or(self.options.aggregate_errors);
718        // Per-route runtime overrides via JSON env var
719        let env_overrides: Option<serde_json::Map<String, serde_json::Value>> =
720            std::env::var("MOCKFORGE_VALIDATION_OVERRIDES_JSON")
721                .ok()
722                .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
723                .and_then(|v| v.as_object().cloned());
724        // Response validation is handled in HTTP layer now
725        let mut effective_mode = env_mode.unwrap_or(self.options.request_mode.clone());
726        // Apply runtime overrides first if present
727        if let Some(map) = &env_overrides {
728            if let Some(v) = map.get(&format!("{} {}", method, path)) {
729                if let Some(m) = v.as_str() {
730                    effective_mode = match m {
731                        "off" => ValidationMode::Disabled,
732                        "warn" => ValidationMode::Warn,
733                        _ => ValidationMode::Enforce,
734                    };
735                }
736            }
737        }
738        // Then static options overrides
739        if let Some(override_mode) = self.options.overrides.get(&format!("{} {}", method, path)) {
740            effective_mode = override_mode.clone();
741        }
742        if matches!(effective_mode, ValidationMode::Disabled) {
743            return Ok(());
744        }
745        if let Some(route) = self.get_route(path, method) {
746            if matches!(effective_mode, ValidationMode::Disabled) {
747                return Ok(());
748            }
749            let mut errors: Vec<String> = Vec::new();
750            let mut details: Vec<serde_json::Value> = Vec::new();
751            // Validate request body if required
752            if let Some(schema) = &route.operation.request_body {
753                if let Some(value) = body {
754                    // First resolve the request body reference if it's a reference
755                    let request_body = match schema {
756                        openapiv3::ReferenceOr::Item(rb) => Some(rb),
757                        openapiv3::ReferenceOr::Reference { reference } => {
758                            // Try to resolve request body reference through spec
759                            self.spec
760                                .spec
761                                .components
762                                .as_ref()
763                                .and_then(|components| {
764                                    components.request_bodies.get(
765                                        reference.trim_start_matches("#/components/requestBodies/"),
766                                    )
767                                })
768                                .and_then(|rb_ref| rb_ref.as_item())
769                        }
770                    };
771
772                    if let Some(rb) = request_body {
773                        if let Some(content) = rb.content.get("application/json") {
774                            if let Some(schema_ref) = &content.schema {
775                                // Resolve schema reference and validate
776                                match schema_ref {
777                                    openapiv3::ReferenceOr::Item(schema) => {
778                                        // Direct schema - validate immediately
779                                        if let Err(validation_error) =
780                                            OpenApiSchema::new(schema.clone()).validate(value)
781                                        {
782                                            let error_msg = validation_error.to_string();
783                                            errors.push(format!(
784                                                "body validation failed: {}",
785                                                error_msg
786                                            ));
787                                            if aggregate {
788                                                details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
789                                            }
790                                        }
791                                    }
792                                    openapiv3::ReferenceOr::Reference { reference } => {
793                                        // Referenced schema - resolve and validate
794                                        if let Some(resolved_schema_ref) =
795                                            self.spec.get_schema(reference)
796                                        {
797                                            if let Err(validation_error) = OpenApiSchema::new(
798                                                resolved_schema_ref.schema.clone(),
799                                            )
800                                            .validate(value)
801                                            {
802                                                let error_msg = validation_error.to_string();
803                                                errors.push(format!(
804                                                    "body validation failed: {}",
805                                                    error_msg
806                                                ));
807                                                if aggregate {
808                                                    details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
809                                                }
810                                            }
811                                        } else {
812                                            // Schema reference couldn't be resolved
813                                            errors.push(format!("body validation failed: could not resolve schema reference {}", reference));
814                                            if aggregate {
815                                                details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve schema reference"}));
816                                            }
817                                        }
818                                    }
819                                }
820                            }
821                        }
822                    } else {
823                        // Request body reference couldn't be resolved or no application/json content
824                        errors.push("body validation failed: could not resolve request body or no application/json content".to_string());
825                        if aggregate {
826                            details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve request body reference"}));
827                        }
828                    }
829                } else {
830                    errors.push("body: Request body is required but not provided".to_string());
831                    details.push(serde_json::json!({"path":"body","code":"required","message":"Request body is required"}));
832                }
833            } else if body.is_some() {
834                // No body expected but provided — not an error by default, but log it
835                tracing::debug!("Body provided for operation without requestBody; accepting");
836            }
837
838            // Validate path/query parameters
839            for p_ref in &route.operation.parameters {
840                if let Some(p) = p_ref.as_item() {
841                    match p {
842                        openapiv3::Parameter::Path { parameter_data, .. } => {
843                            validate_parameter(
844                                parameter_data,
845                                path_params,
846                                "path",
847                                aggregate,
848                                &mut errors,
849                                &mut details,
850                            );
851                        }
852                        openapiv3::Parameter::Query {
853                            parameter_data,
854                            style,
855                            ..
856                        } => {
857                            // For query deepObject, reconstruct value from key-likes: name[prop]
858                            let deep_value = None; // Simplified for now
859                            let style_str = match style {
860                                openapiv3::QueryStyle::Form => Some("form"),
861                                openapiv3::QueryStyle::SpaceDelimited => Some("spaceDelimited"),
862                                openapiv3::QueryStyle::PipeDelimited => Some("pipeDelimited"),
863                                openapiv3::QueryStyle::DeepObject => Some("deepObject"),
864                            };
865                            validate_parameter_with_deep_object(
866                                parameter_data,
867                                query_params,
868                                "query",
869                                deep_value,
870                                style_str,
871                                aggregate,
872                                &mut errors,
873                                &mut details,
874                            );
875                        }
876                        openapiv3::Parameter::Header { parameter_data, .. } => {
877                            validate_parameter(
878                                parameter_data,
879                                header_params,
880                                "header",
881                                aggregate,
882                                &mut errors,
883                                &mut details,
884                            );
885                        }
886                        openapiv3::Parameter::Cookie { parameter_data, .. } => {
887                            validate_parameter(
888                                parameter_data,
889                                cookie_params,
890                                "cookie",
891                                aggregate,
892                                &mut errors,
893                                &mut details,
894                            );
895                        }
896                    }
897                }
898            }
899            if errors.is_empty() {
900                return Ok(());
901            }
902            match effective_mode {
903                ValidationMode::Disabled => Ok(()),
904                ValidationMode::Warn => {
905                    tracing::warn!("Request validation warnings: {:?}", errors);
906                    Ok(())
907                }
908                ValidationMode::Enforce => Err(Error::validation(
909                    serde_json::json!({"errors": errors, "details": details}).to_string(),
910                )),
911            }
912        } else {
913            Err(Error::generic(format!("Route {} {} not found in OpenAPI spec", method, path)))
914        }
915    }
916
917    // Legacy helper removed (mock + status selection happens in handler via route.mock_response_with_status)
918
919    /// Get all paths defined in the spec
920    pub fn paths(&self) -> Vec<String> {
921        let mut paths: Vec<String> = self.routes.iter().map(|route| route.path.clone()).collect();
922        paths.sort();
923        paths.dedup();
924        paths
925    }
926
927    /// Get all HTTP methods supported
928    pub fn methods(&self) -> Vec<String> {
929        let mut methods: Vec<String> =
930            self.routes.iter().map(|route| route.method.clone()).collect();
931        methods.sort();
932        methods.dedup();
933        methods
934    }
935
936    /// Get operation details for a route
937    pub fn get_operation(&self, path: &str, method: &str) -> Option<OpenApiOperation> {
938        self.get_route(path, method).map(|route| {
939            OpenApiOperation::from_operation(
940                &route.method,
941                route.path.clone(),
942                &route.operation,
943                &self.spec,
944            )
945        })
946    }
947
948    /// Extract path parameters from a request path by matching against known routes
949    pub fn extract_path_parameters(
950        &self,
951        path: &str,
952        method: &str,
953    ) -> std::collections::HashMap<String, String> {
954        for route in &self.routes {
955            if route.method != method {
956                continue;
957            }
958
959            if let Some(params) = self.match_path_to_route(path, &route.path) {
960                return params;
961            }
962        }
963        std::collections::HashMap::new()
964    }
965
966    /// Match a request path against a route pattern and extract parameters
967    fn match_path_to_route(
968        &self,
969        request_path: &str,
970        route_pattern: &str,
971    ) -> Option<std::collections::HashMap<String, String>> {
972        let mut params = std::collections::HashMap::new();
973
974        // Split both paths into segments
975        let request_segments: Vec<&str> = request_path.trim_start_matches('/').split('/').collect();
976        let pattern_segments: Vec<&str> =
977            route_pattern.trim_start_matches('/').split('/').collect();
978
979        if request_segments.len() != pattern_segments.len() {
980            return None;
981        }
982
983        for (req_seg, pat_seg) in request_segments.iter().zip(pattern_segments.iter()) {
984            if pat_seg.starts_with('{') && pat_seg.ends_with('}') {
985                // This is a parameter
986                let param_name = &pat_seg[1..pat_seg.len() - 1];
987                params.insert(param_name.to_string(), req_seg.to_string());
988            } else if req_seg != pat_seg {
989                // Static segment doesn't match
990                return None;
991            }
992        }
993
994        Some(params)
995    }
996
997    /// Convert OpenAPI path to Axum-compatible path
998    /// This is a utility function for converting path parameters from {param} to :param format
999    pub fn convert_path_to_axum(openapi_path: &str) -> String {
1000        // Axum v0.7+ uses {param} format, same as OpenAPI
1001        openapi_path.to_string()
1002    }
1003
1004    /// Build router with AI generator support
1005    pub fn build_router_with_ai(
1006        &self,
1007        ai_generator: Option<std::sync::Arc<dyn AiGenerator + Send + Sync>>,
1008    ) -> Router {
1009        use axum::routing::{delete, get, patch, post, put};
1010
1011        let mut router = Router::new();
1012        tracing::debug!("Building router with AI support from {} routes", self.routes.len());
1013
1014        for route in &self.routes {
1015            tracing::debug!("Adding AI-enabled route: {} {}", route.method, route.path);
1016
1017            let route_clone = route.clone();
1018            let ai_generator_clone = ai_generator.clone();
1019
1020            // Create async handler that extracts request data and builds context
1021            let handler = move |headers: HeaderMap, body: Option<Json<Value>>| {
1022                let route = route_clone.clone();
1023                let ai_generator = ai_generator_clone.clone();
1024
1025                async move {
1026                    tracing::debug!(
1027                        "Handling AI request for route: {} {}",
1028                        route.method,
1029                        route.path
1030                    );
1031
1032                    // Build request context
1033                    let mut context = RequestContext::new(route.method.clone(), route.path.clone());
1034
1035                    // Extract headers
1036                    context.headers = headers
1037                        .iter()
1038                        .map(|(k, v)| {
1039                            (k.to_string(), Value::String(v.to_str().unwrap_or("").to_string()))
1040                        })
1041                        .collect();
1042
1043                    // Extract body if present
1044                    context.body = body.map(|Json(b)| b);
1045
1046                    // Generate AI response if AI generator is available and route has AI config
1047                    let (status, response) = if let (Some(generator), Some(_ai_config)) =
1048                        (ai_generator, &route.ai_config)
1049                    {
1050                        route
1051                            .mock_response_with_status_async(&context, Some(generator.as_ref()))
1052                            .await
1053                    } else {
1054                        // No AI support, use static response
1055                        route.mock_response_with_status()
1056                    };
1057
1058                    (
1059                        axum::http::StatusCode::from_u16(status)
1060                            .unwrap_or(axum::http::StatusCode::OK),
1061                        axum::response::Json(response),
1062                    )
1063                }
1064            };
1065
1066            match route.method.as_str() {
1067                "GET" => {
1068                    router = router.route(&route.path, get(handler));
1069                }
1070                "POST" => {
1071                    router = router.route(&route.path, post(handler));
1072                }
1073                "PUT" => {
1074                    router = router.route(&route.path, put(handler));
1075                }
1076                "DELETE" => {
1077                    router = router.route(&route.path, delete(handler));
1078                }
1079                "PATCH" => {
1080                    router = router.route(&route.path, patch(handler));
1081                }
1082                _ => tracing::warn!("Unsupported HTTP method for AI: {}", route.method),
1083            }
1084        }
1085
1086        router
1087    }
1088}
1089
1090// Note: templating helpers are now in core::templating (shared across modules)
1091
1092static LAST_ERRORS: Lazy<Mutex<VecDeque<serde_json::Value>>> =
1093    Lazy::new(|| Mutex::new(VecDeque::with_capacity(20)));
1094
1095/// Record last validation error for Admin UI inspection
1096pub fn record_validation_error(v: &serde_json::Value) {
1097    if let Ok(mut q) = LAST_ERRORS.lock() {
1098        if q.len() >= 20 {
1099            q.pop_front();
1100        }
1101        q.push_back(v.clone());
1102    }
1103    // If mutex is poisoned, we silently fail - validation errors are informational only
1104}
1105
1106/// Get most recent validation error
1107pub fn get_last_validation_error() -> Option<serde_json::Value> {
1108    LAST_ERRORS.lock().ok()?.back().cloned()
1109}
1110
1111/// Get recent validation errors (most recent last)
1112pub fn get_validation_errors() -> Vec<serde_json::Value> {
1113    LAST_ERRORS.lock().map(|q| q.iter().cloned().collect()).unwrap_or_default()
1114}
1115
1116/// Coerce a parameter `value` into the expected JSON type per `schema` where reasonable.
1117/// Applies only to param contexts (not request bodies). Conservative conversions:
1118/// - integer/number: parse from string; arrays: split comma-separated strings and coerce items
1119/// - boolean: parse true/false (case-insensitive) from string
1120fn coerce_value_for_schema(value: &Value, schema: &openapiv3::Schema) -> Value {
1121    // Basic coercion: try to parse strings as appropriate types
1122    match value {
1123        Value::String(s) => {
1124            // Check if schema expects an array and we have a comma-separated string
1125            if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
1126                &schema.schema_kind
1127            {
1128                if s.contains(',') {
1129                    // Split comma-separated string into array
1130                    let parts: Vec<&str> = s.split(',').map(|s| s.trim()).collect();
1131                    let mut array_values = Vec::new();
1132
1133                    for part in parts {
1134                        // Coerce each part based on array item type
1135                        if let Some(items_schema) = &array_type.items {
1136                            if let Some(items_schema_obj) = items_schema.as_item() {
1137                                let part_value = Value::String(part.to_string());
1138                                let coerced_part =
1139                                    coerce_value_for_schema(&part_value, items_schema_obj);
1140                                array_values.push(coerced_part);
1141                            } else {
1142                                // If items schema is a reference or not available, keep as string
1143                                array_values.push(Value::String(part.to_string()));
1144                            }
1145                        } else {
1146                            // No items schema defined, keep as string
1147                            array_values.push(Value::String(part.to_string()));
1148                        }
1149                    }
1150                    return Value::Array(array_values);
1151                }
1152            }
1153
1154            // Only coerce if the schema expects a different type
1155            match &schema.schema_kind {
1156                openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
1157                    // Schema expects string, keep as string
1158                    value.clone()
1159                }
1160                openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => {
1161                    // Schema expects number, try to parse
1162                    if let Ok(n) = s.parse::<f64>() {
1163                        if let Some(num) = serde_json::Number::from_f64(n) {
1164                            return Value::Number(num);
1165                        }
1166                    }
1167                    value.clone()
1168                }
1169                openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => {
1170                    // Schema expects integer, try to parse
1171                    if let Ok(n) = s.parse::<i64>() {
1172                        if let Some(num) = serde_json::Number::from_f64(n as f64) {
1173                            return Value::Number(num);
1174                        }
1175                    }
1176                    value.clone()
1177                }
1178                openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => {
1179                    // Schema expects boolean, try to parse
1180                    match s.to_lowercase().as_str() {
1181                        "true" | "1" | "yes" | "on" => Value::Bool(true),
1182                        "false" | "0" | "no" | "off" => Value::Bool(false),
1183                        _ => value.clone(),
1184                    }
1185                }
1186                _ => {
1187                    // Unknown schema type, keep as string
1188                    value.clone()
1189                }
1190            }
1191        }
1192        _ => value.clone(),
1193    }
1194}
1195
1196/// Apply style-aware coercion for query params
1197fn coerce_by_style(value: &Value, schema: &openapiv3::Schema, style: Option<&str>) -> Value {
1198    // Style-aware coercion for query parameters
1199    match value {
1200        Value::String(s) => {
1201            // Check if schema expects an array and we have a delimited string
1202            if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
1203                &schema.schema_kind
1204            {
1205                let delimiter = match style {
1206                    Some("spaceDelimited") => " ",
1207                    Some("pipeDelimited") => "|",
1208                    Some("form") | None => ",", // Default to form style (comma-separated)
1209                    _ => ",",                   // Fallback to comma
1210                };
1211
1212                if s.contains(delimiter) {
1213                    // Split delimited string into array
1214                    let parts: Vec<&str> = s.split(delimiter).map(|s| s.trim()).collect();
1215                    let mut array_values = Vec::new();
1216
1217                    for part in parts {
1218                        // Coerce each part based on array item type
1219                        if let Some(items_schema) = &array_type.items {
1220                            if let Some(items_schema_obj) = items_schema.as_item() {
1221                                let part_value = Value::String(part.to_string());
1222                                let coerced_part =
1223                                    coerce_by_style(&part_value, items_schema_obj, style);
1224                                array_values.push(coerced_part);
1225                            } else {
1226                                // If items schema is a reference or not available, keep as string
1227                                array_values.push(Value::String(part.to_string()));
1228                            }
1229                        } else {
1230                            // No items schema defined, keep as string
1231                            array_values.push(Value::String(part.to_string()));
1232                        }
1233                    }
1234                    return Value::Array(array_values);
1235                }
1236            }
1237
1238            // Try to parse as number first
1239            if let Ok(n) = s.parse::<f64>() {
1240                if let Some(num) = serde_json::Number::from_f64(n) {
1241                    return Value::Number(num);
1242                }
1243            }
1244            // Try to parse as boolean
1245            match s.to_lowercase().as_str() {
1246                "true" | "1" | "yes" | "on" => return Value::Bool(true),
1247                "false" | "0" | "no" | "off" => return Value::Bool(false),
1248                _ => {}
1249            }
1250            // Keep as string
1251            value.clone()
1252        }
1253        _ => value.clone(),
1254    }
1255}
1256
1257/// Build a deepObject from query params like `name[prop]=val`
1258fn build_deep_object(name: &str, params: &Map<String, Value>) -> Option<Value> {
1259    let prefix = format!("{}[", name);
1260    let mut obj = Map::new();
1261    for (k, v) in params.iter() {
1262        if let Some(rest) = k.strip_prefix(&prefix) {
1263            if let Some(key) = rest.strip_suffix(']') {
1264                obj.insert(key.to_string(), v.clone());
1265            }
1266        }
1267    }
1268    if obj.is_empty() {
1269        None
1270    } else {
1271        Some(Value::Object(obj))
1272    }
1273}
1274
1275// Import the enhanced schema diff functionality
1276// use crate::schema_diff::{validation_diff, to_enhanced_422_json, ValidationError}; // Not currently used
1277
1278/// Generate an enhanced 422 response with detailed schema validation errors
1279/// This function provides comprehensive error information using the new schema diff utility
1280#[allow(clippy::too_many_arguments)]
1281fn generate_enhanced_422_response(
1282    validator: &OpenApiRouteRegistry,
1283    path_template: &str,
1284    method: &str,
1285    body: Option<&Value>,
1286    path_params: &serde_json::Map<String, Value>,
1287    query_params: &serde_json::Map<String, Value>,
1288    header_params: &serde_json::Map<String, Value>,
1289    cookie_params: &serde_json::Map<String, Value>,
1290) -> Value {
1291    let mut field_errors = Vec::new();
1292
1293    // Extract schema validation details if we have a route
1294    if let Some(route) = validator.get_route(path_template, method) {
1295        // Validate request body with detailed error collection
1296        if let Some(schema) = &route.operation.request_body {
1297            if let Some(value) = body {
1298                if let Some(content) =
1299                    schema.as_item().and_then(|rb| rb.content.get("application/json"))
1300                {
1301                    if let Some(_schema_ref) = &content.schema {
1302                        // Basic JSON validation - schema validation deferred
1303                        if serde_json::from_value::<serde_json::Value>(value.clone()).is_err() {
1304                            field_errors.push(json!({
1305                                "path": "body",
1306                                "message": "invalid JSON"
1307                            }));
1308                        }
1309                    }
1310                }
1311            } else {
1312                field_errors.push(json!({
1313                    "path": "body",
1314                    "expected": "object",
1315                    "found": "missing",
1316                    "message": "Request body is required but not provided"
1317                }));
1318            }
1319        }
1320
1321        // Validate parameters with detailed error collection
1322        for param_ref in &route.operation.parameters {
1323            if let Some(param) = param_ref.as_item() {
1324                match param {
1325                    openapiv3::Parameter::Path { parameter_data, .. } => {
1326                        validate_parameter_detailed(
1327                            parameter_data,
1328                            path_params,
1329                            "path",
1330                            "path parameter",
1331                            &mut field_errors,
1332                        );
1333                    }
1334                    openapiv3::Parameter::Query { parameter_data, .. } => {
1335                        let deep_value = if Some("form") == Some("deepObject") {
1336                            build_deep_object(&parameter_data.name, query_params)
1337                        } else {
1338                            None
1339                        };
1340                        validate_parameter_detailed_with_deep(
1341                            parameter_data,
1342                            query_params,
1343                            "query",
1344                            "query parameter",
1345                            deep_value,
1346                            &mut field_errors,
1347                        );
1348                    }
1349                    openapiv3::Parameter::Header { parameter_data, .. } => {
1350                        validate_parameter_detailed(
1351                            parameter_data,
1352                            header_params,
1353                            "header",
1354                            "header parameter",
1355                            &mut field_errors,
1356                        );
1357                    }
1358                    openapiv3::Parameter::Cookie { parameter_data, .. } => {
1359                        validate_parameter_detailed(
1360                            parameter_data,
1361                            cookie_params,
1362                            "cookie",
1363                            "cookie parameter",
1364                            &mut field_errors,
1365                        );
1366                    }
1367                }
1368            }
1369        }
1370    }
1371
1372    // Return the detailed 422 error format
1373    json!({
1374        "error": "Schema validation failed",
1375        "details": field_errors,
1376        "method": method,
1377        "path": path_template,
1378        "timestamp": Utc::now().to_rfc3339(),
1379        "validation_type": "openapi_schema"
1380    })
1381}
1382
1383/// Helper function to validate a parameter
1384fn validate_parameter(
1385    parameter_data: &openapiv3::ParameterData,
1386    params_map: &Map<String, Value>,
1387    prefix: &str,
1388    aggregate: bool,
1389    errors: &mut Vec<String>,
1390    details: &mut Vec<serde_json::Value>,
1391) {
1392    match params_map.get(&parameter_data.name) {
1393        Some(v) => {
1394            if let ParameterSchemaOrContent::Schema(s) = &parameter_data.format {
1395                if let Some(schema) = s.as_item() {
1396                    let coerced = coerce_value_for_schema(v, schema);
1397                    // Validate the coerced value against the schema
1398                    if let Err(validation_error) =
1399                        OpenApiSchema::new(schema.clone()).validate(&coerced)
1400                    {
1401                        let error_msg = validation_error.to_string();
1402                        errors.push(format!(
1403                            "{} parameter '{}' validation failed: {}",
1404                            prefix, parameter_data.name, error_msg
1405                        ));
1406                        if aggregate {
1407                            details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
1408                        }
1409                    }
1410                }
1411            }
1412        }
1413        None => {
1414            if parameter_data.required {
1415                errors.push(format!(
1416                    "missing required {} parameter '{}'",
1417                    prefix, parameter_data.name
1418                ));
1419                details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
1420            }
1421        }
1422    }
1423}
1424
1425/// Helper function to validate a parameter with deep object support
1426#[allow(clippy::too_many_arguments)]
1427fn validate_parameter_with_deep_object(
1428    parameter_data: &openapiv3::ParameterData,
1429    params_map: &Map<String, Value>,
1430    prefix: &str,
1431    deep_value: Option<Value>,
1432    style: Option<&str>,
1433    aggregate: bool,
1434    errors: &mut Vec<String>,
1435    details: &mut Vec<serde_json::Value>,
1436) {
1437    match deep_value.as_ref().or_else(|| params_map.get(&parameter_data.name)) {
1438        Some(v) => {
1439            if let ParameterSchemaOrContent::Schema(s) = &parameter_data.format {
1440                if let Some(schema) = s.as_item() {
1441                    let coerced = coerce_by_style(v, schema, style); // Use the actual style
1442                                                                     // Validate the coerced value against the schema
1443                    if let Err(validation_error) =
1444                        OpenApiSchema::new(schema.clone()).validate(&coerced)
1445                    {
1446                        let error_msg = validation_error.to_string();
1447                        errors.push(format!(
1448                            "{} parameter '{}' validation failed: {}",
1449                            prefix, parameter_data.name, error_msg
1450                        ));
1451                        if aggregate {
1452                            details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
1453                        }
1454                    }
1455                }
1456            }
1457        }
1458        None => {
1459            if parameter_data.required {
1460                errors.push(format!(
1461                    "missing required {} parameter '{}'",
1462                    prefix, parameter_data.name
1463                ));
1464                details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
1465            }
1466        }
1467    }
1468}
1469
1470/// Helper function to validate a parameter with detailed error collection
1471fn validate_parameter_detailed(
1472    parameter_data: &openapiv3::ParameterData,
1473    params_map: &Map<String, Value>,
1474    location: &str,
1475    value_type: &str,
1476    field_errors: &mut Vec<Value>,
1477) {
1478    match params_map.get(&parameter_data.name) {
1479        Some(value) => {
1480            if let ParameterSchemaOrContent::Schema(schema) = &parameter_data.format {
1481                // Collect detailed validation errors for this parameter
1482                let details: Vec<serde_json::Value> = Vec::new();
1483                let param_path = format!("{}.{}", location, parameter_data.name);
1484
1485                // Apply coercion before validation
1486                if let Some(schema_ref) = schema.as_item() {
1487                    let coerced_value = coerce_value_for_schema(value, schema_ref);
1488                    // Validate the coerced value against the schema
1489                    if let Err(validation_error) =
1490                        OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
1491                    {
1492                        field_errors.push(json!({
1493                            "path": param_path,
1494                            "expected": "valid according to schema",
1495                            "found": coerced_value,
1496                            "message": validation_error.to_string()
1497                        }));
1498                    }
1499                }
1500
1501                for detail in details {
1502                    field_errors.push(json!({
1503                        "path": detail["path"],
1504                        "expected": detail["expected_type"],
1505                        "found": detail["value"],
1506                        "message": detail["message"]
1507                    }));
1508                }
1509            }
1510        }
1511        None => {
1512            if parameter_data.required {
1513                field_errors.push(json!({
1514                    "path": format!("{}.{}", location, parameter_data.name),
1515                    "expected": "value",
1516                    "found": "missing",
1517                    "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
1518                }));
1519            }
1520        }
1521    }
1522}
1523
1524/// Helper function to validate a parameter with deep object support and detailed errors
1525fn validate_parameter_detailed_with_deep(
1526    parameter_data: &openapiv3::ParameterData,
1527    params_map: &Map<String, Value>,
1528    location: &str,
1529    value_type: &str,
1530    deep_value: Option<Value>,
1531    field_errors: &mut Vec<Value>,
1532) {
1533    match deep_value.as_ref().or_else(|| params_map.get(&parameter_data.name)) {
1534        Some(value) => {
1535            if let ParameterSchemaOrContent::Schema(schema) = &parameter_data.format {
1536                // Collect detailed validation errors for this parameter
1537                let details: Vec<serde_json::Value> = Vec::new();
1538                let param_path = format!("{}.{}", location, parameter_data.name);
1539
1540                // Apply coercion before validation
1541                if let Some(schema_ref) = schema.as_item() {
1542                    let coerced_value = coerce_by_style(value, schema_ref, Some("form")); // Default to form style for now
1543                                                                                          // Validate the coerced value against the schema
1544                    if let Err(validation_error) =
1545                        OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
1546                    {
1547                        field_errors.push(json!({
1548                            "path": param_path,
1549                            "expected": "valid according to schema",
1550                            "found": coerced_value,
1551                            "message": validation_error.to_string()
1552                        }));
1553                    }
1554                }
1555
1556                for detail in details {
1557                    field_errors.push(json!({
1558                        "path": detail["path"],
1559                        "expected": detail["expected_type"],
1560                        "found": detail["value"],
1561                        "message": detail["message"]
1562                    }));
1563                }
1564            }
1565        }
1566        None => {
1567            if parameter_data.required {
1568                field_errors.push(json!({
1569                    "path": format!("{}.{}", location, parameter_data.name),
1570                    "expected": "value",
1571                    "found": "missing",
1572                    "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
1573                }));
1574            }
1575        }
1576    }
1577}
1578
1579/// Helper function to create an OpenAPI route registry from a file
1580pub async fn create_registry_from_file<P: AsRef<std::path::Path>>(
1581    path: P,
1582) -> Result<OpenApiRouteRegistry> {
1583    let spec = OpenApiSpec::from_file(path).await?;
1584    spec.validate()?;
1585    Ok(OpenApiRouteRegistry::new(spec))
1586}
1587
1588/// Helper function to create an OpenAPI route registry from JSON
1589pub fn create_registry_from_json(json: Value) -> Result<OpenApiRouteRegistry> {
1590    let spec = OpenApiSpec::from_json(json)?;
1591    spec.validate()?;
1592    Ok(OpenApiRouteRegistry::new(spec))
1593}
1594
1595#[cfg(test)]
1596mod tests {
1597    use super::*;
1598    use serde_json::json;
1599
1600    #[tokio::test]
1601    async fn test_registry_creation() {
1602        let spec_json = json!({
1603            "openapi": "3.0.0",
1604            "info": {
1605                "title": "Test API",
1606                "version": "1.0.0"
1607            },
1608            "paths": {
1609                "/users": {
1610                    "get": {
1611                        "summary": "Get users",
1612                        "responses": {
1613                            "200": {
1614                                "description": "Success",
1615                                "content": {
1616                                    "application/json": {
1617                                        "schema": {
1618                                            "type": "array",
1619                                            "items": {
1620                                                "type": "object",
1621                                                "properties": {
1622                                                    "id": {"type": "integer"},
1623                                                    "name": {"type": "string"}
1624                                                }
1625                                            }
1626                                        }
1627                                    }
1628                                }
1629                            }
1630                        }
1631                    },
1632                    "post": {
1633                        "summary": "Create user",
1634                        "requestBody": {
1635                            "content": {
1636                                "application/json": {
1637                                    "schema": {
1638                                        "type": "object",
1639                                        "properties": {
1640                                            "name": {"type": "string"}
1641                                        },
1642                                        "required": ["name"]
1643                                    }
1644                                }
1645                            }
1646                        },
1647                        "responses": {
1648                            "201": {
1649                                "description": "Created",
1650                                "content": {
1651                                    "application/json": {
1652                                        "schema": {
1653                                            "type": "object",
1654                                            "properties": {
1655                                                "id": {"type": "integer"},
1656                                                "name": {"type": "string"}
1657                                            }
1658                                        }
1659                                    }
1660                                }
1661                            }
1662                        }
1663                    }
1664                },
1665                "/users/{id}": {
1666                    "get": {
1667                        "summary": "Get user by ID",
1668                        "parameters": [
1669                            {
1670                                "name": "id",
1671                                "in": "path",
1672                                "required": true,
1673                                "schema": {"type": "integer"}
1674                            }
1675                        ],
1676                        "responses": {
1677                            "200": {
1678                                "description": "Success",
1679                                "content": {
1680                                    "application/json": {
1681                                        "schema": {
1682                                            "type": "object",
1683                                            "properties": {
1684                                                "id": {"type": "integer"},
1685                                                "name": {"type": "string"}
1686                                            }
1687                                        }
1688                                    }
1689                                }
1690                            }
1691                        }
1692                    }
1693                }
1694            }
1695        });
1696
1697        let registry = create_registry_from_json(spec_json).unwrap();
1698
1699        // Test basic properties
1700        assert_eq!(registry.paths().len(), 2);
1701        assert!(registry.paths().contains(&"/users".to_string()));
1702        assert!(registry.paths().contains(&"/users/{id}".to_string()));
1703
1704        assert_eq!(registry.methods().len(), 2);
1705        assert!(registry.methods().contains(&"GET".to_string()));
1706        assert!(registry.methods().contains(&"POST".to_string()));
1707
1708        // Test route lookup
1709        let get_users_route = registry.get_route("/users", "GET").unwrap();
1710        assert_eq!(get_users_route.method, "GET");
1711        assert_eq!(get_users_route.path, "/users");
1712
1713        let post_users_route = registry.get_route("/users", "POST").unwrap();
1714        assert_eq!(post_users_route.method, "POST");
1715        assert!(post_users_route.operation.request_body.is_some());
1716
1717        // Test path parameter conversion
1718        let user_by_id_route = registry.get_route("/users/{id}", "GET").unwrap();
1719        assert_eq!(user_by_id_route.axum_path(), "/users/{id}");
1720    }
1721
1722    #[tokio::test]
1723    async fn test_validate_request_with_params_and_formats() {
1724        let spec_json = json!({
1725            "openapi": "3.0.0",
1726            "info": { "title": "Test API", "version": "1.0.0" },
1727            "paths": {
1728                "/users/{id}": {
1729                    "post": {
1730                        "parameters": [
1731                            { "name": "id", "in": "path", "required": true, "schema": {"type": "string"} },
1732                            { "name": "q",  "in": "query", "required": false, "schema": {"type": "integer"} }
1733                        ],
1734                        "requestBody": {
1735                            "content": {
1736                                "application/json": {
1737                                    "schema": {
1738                                        "type": "object",
1739                                        "required": ["email", "website"],
1740                                        "properties": {
1741                                            "email":   {"type": "string", "format": "email"},
1742                                            "website": {"type": "string", "format": "uri"}
1743                                        }
1744                                    }
1745                                }
1746                            }
1747                        },
1748                        "responses": {"200": {"description": "ok"}}
1749                    }
1750                }
1751            }
1752        });
1753
1754        let registry = create_registry_from_json(spec_json).unwrap();
1755        let mut path_params = serde_json::Map::new();
1756        path_params.insert("id".to_string(), json!("abc"));
1757        let mut query_params = serde_json::Map::new();
1758        query_params.insert("q".to_string(), json!(123));
1759
1760        // valid body
1761        let body = json!({"email":"a@b.co","website":"https://example.com"});
1762        assert!(registry
1763            .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
1764            .is_ok());
1765
1766        // invalid email
1767        let bad_email = json!({"email":"not-an-email","website":"https://example.com"});
1768        assert!(registry
1769            .validate_request_with(
1770                "/users/{id}",
1771                "POST",
1772                &path_params,
1773                &query_params,
1774                Some(&bad_email)
1775            )
1776            .is_err());
1777
1778        // missing required path param
1779        let empty_path_params = serde_json::Map::new();
1780        assert!(registry
1781            .validate_request_with(
1782                "/users/{id}",
1783                "POST",
1784                &empty_path_params,
1785                &query_params,
1786                Some(&body)
1787            )
1788            .is_err());
1789    }
1790
1791    #[tokio::test]
1792    async fn test_ref_resolution_for_params_and_body() {
1793        let spec_json = json!({
1794            "openapi": "3.0.0",
1795            "info": { "title": "Ref API", "version": "1.0.0" },
1796            "components": {
1797                "schemas": {
1798                    "EmailWebsite": {
1799                        "type": "object",
1800                        "required": ["email", "website"],
1801                        "properties": {
1802                            "email":   {"type": "string", "format": "email"},
1803                            "website": {"type": "string", "format": "uri"}
1804                        }
1805                    }
1806                },
1807                "parameters": {
1808                    "PathId": {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}},
1809                    "QueryQ": {"name": "q",  "in": "query", "required": false, "schema": {"type": "integer"}}
1810                },
1811                "requestBodies": {
1812                    "CreateUser": {
1813                        "content": {
1814                            "application/json": {
1815                                "schema": {"$ref": "#/components/schemas/EmailWebsite"}
1816                            }
1817                        }
1818                    }
1819                }
1820            },
1821            "paths": {
1822                "/users/{id}": {
1823                    "post": {
1824                        "parameters": [
1825                            {"$ref": "#/components/parameters/PathId"},
1826                            {"$ref": "#/components/parameters/QueryQ"}
1827                        ],
1828                        "requestBody": {"$ref": "#/components/requestBodies/CreateUser"},
1829                        "responses": {"200": {"description": "ok"}}
1830                    }
1831                }
1832            }
1833        });
1834
1835        let registry = create_registry_from_json(spec_json).unwrap();
1836        let mut path_params = serde_json::Map::new();
1837        path_params.insert("id".to_string(), json!("abc"));
1838        let mut query_params = serde_json::Map::new();
1839        query_params.insert("q".to_string(), json!(7));
1840
1841        let body = json!({"email":"user@example.com","website":"https://example.com"});
1842        assert!(registry
1843            .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
1844            .is_ok());
1845
1846        let bad = json!({"email":"nope","website":"https://example.com"});
1847        assert!(registry
1848            .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&bad))
1849            .is_err());
1850    }
1851
1852    #[tokio::test]
1853    async fn test_header_cookie_and_query_coercion() {
1854        let spec_json = json!({
1855            "openapi": "3.0.0",
1856            "info": { "title": "Params API", "version": "1.0.0" },
1857            "paths": {
1858                "/items": {
1859                    "get": {
1860                        "parameters": [
1861                            {"name": "X-Flag", "in": "header", "required": true, "schema": {"type": "boolean"}},
1862                            {"name": "session", "in": "cookie", "required": true, "schema": {"type": "string"}},
1863                            {"name": "ids", "in": "query", "required": false, "schema": {"type": "array", "items": {"type": "integer"}}}
1864                        ],
1865                        "responses": {"200": {"description": "ok"}}
1866                    }
1867                }
1868            }
1869        });
1870
1871        let registry = create_registry_from_json(spec_json).unwrap();
1872
1873        let path_params = serde_json::Map::new();
1874        let mut query_params = serde_json::Map::new();
1875        // comma-separated string for array should coerce
1876        query_params.insert("ids".to_string(), json!("1,2,3"));
1877        let mut header_params = serde_json::Map::new();
1878        header_params.insert("X-Flag".to_string(), json!("true"));
1879        let mut cookie_params = serde_json::Map::new();
1880        cookie_params.insert("session".to_string(), json!("abc123"));
1881
1882        assert!(registry
1883            .validate_request_with_all(
1884                "/items",
1885                "GET",
1886                &path_params,
1887                &query_params,
1888                &header_params,
1889                &cookie_params,
1890                None
1891            )
1892            .is_ok());
1893
1894        // Missing required cookie
1895        let empty_cookie = serde_json::Map::new();
1896        assert!(registry
1897            .validate_request_with_all(
1898                "/items",
1899                "GET",
1900                &path_params,
1901                &query_params,
1902                &header_params,
1903                &empty_cookie,
1904                None
1905            )
1906            .is_err());
1907
1908        // Bad boolean header value (cannot coerce)
1909        let mut bad_header = serde_json::Map::new();
1910        bad_header.insert("X-Flag".to_string(), json!("notabool"));
1911        assert!(registry
1912            .validate_request_with_all(
1913                "/items",
1914                "GET",
1915                &path_params,
1916                &query_params,
1917                &bad_header,
1918                &cookie_params,
1919                None
1920            )
1921            .is_err());
1922    }
1923
1924    #[tokio::test]
1925    async fn test_query_styles_space_pipe_deepobject() {
1926        let spec_json = json!({
1927            "openapi": "3.0.0",
1928            "info": { "title": "Query Styles API", "version": "1.0.0" },
1929            "paths": {"/search": {"get": {
1930                "parameters": [
1931                    {"name":"tags","in":"query","style":"spaceDelimited","schema":{"type":"array","items":{"type":"string"}}},
1932                    {"name":"ids","in":"query","style":"pipeDelimited","schema":{"type":"array","items":{"type":"integer"}}},
1933                    {"name":"filter","in":"query","style":"deepObject","schema":{"type":"object","properties":{"color":{"type":"string"}},"required":["color"]}}
1934                ],
1935                "responses": {"200": {"description":"ok"}}
1936            }} }
1937        });
1938
1939        let registry = create_registry_from_json(spec_json).unwrap();
1940
1941        let path_params = Map::new();
1942        let mut query = Map::new();
1943        query.insert("tags".into(), json!("alpha beta gamma"));
1944        query.insert("ids".into(), json!("1|2|3"));
1945        query.insert("filter[color]".into(), json!("red"));
1946
1947        assert!(registry
1948            .validate_request_with("/search", "GET", &path_params, &query, None)
1949            .is_ok());
1950    }
1951
1952    #[tokio::test]
1953    async fn test_oneof_anyof_allof_validation() {
1954        let spec_json = json!({
1955            "openapi": "3.0.0",
1956            "info": { "title": "Composite API", "version": "1.0.0" },
1957            "paths": {
1958                "/composite": {
1959                    "post": {
1960                        "requestBody": {
1961                            "content": {
1962                                "application/json": {
1963                                    "schema": {
1964                                        "allOf": [
1965                                            {"type": "object", "required": ["base"], "properties": {"base": {"type": "string"}}}
1966                                        ],
1967                                        "oneOf": [
1968                                            {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"], "not": {"required": ["b"]}},
1969                                            {"type": "object", "properties": {"b": {"type": "integer"}}, "required": ["b"], "not": {"required": ["a"]}}
1970                                        ],
1971                                        "anyOf": [
1972                                            {"type": "object", "properties": {"flag": {"type": "boolean"}}, "required": ["flag"]},
1973                                            {"type": "object", "properties": {"extra": {"type": "string"}}, "required": ["extra"]}
1974                                        ]
1975                                    }
1976                                }
1977                            }
1978                        },
1979                        "responses": {"200": {"description": "ok"}}
1980                    }
1981                }
1982            }
1983        });
1984
1985        let registry = create_registry_from_json(spec_json).unwrap();
1986        // valid: satisfies base via allOf, exactly one of a/b, and at least one of flag/extra
1987        let ok = json!({"base": "x", "a": 1, "flag": true});
1988        assert!(registry
1989            .validate_request_with(
1990                "/composite",
1991                "POST",
1992                &serde_json::Map::new(),
1993                &serde_json::Map::new(),
1994                Some(&ok)
1995            )
1996            .is_ok());
1997
1998        // invalid oneOf: both a and b present
1999        let bad_oneof = json!({"base": "x", "a": 1, "b": 2, "flag": false});
2000        assert!(registry
2001            .validate_request_with(
2002                "/composite",
2003                "POST",
2004                &serde_json::Map::new(),
2005                &serde_json::Map::new(),
2006                Some(&bad_oneof)
2007            )
2008            .is_err());
2009
2010        // invalid anyOf: none of flag/extra present
2011        let bad_anyof = json!({"base": "x", "a": 1});
2012        assert!(registry
2013            .validate_request_with(
2014                "/composite",
2015                "POST",
2016                &serde_json::Map::new(),
2017                &serde_json::Map::new(),
2018                Some(&bad_anyof)
2019            )
2020            .is_err());
2021
2022        // invalid allOf: missing base
2023        let bad_allof = json!({"a": 1, "flag": true});
2024        assert!(registry
2025            .validate_request_with(
2026                "/composite",
2027                "POST",
2028                &serde_json::Map::new(),
2029                &serde_json::Map::new(),
2030                Some(&bad_allof)
2031            )
2032            .is_err());
2033    }
2034
2035    #[tokio::test]
2036    async fn test_overrides_warn_mode_allows_invalid() {
2037        // Spec with a POST route expecting an integer query param
2038        let spec_json = json!({
2039            "openapi": "3.0.0",
2040            "info": { "title": "Overrides API", "version": "1.0.0" },
2041            "paths": {"/things": {"post": {
2042                "parameters": [{"name":"q","in":"query","required":true,"schema":{"type":"integer"}}],
2043                "responses": {"200": {"description":"ok"}}
2044            }}}
2045        });
2046
2047        let spec = OpenApiSpec::from_json(spec_json).unwrap();
2048        let mut overrides = std::collections::HashMap::new();
2049        overrides.insert("POST /things".to_string(), ValidationMode::Warn);
2050        let registry = OpenApiRouteRegistry::new_with_options(
2051            spec,
2052            ValidationOptions {
2053                request_mode: ValidationMode::Enforce,
2054                aggregate_errors: true,
2055                validate_responses: false,
2056                overrides,
2057                admin_skip_prefixes: vec![],
2058                response_template_expand: false,
2059                validation_status: None,
2060            },
2061        );
2062
2063        // Invalid q (missing) should warn, not error
2064        let ok = registry.validate_request_with("/things", "POST", &Map::new(), &Map::new(), None);
2065        assert!(ok.is_ok());
2066    }
2067
2068    #[tokio::test]
2069    async fn test_admin_skip_prefix_short_circuit() {
2070        let spec_json = json!({
2071            "openapi": "3.0.0",
2072            "info": { "title": "Skip API", "version": "1.0.0" },
2073            "paths": {}
2074        });
2075        let spec = OpenApiSpec::from_json(spec_json).unwrap();
2076        let registry = OpenApiRouteRegistry::new_with_options(
2077            spec,
2078            ValidationOptions {
2079                request_mode: ValidationMode::Enforce,
2080                aggregate_errors: true,
2081                validate_responses: false,
2082                overrides: std::collections::HashMap::new(),
2083                admin_skip_prefixes: vec!["/admin".into()],
2084                response_template_expand: false,
2085                validation_status: None,
2086            },
2087        );
2088
2089        // No route exists for this, but skip prefix means it is accepted
2090        let res = registry.validate_request_with_all(
2091            "/admin/__mockforge/health",
2092            "GET",
2093            &Map::new(),
2094            &Map::new(),
2095            &Map::new(),
2096            &Map::new(),
2097            None,
2098        );
2099        assert!(res.is_ok());
2100    }
2101
2102    #[test]
2103    fn test_path_conversion() {
2104        assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users"), "/users");
2105        assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users/{id}"), "/users/{id}");
2106        assert_eq!(
2107            OpenApiRouteRegistry::convert_path_to_axum("/users/{id}/posts/{postId}"),
2108            "/users/{id}/posts/{postId}"
2109        );
2110    }
2111}