mockforge_core/
openapi_routes.rs

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