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