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::{HashMap, 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                // Check if this is a multipart request
286                let is_multipart = headers
287                    .get(axum::http::header::CONTENT_TYPE)
288                    .and_then(|v| v.to_str().ok())
289                    .map(|ct| ct.starts_with("multipart/form-data"))
290                    .unwrap_or(false);
291
292                // Extract multipart data if applicable
293                let mut multipart_fields = std::collections::HashMap::new();
294                let mut multipart_files = std::collections::HashMap::new();
295                let mut body_json: Option<Value> = None;
296
297                if is_multipart {
298                    // For multipart requests, extract fields and files
299                    match extract_multipart_from_bytes(&body, &headers).await {
300                        Ok((fields, files)) => {
301                            multipart_fields = fields;
302                            multipart_files = files;
303                            // Also create a JSON representation for validation
304                            let mut body_obj = serde_json::Map::new();
305                            for (k, v) in &multipart_fields {
306                                body_obj.insert(k.clone(), v.clone());
307                            }
308                            if !body_obj.is_empty() {
309                                body_json = Some(Value::Object(body_obj));
310                            }
311                        }
312                        Err(e) => {
313                            tracing::warn!("Failed to parse multipart data: {}", e);
314                        }
315                    }
316                } else {
317                    // Body: try JSON when present
318                    body_json = if !body.is_empty() {
319                        serde_json::from_slice(&body).ok()
320                    } else {
321                        None
322                    };
323                }
324
325                if let Err(e) = validator.validate_request_with_all(
326                    &path_template,
327                    &method,
328                    &path_map,
329                    &query_map,
330                    &header_map,
331                    &cookie_map,
332                    body_json.as_ref(),
333                ) {
334                    // Choose status: prefer options.validation_status, fallback to env, else 400
335                    let status_code = validator.options.validation_status.unwrap_or_else(|| {
336                        std::env::var("MOCKFORGE_VALIDATION_STATUS")
337                            .ok()
338                            .and_then(|s| s.parse::<u16>().ok())
339                            .unwrap_or(400)
340                    });
341
342                    let payload = if status_code == 422 {
343                        // For 422 responses, use enhanced schema validation with detailed errors
344                        // Note: We need to extract parameters from the request context
345                        // For now, using empty maps as placeholders
346                        let empty_params = serde_json::Map::new();
347                        generate_enhanced_422_response(
348                            &validator,
349                            &path_template,
350                            &method,
351                            body_json.as_ref(),
352                            &empty_params, // path_params
353                            &empty_params, // query_params
354                            &empty_params, // header_params
355                            &empty_params, // cookie_params
356                        )
357                    } else {
358                        // For other status codes, use generic error format
359                        let msg = format!("{}", e);
360                        let detail_val = serde_json::from_str::<serde_json::Value>(&msg)
361                            .unwrap_or(serde_json::json!(msg));
362                        json!({
363                            "error": "request validation failed",
364                            "detail": detail_val,
365                            "method": method,
366                            "path": path_template,
367                            "timestamp": Utc::now().to_rfc3339(),
368                        })
369                    };
370
371                    record_validation_error(&payload);
372                    let status = axum::http::StatusCode::from_u16(status_code)
373                        .unwrap_or(axum::http::StatusCode::BAD_REQUEST);
374
375                    // Serialize payload with fallback for serialization errors
376                    let body_bytes = serde_json::to_vec(&payload)
377                        .unwrap_or_else(|_| br#"{"error":"Serialization failed"}"#.to_vec());
378
379                    return axum::http::Response::builder()
380                        .status(status)
381                        .header(axum::http::header::CONTENT_TYPE, "application/json")
382                        .body(axum::body::Body::from(body_bytes))
383                        .expect("Response builder should create valid response with valid headers and body");
384                }
385
386                // Expand tokens in the response if enabled (options or env)
387                let mut final_response = mock_response.clone();
388                let env_expand = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
389                    .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
390                    .unwrap_or(false);
391                let expand = validator.options.response_template_expand || env_expand;
392                if expand {
393                    final_response = core_expand_tokens(&final_response);
394                }
395
396                // Optional response validation
397                if validator.options.validate_responses {
398                    // Find the first 2xx response in the operation
399                    if let Some((status_code, _response)) = operation
400                        .responses
401                        .responses
402                        .iter()
403                        .filter_map(|(status, resp)| match status {
404                            openapiv3::StatusCode::Code(code) if *code >= 200 && *code < 300 => {
405                                resp.as_item().map(|r| ((*code), r))
406                            }
407                            openapiv3::StatusCode::Range(range)
408                                if *range >= 200 && *range < 300 =>
409                            {
410                                resp.as_item().map(|r| (200, r))
411                            }
412                            _ => None,
413                        })
414                        .next()
415                    {
416                        // Basic response validation - check if response is valid JSON
417                        if serde_json::from_value::<serde_json::Value>(final_response.clone())
418                            .is_err()
419                        {
420                            tracing::warn!(
421                                "Response validation failed: invalid JSON for status {}",
422                                status_code
423                            );
424                        }
425                    }
426                }
427
428                // Return the mock response with the correct status code
429                let mut response = Json(final_response).into_response();
430                *response.status_mut() = axum::http::StatusCode::from_u16(selected_status)
431                    .unwrap_or(axum::http::StatusCode::OK);
432                response
433            };
434
435            // Register the handler based on HTTP method
436            router = match route.method.as_str() {
437                "GET" => router.route(&axum_path, get(handler)),
438                "POST" => router.route(&axum_path, post(handler)),
439                "PUT" => router.route(&axum_path, put(handler)),
440                "DELETE" => router.route(&axum_path, delete(handler)),
441                "PATCH" => router.route(&axum_path, patch(handler)),
442                "HEAD" => router.route(&axum_path, head(handler)),
443                "OPTIONS" => router.route(&axum_path, options(handler)),
444                _ => router, // Skip unknown methods
445            };
446        }
447
448        // Add OpenAPI documentation endpoint
449        let spec_json = serde_json::to_value(&self.spec.spec).unwrap_or(Value::Null);
450        router = router.route("/openapi.json", get(move || async move { Json(spec_json) }));
451
452        router
453    }
454
455    /// Build an Axum router from the OpenAPI spec with latency injection support
456    pub fn build_router_with_latency(self, latency_injector: LatencyInjector) -> Router {
457        self.build_router_with_injectors(latency_injector, None)
458    }
459
460    /// Build an Axum router from the OpenAPI spec with both latency and failure injection support
461    pub fn build_router_with_injectors(
462        self,
463        latency_injector: LatencyInjector,
464        failure_injector: Option<crate::FailureInjector>,
465    ) -> Router {
466        self.build_router_with_injectors_and_overrides(
467            latency_injector,
468            failure_injector,
469            None,
470            false,
471        )
472    }
473
474    /// Build an Axum router from the OpenAPI spec with latency, failure injection, and overrides support
475    pub fn build_router_with_injectors_and_overrides(
476        self,
477        latency_injector: LatencyInjector,
478        failure_injector: Option<crate::FailureInjector>,
479        overrides: Option<Overrides>,
480        overrides_enabled: bool,
481    ) -> Router {
482        let mut router = Router::new();
483
484        // Create individual routes for each operation
485        for route in &self.routes {
486            let axum_path = route.axum_path();
487            let operation = route.operation.clone();
488            let method = route.method.clone();
489            let method_str = method.clone();
490            let method_for_router = method_str.clone();
491            let path_template = route.path.clone();
492            let validator = self.clone_for_validation();
493            let route_clone = route.clone();
494            let injector = latency_injector.clone();
495            let failure_injector = failure_injector.clone();
496            let route_overrides = overrides.clone();
497
498            // Extract tags from operation for latency and failure injection
499            let mut operation_tags = operation.tags.clone();
500            if let Some(operation_id) = &operation.operation_id {
501                operation_tags.push(operation_id.clone());
502            }
503
504            // Handler: inject latency, validate path/query/header/cookie/body, then return mock
505            let handler = move |AxumPath(path_params): AxumPath<
506                std::collections::HashMap<String, String>,
507            >,
508                                RawQuery(raw_query): RawQuery,
509                                headers: HeaderMap,
510                                body: axum::body::Bytes| async move {
511                // Check for failure injection first
512                if let Some(ref failure_injector) = failure_injector {
513                    if let Some((status_code, error_message)) =
514                        failure_injector.process_request(&operation_tags)
515                    {
516                        return (
517                            axum::http::StatusCode::from_u16(status_code)
518                                .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR),
519                            axum::Json(serde_json::json!({
520                                "error": error_message,
521                                "injected_failure": true
522                            })),
523                        );
524                    }
525                }
526
527                // Inject latency before processing the request
528                if let Err(e) = injector.inject_latency(&operation_tags).await {
529                    tracing::warn!("Failed to inject latency: {}", e);
530                }
531
532                // Determine scenario from header or environment variable
533                // Header takes precedence over environment variable
534                let scenario = headers
535                    .get("X-Mockforge-Scenario")
536                    .and_then(|v| v.to_str().ok())
537                    .map(|s| s.to_string())
538                    .or_else(|| std::env::var("MOCKFORGE_HTTP_SCENARIO").ok());
539
540                // Admin routes are mounted separately; no validation skip needed here.
541                // Build params maps
542                let mut path_map = Map::new();
543                for (k, v) in path_params {
544                    path_map.insert(k, Value::String(v));
545                }
546
547                // Query
548                let mut query_map = Map::new();
549                if let Some(q) = raw_query {
550                    for (k, v) in url::form_urlencoded::parse(q.as_bytes()) {
551                        query_map.insert(k.to_string(), Value::String(v.to_string()));
552                    }
553                }
554
555                // Headers: only capture those declared on this operation
556                let mut header_map = Map::new();
557                for p_ref in &operation.parameters {
558                    if let Some(openapiv3::Parameter::Header { parameter_data, .. }) =
559                        p_ref.as_item()
560                    {
561                        let name_lc = parameter_data.name.to_ascii_lowercase();
562                        if let Ok(hn) = axum::http::HeaderName::from_bytes(name_lc.as_bytes()) {
563                            if let Some(val) = headers.get(hn) {
564                                if let Ok(s) = val.to_str() {
565                                    header_map.insert(
566                                        parameter_data.name.clone(),
567                                        Value::String(s.to_string()),
568                                    );
569                                }
570                            }
571                        }
572                    }
573                }
574
575                // Cookies: parse Cookie header
576                let mut cookie_map = Map::new();
577                if let Some(val) = headers.get(axum::http::header::COOKIE) {
578                    if let Ok(s) = val.to_str() {
579                        for part in s.split(';') {
580                            let part = part.trim();
581                            if let Some((k, v)) = part.split_once('=') {
582                                cookie_map.insert(k.to_string(), Value::String(v.to_string()));
583                            }
584                        }
585                    }
586                }
587
588                // Check if this is a multipart request
589                let is_multipart = headers
590                    .get(axum::http::header::CONTENT_TYPE)
591                    .and_then(|v| v.to_str().ok())
592                    .map(|ct| ct.starts_with("multipart/form-data"))
593                    .unwrap_or(false);
594
595                // Extract multipart data if applicable
596                let mut multipart_fields = std::collections::HashMap::new();
597                let mut multipart_files = std::collections::HashMap::new();
598                let mut body_json: Option<Value> = None;
599
600                if is_multipart {
601                    // For multipart requests, extract fields and files
602                    match extract_multipart_from_bytes(&body, &headers).await {
603                        Ok((fields, files)) => {
604                            multipart_fields = fields;
605                            multipart_files = files;
606                            // Also create a JSON representation for validation
607                            let mut body_obj = serde_json::Map::new();
608                            for (k, v) in &multipart_fields {
609                                body_obj.insert(k.clone(), v.clone());
610                            }
611                            if !body_obj.is_empty() {
612                                body_json = Some(Value::Object(body_obj));
613                            }
614                        }
615                        Err(e) => {
616                            tracing::warn!("Failed to parse multipart data: {}", e);
617                        }
618                    }
619                } else {
620                    // Body: try JSON when present
621                    body_json = if !body.is_empty() {
622                        serde_json::from_slice(&body).ok()
623                    } else {
624                        None
625                    };
626                }
627
628                if let Err(e) = validator.validate_request_with_all(
629                    &path_template,
630                    &method_str,
631                    &path_map,
632                    &query_map,
633                    &header_map,
634                    &cookie_map,
635                    body_json.as_ref(),
636                ) {
637                    let msg = format!("{}", e);
638                    let detail_val = serde_json::from_str::<serde_json::Value>(&msg)
639                        .unwrap_or(serde_json::json!(msg));
640                    let payload = serde_json::json!({
641                        "error": "request validation failed",
642                        "detail": detail_val,
643                        "method": method_str,
644                        "path": path_template,
645                        "timestamp": Utc::now().to_rfc3339(),
646                    });
647                    record_validation_error(&payload);
648                    // Choose status: prefer options.validation_status, fallback to env, else 400
649                    let status_code = validator.options.validation_status.unwrap_or_else(|| {
650                        std::env::var("MOCKFORGE_VALIDATION_STATUS")
651                            .ok()
652                            .and_then(|s| s.parse::<u16>().ok())
653                            .unwrap_or(400)
654                    });
655                    return (
656                        axum::http::StatusCode::from_u16(status_code)
657                            .unwrap_or(axum::http::StatusCode::BAD_REQUEST),
658                        Json(payload),
659                    );
660                }
661
662                // Generate mock response with scenario support
663                let (selected_status, mock_response) =
664                    route_clone.mock_response_with_status_and_scenario(scenario.as_deref());
665
666                // Expand templating tokens in response if enabled (options or env)
667                let mut response = mock_response.clone();
668                let env_expand = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
669                    .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
670                    .unwrap_or(false);
671                let expand = validator.options.response_template_expand || env_expand;
672                if expand {
673                    response = core_expand_tokens(&response);
674                }
675
676                // Apply overrides if provided and enabled
677                if let Some(ref overrides) = route_overrides {
678                    if overrides_enabled {
679                        // Extract tags from operation for override matching
680                        let operation_tags =
681                            operation.operation_id.clone().map(|id| vec![id]).unwrap_or_default();
682                        overrides.apply(
683                            &operation.operation_id.unwrap_or_default(),
684                            &operation_tags,
685                            &path_template,
686                            &mut response,
687                        );
688                    }
689                }
690
691                // Return the mock response
692                (
693                    axum::http::StatusCode::from_u16(selected_status)
694                        .unwrap_or(axum::http::StatusCode::OK),
695                    Json(response),
696                )
697            };
698
699            // Add route to router based on HTTP method
700            router = match method_for_router.as_str() {
701                "GET" => router.route(&axum_path, get(handler)),
702                "POST" => router.route(&axum_path, post(handler)),
703                "PUT" => router.route(&axum_path, put(handler)),
704                "PATCH" => router.route(&axum_path, patch(handler)),
705                "DELETE" => router.route(&axum_path, delete(handler)),
706                "HEAD" => router.route(&axum_path, head(handler)),
707                "OPTIONS" => router.route(&axum_path, options(handler)),
708                _ => router.route(&axum_path, get(handler)), // Default to GET for unknown methods
709            };
710        }
711
712        // Add OpenAPI documentation endpoint
713        let spec_json = serde_json::to_value(&self.spec.spec).unwrap_or(Value::Null);
714        router = router.route("/openapi.json", get(move || async move { Json(spec_json) }));
715
716        router
717    }
718
719    /// Get route by path and method
720    pub fn get_route(&self, path: &str, method: &str) -> Option<&OpenApiRoute> {
721        self.routes.iter().find(|route| route.path == path && route.method == method)
722    }
723
724    /// Get all routes for a specific path
725    pub fn get_routes_for_path(&self, path: &str) -> Vec<&OpenApiRoute> {
726        self.routes.iter().filter(|route| route.path == path).collect()
727    }
728
729    /// Validate request against OpenAPI spec (legacy body-only)
730    pub fn validate_request(&self, path: &str, method: &str, body: Option<&Value>) -> Result<()> {
731        self.validate_request_with(path, method, &Map::new(), &Map::new(), body)
732    }
733
734    /// Validate request against OpenAPI spec with path/query params
735    pub fn validate_request_with(
736        &self,
737        path: &str,
738        method: &str,
739        path_params: &Map<String, Value>,
740        query_params: &Map<String, Value>,
741        body: Option<&Value>,
742    ) -> Result<()> {
743        self.validate_request_with_all(
744            path,
745            method,
746            path_params,
747            query_params,
748            &Map::new(),
749            &Map::new(),
750            body,
751        )
752    }
753
754    /// Validate request against OpenAPI spec with path/query/header/cookie params
755    #[allow(clippy::too_many_arguments)]
756    pub fn validate_request_with_all(
757        &self,
758        path: &str,
759        method: &str,
760        path_params: &Map<String, Value>,
761        query_params: &Map<String, Value>,
762        header_params: &Map<String, Value>,
763        cookie_params: &Map<String, Value>,
764        body: Option<&Value>,
765    ) -> Result<()> {
766        // Skip validation for any configured admin prefixes
767        for pref in &self.options.admin_skip_prefixes {
768            if !pref.is_empty() && path.starts_with(pref) {
769                return Ok(());
770            }
771        }
772        // Runtime env overrides
773        let env_mode = std::env::var("MOCKFORGE_REQUEST_VALIDATION").ok().map(|v| {
774            match v.to_ascii_lowercase().as_str() {
775                "off" | "disable" | "disabled" => ValidationMode::Disabled,
776                "warn" | "warning" => ValidationMode::Warn,
777                _ => ValidationMode::Enforce,
778            }
779        });
780        let aggregate = std::env::var("MOCKFORGE_AGGREGATE_ERRORS")
781            .ok()
782            .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
783            .unwrap_or(self.options.aggregate_errors);
784        // Per-route runtime overrides via JSON env var
785        let env_overrides: Option<serde_json::Map<String, serde_json::Value>> =
786            std::env::var("MOCKFORGE_VALIDATION_OVERRIDES_JSON")
787                .ok()
788                .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
789                .and_then(|v| v.as_object().cloned());
790        // Response validation is handled in HTTP layer now
791        let mut effective_mode = env_mode.unwrap_or(self.options.request_mode.clone());
792        // Apply runtime overrides first if present
793        if let Some(map) = &env_overrides {
794            if let Some(v) = map.get(&format!("{} {}", method, path)) {
795                if let Some(m) = v.as_str() {
796                    effective_mode = match m {
797                        "off" => ValidationMode::Disabled,
798                        "warn" => ValidationMode::Warn,
799                        _ => ValidationMode::Enforce,
800                    };
801                }
802            }
803        }
804        // Then static options overrides
805        if let Some(override_mode) = self.options.overrides.get(&format!("{} {}", method, path)) {
806            effective_mode = override_mode.clone();
807        }
808        if matches!(effective_mode, ValidationMode::Disabled) {
809            return Ok(());
810        }
811        if let Some(route) = self.get_route(path, method) {
812            if matches!(effective_mode, ValidationMode::Disabled) {
813                return Ok(());
814            }
815            let mut errors: Vec<String> = Vec::new();
816            let mut details: Vec<serde_json::Value> = Vec::new();
817            // Validate request body if required
818            if let Some(schema) = &route.operation.request_body {
819                if let Some(value) = body {
820                    // First resolve the request body reference if it's a reference
821                    let request_body = match schema {
822                        openapiv3::ReferenceOr::Item(rb) => Some(rb),
823                        openapiv3::ReferenceOr::Reference { reference } => {
824                            // Try to resolve request body reference through spec
825                            self.spec
826                                .spec
827                                .components
828                                .as_ref()
829                                .and_then(|components| {
830                                    components.request_bodies.get(
831                                        reference.trim_start_matches("#/components/requestBodies/"),
832                                    )
833                                })
834                                .and_then(|rb_ref| rb_ref.as_item())
835                        }
836                    };
837
838                    if let Some(rb) = request_body {
839                        if let Some(content) = rb.content.get("application/json") {
840                            if let Some(schema_ref) = &content.schema {
841                                // Resolve schema reference and validate
842                                match schema_ref {
843                                    openapiv3::ReferenceOr::Item(schema) => {
844                                        // Direct schema - validate immediately
845                                        if let Err(validation_error) =
846                                            OpenApiSchema::new(schema.clone()).validate(value)
847                                        {
848                                            let error_msg = validation_error.to_string();
849                                            errors.push(format!(
850                                                "body validation failed: {}",
851                                                error_msg
852                                            ));
853                                            if aggregate {
854                                                details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
855                                            }
856                                        }
857                                    }
858                                    openapiv3::ReferenceOr::Reference { reference } => {
859                                        // Referenced schema - resolve and validate
860                                        if let Some(resolved_schema_ref) =
861                                            self.spec.get_schema(reference)
862                                        {
863                                            if let Err(validation_error) = OpenApiSchema::new(
864                                                resolved_schema_ref.schema.clone(),
865                                            )
866                                            .validate(value)
867                                            {
868                                                let error_msg = validation_error.to_string();
869                                                errors.push(format!(
870                                                    "body validation failed: {}",
871                                                    error_msg
872                                                ));
873                                                if aggregate {
874                                                    details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
875                                                }
876                                            }
877                                        } else {
878                                            // Schema reference couldn't be resolved
879                                            errors.push(format!("body validation failed: could not resolve schema reference {}", reference));
880                                            if aggregate {
881                                                details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve schema reference"}));
882                                            }
883                                        }
884                                    }
885                                }
886                            }
887                        }
888                    } else {
889                        // Request body reference couldn't be resolved or no application/json content
890                        errors.push("body validation failed: could not resolve request body or no application/json content".to_string());
891                        if aggregate {
892                            details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve request body reference"}));
893                        }
894                    }
895                } else {
896                    errors.push("body: Request body is required but not provided".to_string());
897                    details.push(serde_json::json!({"path":"body","code":"required","message":"Request body is required"}));
898                }
899            } else if body.is_some() {
900                // No body expected but provided — not an error by default, but log it
901                tracing::debug!("Body provided for operation without requestBody; accepting");
902            }
903
904            // Validate path/query parameters
905            for p_ref in &route.operation.parameters {
906                if let Some(p) = p_ref.as_item() {
907                    match p {
908                        openapiv3::Parameter::Path { parameter_data, .. } => {
909                            validate_parameter(
910                                parameter_data,
911                                path_params,
912                                "path",
913                                aggregate,
914                                &mut errors,
915                                &mut details,
916                            );
917                        }
918                        openapiv3::Parameter::Query {
919                            parameter_data,
920                            style,
921                            ..
922                        } => {
923                            // For query deepObject, reconstruct value from key-likes: name[prop]
924                            let deep_value = None; // Simplified for now
925                            let style_str = match style {
926                                openapiv3::QueryStyle::Form => Some("form"),
927                                openapiv3::QueryStyle::SpaceDelimited => Some("spaceDelimited"),
928                                openapiv3::QueryStyle::PipeDelimited => Some("pipeDelimited"),
929                                openapiv3::QueryStyle::DeepObject => Some("deepObject"),
930                            };
931                            validate_parameter_with_deep_object(
932                                parameter_data,
933                                query_params,
934                                "query",
935                                deep_value,
936                                style_str,
937                                aggregate,
938                                &mut errors,
939                                &mut details,
940                            );
941                        }
942                        openapiv3::Parameter::Header { parameter_data, .. } => {
943                            validate_parameter(
944                                parameter_data,
945                                header_params,
946                                "header",
947                                aggregate,
948                                &mut errors,
949                                &mut details,
950                            );
951                        }
952                        openapiv3::Parameter::Cookie { parameter_data, .. } => {
953                            validate_parameter(
954                                parameter_data,
955                                cookie_params,
956                                "cookie",
957                                aggregate,
958                                &mut errors,
959                                &mut details,
960                            );
961                        }
962                    }
963                }
964            }
965            if errors.is_empty() {
966                return Ok(());
967            }
968            match effective_mode {
969                ValidationMode::Disabled => Ok(()),
970                ValidationMode::Warn => {
971                    tracing::warn!("Request validation warnings: {:?}", errors);
972                    Ok(())
973                }
974                ValidationMode::Enforce => Err(Error::validation(
975                    serde_json::json!({"errors": errors, "details": details}).to_string(),
976                )),
977            }
978        } else {
979            Err(Error::generic(format!("Route {} {} not found in OpenAPI spec", method, path)))
980        }
981    }
982
983    // Legacy helper removed (mock + status selection happens in handler via route.mock_response_with_status)
984
985    /// Get all paths defined in the spec
986    pub fn paths(&self) -> Vec<String> {
987        let mut paths: Vec<String> = self.routes.iter().map(|route| route.path.clone()).collect();
988        paths.sort();
989        paths.dedup();
990        paths
991    }
992
993    /// Get all HTTP methods supported
994    pub fn methods(&self) -> Vec<String> {
995        let mut methods: Vec<String> =
996            self.routes.iter().map(|route| route.method.clone()).collect();
997        methods.sort();
998        methods.dedup();
999        methods
1000    }
1001
1002    /// Get operation details for a route
1003    pub fn get_operation(&self, path: &str, method: &str) -> Option<OpenApiOperation> {
1004        self.get_route(path, method).map(|route| {
1005            OpenApiOperation::from_operation(
1006                &route.method,
1007                route.path.clone(),
1008                &route.operation,
1009                &self.spec,
1010            )
1011        })
1012    }
1013
1014    /// Extract path parameters from a request path by matching against known routes
1015    pub fn extract_path_parameters(
1016        &self,
1017        path: &str,
1018        method: &str,
1019    ) -> std::collections::HashMap<String, String> {
1020        for route in &self.routes {
1021            if route.method != method {
1022                continue;
1023            }
1024
1025            if let Some(params) = self.match_path_to_route(path, &route.path) {
1026                return params;
1027            }
1028        }
1029        std::collections::HashMap::new()
1030    }
1031
1032    /// Match a request path against a route pattern and extract parameters
1033    fn match_path_to_route(
1034        &self,
1035        request_path: &str,
1036        route_pattern: &str,
1037    ) -> Option<std::collections::HashMap<String, String>> {
1038        let mut params = std::collections::HashMap::new();
1039
1040        // Split both paths into segments
1041        let request_segments: Vec<&str> = request_path.trim_start_matches('/').split('/').collect();
1042        let pattern_segments: Vec<&str> =
1043            route_pattern.trim_start_matches('/').split('/').collect();
1044
1045        if request_segments.len() != pattern_segments.len() {
1046            return None;
1047        }
1048
1049        for (req_seg, pat_seg) in request_segments.iter().zip(pattern_segments.iter()) {
1050            if pat_seg.starts_with('{') && pat_seg.ends_with('}') {
1051                // This is a parameter
1052                let param_name = &pat_seg[1..pat_seg.len() - 1];
1053                params.insert(param_name.to_string(), req_seg.to_string());
1054            } else if req_seg != pat_seg {
1055                // Static segment doesn't match
1056                return None;
1057            }
1058        }
1059
1060        Some(params)
1061    }
1062
1063    /// Convert OpenAPI path to Axum-compatible path
1064    /// This is a utility function for converting path parameters from {param} to :param format
1065    pub fn convert_path_to_axum(openapi_path: &str) -> String {
1066        // Axum v0.7+ uses {param} format, same as OpenAPI
1067        openapi_path.to_string()
1068    }
1069
1070    /// Build router with AI generator support
1071    pub fn build_router_with_ai(
1072        &self,
1073        ai_generator: Option<std::sync::Arc<dyn AiGenerator + Send + Sync>>,
1074    ) -> Router {
1075        use axum::routing::{delete, get, patch, post, put};
1076
1077        let mut router = Router::new();
1078        tracing::debug!("Building router with AI support from {} routes", self.routes.len());
1079
1080        for route in &self.routes {
1081            tracing::debug!("Adding AI-enabled route: {} {}", route.method, route.path);
1082
1083            let route_clone = route.clone();
1084            let ai_generator_clone = ai_generator.clone();
1085
1086            // Create async handler that extracts request data and builds context
1087            let handler = move |headers: HeaderMap, body: Option<Json<Value>>| {
1088                let route = route_clone.clone();
1089                let ai_generator = ai_generator_clone.clone();
1090
1091                async move {
1092                    tracing::debug!(
1093                        "Handling AI request for route: {} {}",
1094                        route.method,
1095                        route.path
1096                    );
1097
1098                    // Build request context
1099                    let mut context = RequestContext::new(route.method.clone(), route.path.clone());
1100
1101                    // Extract headers
1102                    context.headers = headers
1103                        .iter()
1104                        .map(|(k, v)| {
1105                            (k.to_string(), Value::String(v.to_str().unwrap_or("").to_string()))
1106                        })
1107                        .collect();
1108
1109                    // Extract body if present
1110                    context.body = body.map(|Json(b)| b);
1111
1112                    // Generate AI response if AI generator is available and route has AI config
1113                    let (status, response) = if let (Some(generator), Some(_ai_config)) =
1114                        (ai_generator, &route.ai_config)
1115                    {
1116                        route
1117                            .mock_response_with_status_async(&context, Some(generator.as_ref()))
1118                            .await
1119                    } else {
1120                        // No AI support, use static response
1121                        route.mock_response_with_status()
1122                    };
1123
1124                    (
1125                        axum::http::StatusCode::from_u16(status)
1126                            .unwrap_or(axum::http::StatusCode::OK),
1127                        axum::response::Json(response),
1128                    )
1129                }
1130            };
1131
1132            match route.method.as_str() {
1133                "GET" => {
1134                    router = router.route(&route.path, get(handler));
1135                }
1136                "POST" => {
1137                    router = router.route(&route.path, post(handler));
1138                }
1139                "PUT" => {
1140                    router = router.route(&route.path, put(handler));
1141                }
1142                "DELETE" => {
1143                    router = router.route(&route.path, delete(handler));
1144                }
1145                "PATCH" => {
1146                    router = router.route(&route.path, patch(handler));
1147                }
1148                _ => tracing::warn!("Unsupported HTTP method for AI: {}", route.method),
1149            }
1150        }
1151
1152        router
1153    }
1154
1155    /// Build router with MockAI (Behavioral Mock Intelligence) support
1156    ///
1157    /// This method integrates MockAI for intelligent, context-aware response generation,
1158    /// mutation detection, validation error generation, and pagination intelligence.
1159    ///
1160    /// # Arguments
1161    /// * `mockai` - Optional MockAI instance for intelligent behavior
1162    ///
1163    /// # Returns
1164    /// Axum router with MockAI-powered response generation
1165    pub fn build_router_with_mockai(
1166        &self,
1167        mockai: Option<std::sync::Arc<tokio::sync::RwLock<crate::intelligent_behavior::MockAI>>>,
1168    ) -> Router {
1169        use crate::intelligent_behavior::{Request as MockAIRequest, Response as MockAIResponse};
1170        use axum::extract::Query;
1171        use axum::routing::{delete, get, patch, post, put};
1172
1173        let mut router = Router::new();
1174        tracing::debug!("Building router with MockAI support from {} routes", self.routes.len());
1175
1176        for route in &self.routes {
1177            tracing::debug!("Adding MockAI-enabled route: {} {}", route.method, route.path);
1178
1179            let route_clone = route.clone();
1180            let mockai_clone = mockai.clone();
1181
1182            // Create async handler that processes requests through MockAI
1183            // Query params are extracted via Query extractor with HashMap
1184            // Note: Using Query<HashMap<String, String>> wrapped in Option to handle missing query params
1185            let handler = move |query: axum::extract::Query<HashMap<String, String>>,
1186                                headers: HeaderMap,
1187                                body: Option<Json<Value>>| {
1188                let route = route_clone.clone();
1189                let mockai = mockai_clone.clone();
1190
1191                async move {
1192                    tracing::debug!(
1193                        "Handling MockAI request for route: {} {}",
1194                        route.method,
1195                        route.path
1196                    );
1197
1198                    // Query parameters are already parsed by Query extractor
1199                    let mockai_query = query.0;
1200
1201                    // If MockAI is enabled, use it to process the request
1202                    if let Some(mockai_arc) = mockai {
1203                        let mockai_guard = mockai_arc.read().await;
1204
1205                        // Build MockAI request
1206                        let mut mockai_headers = HashMap::new();
1207                        for (k, v) in headers.iter() {
1208                            mockai_headers
1209                                .insert(k.to_string(), v.to_str().unwrap_or("").to_string());
1210                        }
1211
1212                        let mockai_request = MockAIRequest {
1213                            method: route.method.clone(),
1214                            path: route.path.clone(),
1215                            body: body.as_ref().map(|Json(b)| b.clone()),
1216                            query_params: mockai_query,
1217                            headers: mockai_headers,
1218                        };
1219
1220                        // Process request through MockAI
1221                        match mockai_guard.process_request(&mockai_request).await {
1222                            Ok(mockai_response) => {
1223                                tracing::debug!(
1224                                    "MockAI generated response with status: {}",
1225                                    mockai_response.status_code
1226                                );
1227                                return (
1228                                    axum::http::StatusCode::from_u16(mockai_response.status_code)
1229                                        .unwrap_or(axum::http::StatusCode::OK),
1230                                    axum::response::Json(mockai_response.body),
1231                                );
1232                            }
1233                            Err(e) => {
1234                                tracing::warn!(
1235                                    "MockAI processing failed for {} {}: {}, falling back to standard response",
1236                                    route.method,
1237                                    route.path,
1238                                    e
1239                                );
1240                                // Fall through to standard response generation
1241                            }
1242                        }
1243                    }
1244
1245                    // Fallback to standard response generation
1246                    let (status, response) = route.mock_response_with_status();
1247                    (
1248                        axum::http::StatusCode::from_u16(status)
1249                            .unwrap_or(axum::http::StatusCode::OK),
1250                        axum::response::Json(response),
1251                    )
1252                }
1253            };
1254
1255            match route.method.as_str() {
1256                "GET" => {
1257                    router = router.route(&route.path, get(handler));
1258                }
1259                "POST" => {
1260                    router = router.route(&route.path, post(handler));
1261                }
1262                "PUT" => {
1263                    router = router.route(&route.path, put(handler));
1264                }
1265                "DELETE" => {
1266                    router = router.route(&route.path, delete(handler));
1267                }
1268                "PATCH" => {
1269                    router = router.route(&route.path, patch(handler));
1270                }
1271                _ => tracing::warn!("Unsupported HTTP method for MockAI: {}", route.method),
1272            }
1273        }
1274
1275        router
1276    }
1277}
1278
1279// Note: templating helpers are now in core::templating (shared across modules)
1280
1281/// Extract multipart form data from request body bytes
1282/// Returns (form_fields, file_paths) where file_paths maps field names to stored file paths
1283async fn extract_multipart_from_bytes(
1284    body: &axum::body::Bytes,
1285    headers: &HeaderMap,
1286) -> Result<(
1287    std::collections::HashMap<String, Value>,
1288    std::collections::HashMap<String, String>,
1289)> {
1290    // Get boundary from Content-Type header
1291    let boundary = headers
1292        .get(axum::http::header::CONTENT_TYPE)
1293        .and_then(|v| v.to_str().ok())
1294        .and_then(|ct| {
1295            ct.split(';').find_map(|part| {
1296                let part = part.trim();
1297                if part.starts_with("boundary=") {
1298                    Some(part.strip_prefix("boundary=").unwrap_or("").trim_matches('"'))
1299                } else {
1300                    None
1301                }
1302            })
1303        })
1304        .ok_or_else(|| Error::generic("Missing boundary in Content-Type header"))?;
1305
1306    let mut fields = std::collections::HashMap::new();
1307    let mut files = std::collections::HashMap::new();
1308
1309    // Parse multipart data using bytes directly (not string conversion)
1310    // Multipart format: --boundary\r\n...\r\n--boundary\r\n...\r\n--boundary--\r\n
1311    let boundary_prefix = format!("--{}", boundary).into_bytes();
1312    let boundary_line = format!("\r\n--{}\r\n", boundary).into_bytes();
1313    let end_boundary = format!("\r\n--{}--\r\n", boundary).into_bytes();
1314
1315    // Find all boundary positions
1316    let mut pos = 0;
1317    let mut parts = Vec::new();
1318
1319    // Skip initial boundary if present
1320    if body.starts_with(&boundary_prefix) {
1321        if let Some(first_crlf) = body.iter().position(|&b| b == b'\r') {
1322            pos = first_crlf + 2; // Skip --boundary\r\n
1323        }
1324    }
1325
1326    // Find all middle boundaries
1327    while let Some(boundary_pos) = body[pos..]
1328        .windows(boundary_line.len())
1329        .position(|window| window == boundary_line.as_slice())
1330    {
1331        let actual_pos = pos + boundary_pos;
1332        if actual_pos > pos {
1333            parts.push((pos, actual_pos));
1334        }
1335        pos = actual_pos + boundary_line.len();
1336    }
1337
1338    // Find final boundary
1339    if let Some(end_pos) = body[pos..]
1340        .windows(end_boundary.len())
1341        .position(|window| window == end_boundary.as_slice())
1342    {
1343        let actual_end = pos + end_pos;
1344        if actual_end > pos {
1345            parts.push((pos, actual_end));
1346        }
1347    } else if pos < body.len() {
1348        // No final boundary found, treat rest as last part
1349        parts.push((pos, body.len()));
1350    }
1351
1352    // Process each part
1353    for (start, end) in parts {
1354        let part_data = &body[start..end];
1355
1356        // Find header/body separator (CRLF CRLF)
1357        let separator = b"\r\n\r\n";
1358        if let Some(sep_pos) =
1359            part_data.windows(separator.len()).position(|window| window == separator)
1360        {
1361            let header_bytes = &part_data[..sep_pos];
1362            let body_start = sep_pos + separator.len();
1363            let body_data = &part_data[body_start..];
1364
1365            // Parse headers (assuming UTF-8)
1366            let header_str = String::from_utf8_lossy(header_bytes);
1367            let mut field_name = None;
1368            let mut filename = None;
1369
1370            for header_line in header_str.lines() {
1371                if header_line.starts_with("Content-Disposition:") {
1372                    // Extract field name
1373                    if let Some(name_start) = header_line.find("name=\"") {
1374                        let name_start = name_start + 6;
1375                        if let Some(name_end) = header_line[name_start..].find('"') {
1376                            field_name =
1377                                Some(header_line[name_start..name_start + name_end].to_string());
1378                        }
1379                    }
1380
1381                    // Extract filename if present
1382                    if let Some(file_start) = header_line.find("filename=\"") {
1383                        let file_start = file_start + 10;
1384                        if let Some(file_end) = header_line[file_start..].find('"') {
1385                            filename =
1386                                Some(header_line[file_start..file_start + file_end].to_string());
1387                        }
1388                    }
1389                }
1390            }
1391
1392            if let Some(name) = field_name {
1393                if let Some(file) = filename {
1394                    // This is a file upload - store to temp directory
1395                    let temp_dir = std::env::temp_dir().join("mockforge-uploads");
1396                    std::fs::create_dir_all(&temp_dir).map_err(|e| {
1397                        Error::generic(format!("Failed to create temp directory: {}", e))
1398                    })?;
1399
1400                    let file_path = temp_dir.join(format!("{}_{}", uuid::Uuid::new_v4(), file));
1401                    std::fs::write(&file_path, body_data)
1402                        .map_err(|e| Error::generic(format!("Failed to write file: {}", e)))?;
1403
1404                    let file_path_str = file_path.to_string_lossy().to_string();
1405                    files.insert(name.clone(), file_path_str.clone());
1406                    fields.insert(name, Value::String(file_path_str));
1407                } else {
1408                    // This is a regular form field - try to parse as UTF-8 string
1409                    // Trim trailing CRLF
1410                    let body_str = body_data
1411                        .strip_suffix(b"\r\n")
1412                        .or_else(|| body_data.strip_suffix(b"\n"))
1413                        .unwrap_or(body_data);
1414
1415                    if let Ok(field_value) = String::from_utf8(body_str.to_vec()) {
1416                        fields.insert(name, Value::String(field_value.trim().to_string()));
1417                    } else {
1418                        // Non-UTF-8 field value - store as base64 encoded string
1419                        use base64::{engine::general_purpose, Engine as _};
1420                        fields.insert(
1421                            name,
1422                            Value::String(general_purpose::STANDARD.encode(body_str)),
1423                        );
1424                    }
1425                }
1426            }
1427        }
1428    }
1429
1430    Ok((fields, files))
1431}
1432
1433static LAST_ERRORS: Lazy<Mutex<VecDeque<serde_json::Value>>> =
1434    Lazy::new(|| Mutex::new(VecDeque::with_capacity(20)));
1435
1436/// Record last validation error for Admin UI inspection
1437pub fn record_validation_error(v: &serde_json::Value) {
1438    if let Ok(mut q) = LAST_ERRORS.lock() {
1439        if q.len() >= 20 {
1440            q.pop_front();
1441        }
1442        q.push_back(v.clone());
1443    }
1444    // If mutex is poisoned, we silently fail - validation errors are informational only
1445}
1446
1447/// Get most recent validation error
1448pub fn get_last_validation_error() -> Option<serde_json::Value> {
1449    LAST_ERRORS.lock().ok()?.back().cloned()
1450}
1451
1452/// Get recent validation errors (most recent last)
1453pub fn get_validation_errors() -> Vec<serde_json::Value> {
1454    LAST_ERRORS.lock().map(|q| q.iter().cloned().collect()).unwrap_or_default()
1455}
1456
1457/// Coerce a parameter `value` into the expected JSON type per `schema` where reasonable.
1458/// Applies only to param contexts (not request bodies). Conservative conversions:
1459/// - integer/number: parse from string; arrays: split comma-separated strings and coerce items
1460/// - boolean: parse true/false (case-insensitive) from string
1461fn coerce_value_for_schema(value: &Value, schema: &openapiv3::Schema) -> Value {
1462    // Basic coercion: try to parse strings as appropriate types
1463    match value {
1464        Value::String(s) => {
1465            // Check if schema expects an array and we have a comma-separated string
1466            if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
1467                &schema.schema_kind
1468            {
1469                if s.contains(',') {
1470                    // Split comma-separated string into array
1471                    let parts: Vec<&str> = s.split(',').map(|s| s.trim()).collect();
1472                    let mut array_values = Vec::new();
1473
1474                    for part in parts {
1475                        // Coerce each part based on array item type
1476                        if let Some(items_schema) = &array_type.items {
1477                            if let Some(items_schema_obj) = items_schema.as_item() {
1478                                let part_value = Value::String(part.to_string());
1479                                let coerced_part =
1480                                    coerce_value_for_schema(&part_value, items_schema_obj);
1481                                array_values.push(coerced_part);
1482                            } else {
1483                                // If items schema is a reference or not available, keep as string
1484                                array_values.push(Value::String(part.to_string()));
1485                            }
1486                        } else {
1487                            // No items schema defined, keep as string
1488                            array_values.push(Value::String(part.to_string()));
1489                        }
1490                    }
1491                    return Value::Array(array_values);
1492                }
1493            }
1494
1495            // Only coerce if the schema expects a different type
1496            match &schema.schema_kind {
1497                openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
1498                    // Schema expects string, keep as string
1499                    value.clone()
1500                }
1501                openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => {
1502                    // Schema expects number, try to parse
1503                    if let Ok(n) = s.parse::<f64>() {
1504                        if let Some(num) = serde_json::Number::from_f64(n) {
1505                            return Value::Number(num);
1506                        }
1507                    }
1508                    value.clone()
1509                }
1510                openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => {
1511                    // Schema expects integer, try to parse
1512                    if let Ok(n) = s.parse::<i64>() {
1513                        if let Some(num) = serde_json::Number::from_f64(n as f64) {
1514                            return Value::Number(num);
1515                        }
1516                    }
1517                    value.clone()
1518                }
1519                openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => {
1520                    // Schema expects boolean, try to parse
1521                    match s.to_lowercase().as_str() {
1522                        "true" | "1" | "yes" | "on" => Value::Bool(true),
1523                        "false" | "0" | "no" | "off" => Value::Bool(false),
1524                        _ => value.clone(),
1525                    }
1526                }
1527                _ => {
1528                    // Unknown schema type, keep as string
1529                    value.clone()
1530                }
1531            }
1532        }
1533        _ => value.clone(),
1534    }
1535}
1536
1537/// Apply style-aware coercion for query params
1538fn coerce_by_style(value: &Value, schema: &openapiv3::Schema, style: Option<&str>) -> Value {
1539    // Style-aware coercion for query parameters
1540    match value {
1541        Value::String(s) => {
1542            // Check if schema expects an array and we have a delimited string
1543            if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
1544                &schema.schema_kind
1545            {
1546                let delimiter = match style {
1547                    Some("spaceDelimited") => " ",
1548                    Some("pipeDelimited") => "|",
1549                    Some("form") | None => ",", // Default to form style (comma-separated)
1550                    _ => ",",                   // Fallback to comma
1551                };
1552
1553                if s.contains(delimiter) {
1554                    // Split delimited string into array
1555                    let parts: Vec<&str> = s.split(delimiter).map(|s| s.trim()).collect();
1556                    let mut array_values = Vec::new();
1557
1558                    for part in parts {
1559                        // Coerce each part based on array item type
1560                        if let Some(items_schema) = &array_type.items {
1561                            if let Some(items_schema_obj) = items_schema.as_item() {
1562                                let part_value = Value::String(part.to_string());
1563                                let coerced_part =
1564                                    coerce_by_style(&part_value, items_schema_obj, style);
1565                                array_values.push(coerced_part);
1566                            } else {
1567                                // If items schema is a reference or not available, keep as string
1568                                array_values.push(Value::String(part.to_string()));
1569                            }
1570                        } else {
1571                            // No items schema defined, keep as string
1572                            array_values.push(Value::String(part.to_string()));
1573                        }
1574                    }
1575                    return Value::Array(array_values);
1576                }
1577            }
1578
1579            // Try to parse as number first
1580            if let Ok(n) = s.parse::<f64>() {
1581                if let Some(num) = serde_json::Number::from_f64(n) {
1582                    return Value::Number(num);
1583                }
1584            }
1585            // Try to parse as boolean
1586            match s.to_lowercase().as_str() {
1587                "true" | "1" | "yes" | "on" => return Value::Bool(true),
1588                "false" | "0" | "no" | "off" => return Value::Bool(false),
1589                _ => {}
1590            }
1591            // Keep as string
1592            value.clone()
1593        }
1594        _ => value.clone(),
1595    }
1596}
1597
1598/// Build a deepObject from query params like `name[prop]=val`
1599fn build_deep_object(name: &str, params: &Map<String, Value>) -> Option<Value> {
1600    let prefix = format!("{}[", name);
1601    let mut obj = Map::new();
1602    for (k, v) in params.iter() {
1603        if let Some(rest) = k.strip_prefix(&prefix) {
1604            if let Some(key) = rest.strip_suffix(']') {
1605                obj.insert(key.to_string(), v.clone());
1606            }
1607        }
1608    }
1609    if obj.is_empty() {
1610        None
1611    } else {
1612        Some(Value::Object(obj))
1613    }
1614}
1615
1616// Import the enhanced schema diff functionality
1617// use crate::schema_diff::{validation_diff, to_enhanced_422_json, ValidationError}; // Not currently used
1618
1619/// Generate an enhanced 422 response with detailed schema validation errors
1620/// This function provides comprehensive error information using the new schema diff utility
1621#[allow(clippy::too_many_arguments)]
1622fn generate_enhanced_422_response(
1623    validator: &OpenApiRouteRegistry,
1624    path_template: &str,
1625    method: &str,
1626    body: Option<&Value>,
1627    path_params: &serde_json::Map<String, Value>,
1628    query_params: &serde_json::Map<String, Value>,
1629    header_params: &serde_json::Map<String, Value>,
1630    cookie_params: &serde_json::Map<String, Value>,
1631) -> Value {
1632    let mut field_errors = Vec::new();
1633
1634    // Extract schema validation details if we have a route
1635    if let Some(route) = validator.get_route(path_template, method) {
1636        // Validate request body with detailed error collection
1637        if let Some(schema) = &route.operation.request_body {
1638            if let Some(value) = body {
1639                if let Some(content) =
1640                    schema.as_item().and_then(|rb| rb.content.get("application/json"))
1641                {
1642                    if let Some(_schema_ref) = &content.schema {
1643                        // Basic JSON validation - schema validation deferred
1644                        if serde_json::from_value::<serde_json::Value>(value.clone()).is_err() {
1645                            field_errors.push(json!({
1646                                "path": "body",
1647                                "message": "invalid JSON"
1648                            }));
1649                        }
1650                    }
1651                }
1652            } else {
1653                field_errors.push(json!({
1654                    "path": "body",
1655                    "expected": "object",
1656                    "found": "missing",
1657                    "message": "Request body is required but not provided"
1658                }));
1659            }
1660        }
1661
1662        // Validate parameters with detailed error collection
1663        for param_ref in &route.operation.parameters {
1664            if let Some(param) = param_ref.as_item() {
1665                match param {
1666                    openapiv3::Parameter::Path { parameter_data, .. } => {
1667                        validate_parameter_detailed(
1668                            parameter_data,
1669                            path_params,
1670                            "path",
1671                            "path parameter",
1672                            &mut field_errors,
1673                        );
1674                    }
1675                    openapiv3::Parameter::Query { parameter_data, .. } => {
1676                        let deep_value = if Some("form") == Some("deepObject") {
1677                            build_deep_object(&parameter_data.name, query_params)
1678                        } else {
1679                            None
1680                        };
1681                        validate_parameter_detailed_with_deep(
1682                            parameter_data,
1683                            query_params,
1684                            "query",
1685                            "query parameter",
1686                            deep_value,
1687                            &mut field_errors,
1688                        );
1689                    }
1690                    openapiv3::Parameter::Header { parameter_data, .. } => {
1691                        validate_parameter_detailed(
1692                            parameter_data,
1693                            header_params,
1694                            "header",
1695                            "header parameter",
1696                            &mut field_errors,
1697                        );
1698                    }
1699                    openapiv3::Parameter::Cookie { parameter_data, .. } => {
1700                        validate_parameter_detailed(
1701                            parameter_data,
1702                            cookie_params,
1703                            "cookie",
1704                            "cookie parameter",
1705                            &mut field_errors,
1706                        );
1707                    }
1708                }
1709            }
1710        }
1711    }
1712
1713    // Return the detailed 422 error format
1714    json!({
1715        "error": "Schema validation failed",
1716        "details": field_errors,
1717        "method": method,
1718        "path": path_template,
1719        "timestamp": Utc::now().to_rfc3339(),
1720        "validation_type": "openapi_schema"
1721    })
1722}
1723
1724/// Helper function to validate a parameter
1725fn validate_parameter(
1726    parameter_data: &openapiv3::ParameterData,
1727    params_map: &Map<String, Value>,
1728    prefix: &str,
1729    aggregate: bool,
1730    errors: &mut Vec<String>,
1731    details: &mut Vec<serde_json::Value>,
1732) {
1733    match params_map.get(&parameter_data.name) {
1734        Some(v) => {
1735            if let ParameterSchemaOrContent::Schema(s) = &parameter_data.format {
1736                if let Some(schema) = s.as_item() {
1737                    let coerced = coerce_value_for_schema(v, schema);
1738                    // Validate the coerced value against the schema
1739                    if let Err(validation_error) =
1740                        OpenApiSchema::new(schema.clone()).validate(&coerced)
1741                    {
1742                        let error_msg = validation_error.to_string();
1743                        errors.push(format!(
1744                            "{} parameter '{}' validation failed: {}",
1745                            prefix, parameter_data.name, error_msg
1746                        ));
1747                        if aggregate {
1748                            details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
1749                        }
1750                    }
1751                }
1752            }
1753        }
1754        None => {
1755            if parameter_data.required {
1756                errors.push(format!(
1757                    "missing required {} parameter '{}'",
1758                    prefix, parameter_data.name
1759                ));
1760                details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
1761            }
1762        }
1763    }
1764}
1765
1766/// Helper function to validate a parameter with deep object support
1767#[allow(clippy::too_many_arguments)]
1768fn validate_parameter_with_deep_object(
1769    parameter_data: &openapiv3::ParameterData,
1770    params_map: &Map<String, Value>,
1771    prefix: &str,
1772    deep_value: Option<Value>,
1773    style: Option<&str>,
1774    aggregate: bool,
1775    errors: &mut Vec<String>,
1776    details: &mut Vec<serde_json::Value>,
1777) {
1778    match deep_value.as_ref().or_else(|| params_map.get(&parameter_data.name)) {
1779        Some(v) => {
1780            if let ParameterSchemaOrContent::Schema(s) = &parameter_data.format {
1781                if let Some(schema) = s.as_item() {
1782                    let coerced = coerce_by_style(v, schema, style); // Use the actual style
1783                                                                     // Validate the coerced value against the schema
1784                    if let Err(validation_error) =
1785                        OpenApiSchema::new(schema.clone()).validate(&coerced)
1786                    {
1787                        let error_msg = validation_error.to_string();
1788                        errors.push(format!(
1789                            "{} parameter '{}' validation failed: {}",
1790                            prefix, parameter_data.name, error_msg
1791                        ));
1792                        if aggregate {
1793                            details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
1794                        }
1795                    }
1796                }
1797            }
1798        }
1799        None => {
1800            if parameter_data.required {
1801                errors.push(format!(
1802                    "missing required {} parameter '{}'",
1803                    prefix, parameter_data.name
1804                ));
1805                details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
1806            }
1807        }
1808    }
1809}
1810
1811/// Helper function to validate a parameter with detailed error collection
1812fn validate_parameter_detailed(
1813    parameter_data: &openapiv3::ParameterData,
1814    params_map: &Map<String, Value>,
1815    location: &str,
1816    value_type: &str,
1817    field_errors: &mut Vec<Value>,
1818) {
1819    match params_map.get(&parameter_data.name) {
1820        Some(value) => {
1821            if let ParameterSchemaOrContent::Schema(schema) = &parameter_data.format {
1822                // Collect detailed validation errors for this parameter
1823                let details: Vec<serde_json::Value> = Vec::new();
1824                let param_path = format!("{}.{}", location, parameter_data.name);
1825
1826                // Apply coercion before validation
1827                if let Some(schema_ref) = schema.as_item() {
1828                    let coerced_value = coerce_value_for_schema(value, schema_ref);
1829                    // Validate the coerced value against the schema
1830                    if let Err(validation_error) =
1831                        OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
1832                    {
1833                        field_errors.push(json!({
1834                            "path": param_path,
1835                            "expected": "valid according to schema",
1836                            "found": coerced_value,
1837                            "message": validation_error.to_string()
1838                        }));
1839                    }
1840                }
1841
1842                for detail in details {
1843                    field_errors.push(json!({
1844                        "path": detail["path"],
1845                        "expected": detail["expected_type"],
1846                        "found": detail["value"],
1847                        "message": detail["message"]
1848                    }));
1849                }
1850            }
1851        }
1852        None => {
1853            if parameter_data.required {
1854                field_errors.push(json!({
1855                    "path": format!("{}.{}", location, parameter_data.name),
1856                    "expected": "value",
1857                    "found": "missing",
1858                    "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
1859                }));
1860            }
1861        }
1862    }
1863}
1864
1865/// Helper function to validate a parameter with deep object support and detailed errors
1866fn validate_parameter_detailed_with_deep(
1867    parameter_data: &openapiv3::ParameterData,
1868    params_map: &Map<String, Value>,
1869    location: &str,
1870    value_type: &str,
1871    deep_value: Option<Value>,
1872    field_errors: &mut Vec<Value>,
1873) {
1874    match deep_value.as_ref().or_else(|| params_map.get(&parameter_data.name)) {
1875        Some(value) => {
1876            if let ParameterSchemaOrContent::Schema(schema) = &parameter_data.format {
1877                // Collect detailed validation errors for this parameter
1878                let details: Vec<serde_json::Value> = Vec::new();
1879                let param_path = format!("{}.{}", location, parameter_data.name);
1880
1881                // Apply coercion before validation
1882                if let Some(schema_ref) = schema.as_item() {
1883                    let coerced_value = coerce_by_style(value, schema_ref, Some("form")); // Default to form style for now
1884                                                                                          // Validate the coerced value against the schema
1885                    if let Err(validation_error) =
1886                        OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
1887                    {
1888                        field_errors.push(json!({
1889                            "path": param_path,
1890                            "expected": "valid according to schema",
1891                            "found": coerced_value,
1892                            "message": validation_error.to_string()
1893                        }));
1894                    }
1895                }
1896
1897                for detail in details {
1898                    field_errors.push(json!({
1899                        "path": detail["path"],
1900                        "expected": detail["expected_type"],
1901                        "found": detail["value"],
1902                        "message": detail["message"]
1903                    }));
1904                }
1905            }
1906        }
1907        None => {
1908            if parameter_data.required {
1909                field_errors.push(json!({
1910                    "path": format!("{}.{}", location, parameter_data.name),
1911                    "expected": "value",
1912                    "found": "missing",
1913                    "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
1914                }));
1915            }
1916        }
1917    }
1918}
1919
1920/// Helper function to create an OpenAPI route registry from a file
1921pub async fn create_registry_from_file<P: AsRef<std::path::Path>>(
1922    path: P,
1923) -> Result<OpenApiRouteRegistry> {
1924    let spec = OpenApiSpec::from_file(path).await?;
1925    spec.validate()?;
1926    Ok(OpenApiRouteRegistry::new(spec))
1927}
1928
1929/// Helper function to create an OpenAPI route registry from JSON
1930pub fn create_registry_from_json(json: Value) -> Result<OpenApiRouteRegistry> {
1931    let spec = OpenApiSpec::from_json(json)?;
1932    spec.validate()?;
1933    Ok(OpenApiRouteRegistry::new(spec))
1934}
1935
1936#[cfg(test)]
1937mod tests {
1938    use super::*;
1939    use serde_json::json;
1940
1941    #[tokio::test]
1942    async fn test_registry_creation() {
1943        let spec_json = json!({
1944            "openapi": "3.0.0",
1945            "info": {
1946                "title": "Test API",
1947                "version": "1.0.0"
1948            },
1949            "paths": {
1950                "/users": {
1951                    "get": {
1952                        "summary": "Get users",
1953                        "responses": {
1954                            "200": {
1955                                "description": "Success",
1956                                "content": {
1957                                    "application/json": {
1958                                        "schema": {
1959                                            "type": "array",
1960                                            "items": {
1961                                                "type": "object",
1962                                                "properties": {
1963                                                    "id": {"type": "integer"},
1964                                                    "name": {"type": "string"}
1965                                                }
1966                                            }
1967                                        }
1968                                    }
1969                                }
1970                            }
1971                        }
1972                    },
1973                    "post": {
1974                        "summary": "Create user",
1975                        "requestBody": {
1976                            "content": {
1977                                "application/json": {
1978                                    "schema": {
1979                                        "type": "object",
1980                                        "properties": {
1981                                            "name": {"type": "string"}
1982                                        },
1983                                        "required": ["name"]
1984                                    }
1985                                }
1986                            }
1987                        },
1988                        "responses": {
1989                            "201": {
1990                                "description": "Created",
1991                                "content": {
1992                                    "application/json": {
1993                                        "schema": {
1994                                            "type": "object",
1995                                            "properties": {
1996                                                "id": {"type": "integer"},
1997                                                "name": {"type": "string"}
1998                                            }
1999                                        }
2000                                    }
2001                                }
2002                            }
2003                        }
2004                    }
2005                },
2006                "/users/{id}": {
2007                    "get": {
2008                        "summary": "Get user by ID",
2009                        "parameters": [
2010                            {
2011                                "name": "id",
2012                                "in": "path",
2013                                "required": true,
2014                                "schema": {"type": "integer"}
2015                            }
2016                        ],
2017                        "responses": {
2018                            "200": {
2019                                "description": "Success",
2020                                "content": {
2021                                    "application/json": {
2022                                        "schema": {
2023                                            "type": "object",
2024                                            "properties": {
2025                                                "id": {"type": "integer"},
2026                                                "name": {"type": "string"}
2027                                            }
2028                                        }
2029                                    }
2030                                }
2031                            }
2032                        }
2033                    }
2034                }
2035            }
2036        });
2037
2038        let registry = create_registry_from_json(spec_json).unwrap();
2039
2040        // Test basic properties
2041        assert_eq!(registry.paths().len(), 2);
2042        assert!(registry.paths().contains(&"/users".to_string()));
2043        assert!(registry.paths().contains(&"/users/{id}".to_string()));
2044
2045        assert_eq!(registry.methods().len(), 2);
2046        assert!(registry.methods().contains(&"GET".to_string()));
2047        assert!(registry.methods().contains(&"POST".to_string()));
2048
2049        // Test route lookup
2050        let get_users_route = registry.get_route("/users", "GET").unwrap();
2051        assert_eq!(get_users_route.method, "GET");
2052        assert_eq!(get_users_route.path, "/users");
2053
2054        let post_users_route = registry.get_route("/users", "POST").unwrap();
2055        assert_eq!(post_users_route.method, "POST");
2056        assert!(post_users_route.operation.request_body.is_some());
2057
2058        // Test path parameter conversion
2059        let user_by_id_route = registry.get_route("/users/{id}", "GET").unwrap();
2060        assert_eq!(user_by_id_route.axum_path(), "/users/{id}");
2061    }
2062
2063    #[tokio::test]
2064    async fn test_validate_request_with_params_and_formats() {
2065        let spec_json = json!({
2066            "openapi": "3.0.0",
2067            "info": { "title": "Test API", "version": "1.0.0" },
2068            "paths": {
2069                "/users/{id}": {
2070                    "post": {
2071                        "parameters": [
2072                            { "name": "id", "in": "path", "required": true, "schema": {"type": "string"} },
2073                            { "name": "q",  "in": "query", "required": false, "schema": {"type": "integer"} }
2074                        ],
2075                        "requestBody": {
2076                            "content": {
2077                                "application/json": {
2078                                    "schema": {
2079                                        "type": "object",
2080                                        "required": ["email", "website"],
2081                                        "properties": {
2082                                            "email":   {"type": "string", "format": "email"},
2083                                            "website": {"type": "string", "format": "uri"}
2084                                        }
2085                                    }
2086                                }
2087                            }
2088                        },
2089                        "responses": {"200": {"description": "ok"}}
2090                    }
2091                }
2092            }
2093        });
2094
2095        let registry = create_registry_from_json(spec_json).unwrap();
2096        let mut path_params = serde_json::Map::new();
2097        path_params.insert("id".to_string(), json!("abc"));
2098        let mut query_params = serde_json::Map::new();
2099        query_params.insert("q".to_string(), json!(123));
2100
2101        // valid body
2102        let body = json!({"email":"a@b.co","website":"https://example.com"});
2103        assert!(registry
2104            .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2105            .is_ok());
2106
2107        // invalid email
2108        let bad_email = json!({"email":"not-an-email","website":"https://example.com"});
2109        assert!(registry
2110            .validate_request_with(
2111                "/users/{id}",
2112                "POST",
2113                &path_params,
2114                &query_params,
2115                Some(&bad_email)
2116            )
2117            .is_err());
2118
2119        // missing required path param
2120        let empty_path_params = serde_json::Map::new();
2121        assert!(registry
2122            .validate_request_with(
2123                "/users/{id}",
2124                "POST",
2125                &empty_path_params,
2126                &query_params,
2127                Some(&body)
2128            )
2129            .is_err());
2130    }
2131
2132    #[tokio::test]
2133    async fn test_ref_resolution_for_params_and_body() {
2134        let spec_json = json!({
2135            "openapi": "3.0.0",
2136            "info": { "title": "Ref API", "version": "1.0.0" },
2137            "components": {
2138                "schemas": {
2139                    "EmailWebsite": {
2140                        "type": "object",
2141                        "required": ["email", "website"],
2142                        "properties": {
2143                            "email":   {"type": "string", "format": "email"},
2144                            "website": {"type": "string", "format": "uri"}
2145                        }
2146                    }
2147                },
2148                "parameters": {
2149                    "PathId": {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}},
2150                    "QueryQ": {"name": "q",  "in": "query", "required": false, "schema": {"type": "integer"}}
2151                },
2152                "requestBodies": {
2153                    "CreateUser": {
2154                        "content": {
2155                            "application/json": {
2156                                "schema": {"$ref": "#/components/schemas/EmailWebsite"}
2157                            }
2158                        }
2159                    }
2160                }
2161            },
2162            "paths": {
2163                "/users/{id}": {
2164                    "post": {
2165                        "parameters": [
2166                            {"$ref": "#/components/parameters/PathId"},
2167                            {"$ref": "#/components/parameters/QueryQ"}
2168                        ],
2169                        "requestBody": {"$ref": "#/components/requestBodies/CreateUser"},
2170                        "responses": {"200": {"description": "ok"}}
2171                    }
2172                }
2173            }
2174        });
2175
2176        let registry = create_registry_from_json(spec_json).unwrap();
2177        let mut path_params = serde_json::Map::new();
2178        path_params.insert("id".to_string(), json!("abc"));
2179        let mut query_params = serde_json::Map::new();
2180        query_params.insert("q".to_string(), json!(7));
2181
2182        let body = json!({"email":"user@example.com","website":"https://example.com"});
2183        assert!(registry
2184            .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2185            .is_ok());
2186
2187        let bad = json!({"email":"nope","website":"https://example.com"});
2188        assert!(registry
2189            .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&bad))
2190            .is_err());
2191    }
2192
2193    #[tokio::test]
2194    async fn test_header_cookie_and_query_coercion() {
2195        let spec_json = json!({
2196            "openapi": "3.0.0",
2197            "info": { "title": "Params API", "version": "1.0.0" },
2198            "paths": {
2199                "/items": {
2200                    "get": {
2201                        "parameters": [
2202                            {"name": "X-Flag", "in": "header", "required": true, "schema": {"type": "boolean"}},
2203                            {"name": "session", "in": "cookie", "required": true, "schema": {"type": "string"}},
2204                            {"name": "ids", "in": "query", "required": false, "schema": {"type": "array", "items": {"type": "integer"}}}
2205                        ],
2206                        "responses": {"200": {"description": "ok"}}
2207                    }
2208                }
2209            }
2210        });
2211
2212        let registry = create_registry_from_json(spec_json).unwrap();
2213
2214        let path_params = serde_json::Map::new();
2215        let mut query_params = serde_json::Map::new();
2216        // comma-separated string for array should coerce
2217        query_params.insert("ids".to_string(), json!("1,2,3"));
2218        let mut header_params = serde_json::Map::new();
2219        header_params.insert("X-Flag".to_string(), json!("true"));
2220        let mut cookie_params = serde_json::Map::new();
2221        cookie_params.insert("session".to_string(), json!("abc123"));
2222
2223        assert!(registry
2224            .validate_request_with_all(
2225                "/items",
2226                "GET",
2227                &path_params,
2228                &query_params,
2229                &header_params,
2230                &cookie_params,
2231                None
2232            )
2233            .is_ok());
2234
2235        // Missing required cookie
2236        let empty_cookie = serde_json::Map::new();
2237        assert!(registry
2238            .validate_request_with_all(
2239                "/items",
2240                "GET",
2241                &path_params,
2242                &query_params,
2243                &header_params,
2244                &empty_cookie,
2245                None
2246            )
2247            .is_err());
2248
2249        // Bad boolean header value (cannot coerce)
2250        let mut bad_header = serde_json::Map::new();
2251        bad_header.insert("X-Flag".to_string(), json!("notabool"));
2252        assert!(registry
2253            .validate_request_with_all(
2254                "/items",
2255                "GET",
2256                &path_params,
2257                &query_params,
2258                &bad_header,
2259                &cookie_params,
2260                None
2261            )
2262            .is_err());
2263    }
2264
2265    #[tokio::test]
2266    async fn test_query_styles_space_pipe_deepobject() {
2267        let spec_json = json!({
2268            "openapi": "3.0.0",
2269            "info": { "title": "Query Styles API", "version": "1.0.0" },
2270            "paths": {"/search": {"get": {
2271                "parameters": [
2272                    {"name":"tags","in":"query","style":"spaceDelimited","schema":{"type":"array","items":{"type":"string"}}},
2273                    {"name":"ids","in":"query","style":"pipeDelimited","schema":{"type":"array","items":{"type":"integer"}}},
2274                    {"name":"filter","in":"query","style":"deepObject","schema":{"type":"object","properties":{"color":{"type":"string"}},"required":["color"]}}
2275                ],
2276                "responses": {"200": {"description":"ok"}}
2277            }} }
2278        });
2279
2280        let registry = create_registry_from_json(spec_json).unwrap();
2281
2282        let path_params = Map::new();
2283        let mut query = Map::new();
2284        query.insert("tags".into(), json!("alpha beta gamma"));
2285        query.insert("ids".into(), json!("1|2|3"));
2286        query.insert("filter[color]".into(), json!("red"));
2287
2288        assert!(registry
2289            .validate_request_with("/search", "GET", &path_params, &query, None)
2290            .is_ok());
2291    }
2292
2293    #[tokio::test]
2294    async fn test_oneof_anyof_allof_validation() {
2295        let spec_json = json!({
2296            "openapi": "3.0.0",
2297            "info": { "title": "Composite API", "version": "1.0.0" },
2298            "paths": {
2299                "/composite": {
2300                    "post": {
2301                        "requestBody": {
2302                            "content": {
2303                                "application/json": {
2304                                    "schema": {
2305                                        "allOf": [
2306                                            {"type": "object", "required": ["base"], "properties": {"base": {"type": "string"}}}
2307                                        ],
2308                                        "oneOf": [
2309                                            {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"], "not": {"required": ["b"]}},
2310                                            {"type": "object", "properties": {"b": {"type": "integer"}}, "required": ["b"], "not": {"required": ["a"]}}
2311                                        ],
2312                                        "anyOf": [
2313                                            {"type": "object", "properties": {"flag": {"type": "boolean"}}, "required": ["flag"]},
2314                                            {"type": "object", "properties": {"extra": {"type": "string"}}, "required": ["extra"]}
2315                                        ]
2316                                    }
2317                                }
2318                            }
2319                        },
2320                        "responses": {"200": {"description": "ok"}}
2321                    }
2322                }
2323            }
2324        });
2325
2326        let registry = create_registry_from_json(spec_json).unwrap();
2327        // valid: satisfies base via allOf, exactly one of a/b, and at least one of flag/extra
2328        let ok = json!({"base": "x", "a": 1, "flag": true});
2329        assert!(registry
2330            .validate_request_with(
2331                "/composite",
2332                "POST",
2333                &serde_json::Map::new(),
2334                &serde_json::Map::new(),
2335                Some(&ok)
2336            )
2337            .is_ok());
2338
2339        // invalid oneOf: both a and b present
2340        let bad_oneof = json!({"base": "x", "a": 1, "b": 2, "flag": false});
2341        assert!(registry
2342            .validate_request_with(
2343                "/composite",
2344                "POST",
2345                &serde_json::Map::new(),
2346                &serde_json::Map::new(),
2347                Some(&bad_oneof)
2348            )
2349            .is_err());
2350
2351        // invalid anyOf: none of flag/extra present
2352        let bad_anyof = json!({"base": "x", "a": 1});
2353        assert!(registry
2354            .validate_request_with(
2355                "/composite",
2356                "POST",
2357                &serde_json::Map::new(),
2358                &serde_json::Map::new(),
2359                Some(&bad_anyof)
2360            )
2361            .is_err());
2362
2363        // invalid allOf: missing base
2364        let bad_allof = json!({"a": 1, "flag": true});
2365        assert!(registry
2366            .validate_request_with(
2367                "/composite",
2368                "POST",
2369                &serde_json::Map::new(),
2370                &serde_json::Map::new(),
2371                Some(&bad_allof)
2372            )
2373            .is_err());
2374    }
2375
2376    #[tokio::test]
2377    async fn test_overrides_warn_mode_allows_invalid() {
2378        // Spec with a POST route expecting an integer query param
2379        let spec_json = json!({
2380            "openapi": "3.0.0",
2381            "info": { "title": "Overrides API", "version": "1.0.0" },
2382            "paths": {"/things": {"post": {
2383                "parameters": [{"name":"q","in":"query","required":true,"schema":{"type":"integer"}}],
2384                "responses": {"200": {"description":"ok"}}
2385            }}}
2386        });
2387
2388        let spec = OpenApiSpec::from_json(spec_json).unwrap();
2389        let mut overrides = std::collections::HashMap::new();
2390        overrides.insert("POST /things".to_string(), ValidationMode::Warn);
2391        let registry = OpenApiRouteRegistry::new_with_options(
2392            spec,
2393            ValidationOptions {
2394                request_mode: ValidationMode::Enforce,
2395                aggregate_errors: true,
2396                validate_responses: false,
2397                overrides,
2398                admin_skip_prefixes: vec![],
2399                response_template_expand: false,
2400                validation_status: None,
2401            },
2402        );
2403
2404        // Invalid q (missing) should warn, not error
2405        let ok = registry.validate_request_with("/things", "POST", &Map::new(), &Map::new(), None);
2406        assert!(ok.is_ok());
2407    }
2408
2409    #[tokio::test]
2410    async fn test_admin_skip_prefix_short_circuit() {
2411        let spec_json = json!({
2412            "openapi": "3.0.0",
2413            "info": { "title": "Skip API", "version": "1.0.0" },
2414            "paths": {}
2415        });
2416        let spec = OpenApiSpec::from_json(spec_json).unwrap();
2417        let registry = OpenApiRouteRegistry::new_with_options(
2418            spec,
2419            ValidationOptions {
2420                request_mode: ValidationMode::Enforce,
2421                aggregate_errors: true,
2422                validate_responses: false,
2423                overrides: std::collections::HashMap::new(),
2424                admin_skip_prefixes: vec!["/admin".into()],
2425                response_template_expand: false,
2426                validation_status: None,
2427            },
2428        );
2429
2430        // No route exists for this, but skip prefix means it is accepted
2431        let res = registry.validate_request_with_all(
2432            "/admin/__mockforge/health",
2433            "GET",
2434            &Map::new(),
2435            &Map::new(),
2436            &Map::new(),
2437            &Map::new(),
2438            None,
2439        );
2440        assert!(res.is_ok());
2441    }
2442
2443    #[test]
2444    fn test_path_conversion() {
2445        assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users"), "/users");
2446        assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users/{id}"), "/users/{id}");
2447        assert_eq!(
2448            OpenApiRouteRegistry::convert_path_to_axum("/users/{id}/posts/{postId}"),
2449            "/users/{id}/posts/{postId}"
2450        );
2451    }
2452}