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