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