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