1pub mod builder;
13pub mod generation;
14#[doc(hidden)]
15pub mod registry;
16pub mod validation;
17
18use crate::response::AiGenerator;
19use crate::response_rewriter::ResponseRewriter;
20use crate::{OpenApiOperation, OpenApiRoute, OpenApiSchema, OpenApiSpec};
21use axum::extract::{DefaultBodyLimit, Path as AxumPath, RawQuery};
22use axum::http::HeaderMap;
23use axum::response::IntoResponse;
24use axum::routing::*;
25use axum::{Json, Router};
26pub use builder::*;
27use chrono::Utc;
28pub use generation::*;
29use mockforge_foundation::ai_response::RequestContext;
30use mockforge_foundation::error::{Error, Result};
31use mockforge_foundation::latency::LatencyInjector;
32use mockforge_foundation::response_generation_trace::ResponseGenerationTrace;
33use mockforge_foundation::schema_diff::validation_diff;
34use once_cell::sync::Lazy;
35use openapiv3::ParameterSchemaOrContent;
36use serde_json::{json, Map, Value};
37use std::collections::{HashMap, HashSet, VecDeque};
38use std::sync::{Arc, Mutex};
39use tracing;
40pub use validation::*;
41
42#[derive(Clone)]
44pub struct OpenApiRouteRegistry {
45 spec: Arc<OpenApiSpec>,
47 routes: Vec<OpenApiRoute>,
49 options: ValidationOptions,
51 custom_fixture_loader: Option<Arc<crate::custom_fixture::CustomFixtureLoader>>,
53}
54
55#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)]
57pub enum ValidationMode {
58 Disabled,
60 #[default]
62 Warn,
63 Enforce,
65}
66
67#[derive(Debug, Clone)]
69pub struct ValidationOptions {
70 pub request_mode: ValidationMode,
72 pub aggregate_errors: bool,
74 pub validate_responses: bool,
76 pub overrides: HashMap<String, ValidationMode>,
78 pub admin_skip_prefixes: Vec<String>,
80 pub response_template_expand: bool,
82 pub validation_status: Option<u16>,
84}
85
86impl Default for ValidationOptions {
87 fn default() -> Self {
88 Self {
89 request_mode: ValidationMode::Enforce,
90 aggregate_errors: true,
91 validate_responses: false,
92 overrides: HashMap::new(),
93 admin_skip_prefixes: Vec::new(),
94 response_template_expand: false,
95 validation_status: None,
96 }
97 }
98}
99
100#[derive(Clone)]
105pub struct RouterContext {
106 pub custom_fixture_loader: Option<Arc<crate::custom_fixture::CustomFixtureLoader>>,
108 pub latency_injector: Option<LatencyInjector>,
110 pub failure_injector: Option<mockforge_foundation::failure_injection::FailureInjector>,
112 pub response_rewriter: Option<Arc<dyn ResponseRewriter>>,
116 pub overrides_enabled: bool,
119 pub ai_generator: Option<Arc<dyn AiGenerator + Send + Sync>>,
121 pub mockai: Option<
125 Arc<
126 tokio::sync::RwLock<
127 dyn mockforge_foundation::intelligent_behavior::MockAiBehavior + Send + Sync,
128 >,
129 >,
130 >,
131 pub enable_full_validation: bool,
133 pub enable_template_expand: bool,
135 pub add_spec_endpoint: bool,
137}
138
139impl Default for RouterContext {
140 fn default() -> Self {
141 Self {
142 custom_fixture_loader: None,
143 latency_injector: None,
144 failure_injector: None,
145 response_rewriter: None,
146 overrides_enabled: false,
147 ai_generator: None,
148 mockai: None,
149 enable_full_validation: false,
150 enable_template_expand: false,
151 add_spec_endpoint: true,
152 }
153 }
154}
155
156fn openapi_body_limit_bytes() -> usize {
171 const DEFAULT_MB: usize = 50;
172 std::env::var("MOCKFORGE_HTTP_BODY_LIMIT_MB")
173 .ok()
174 .and_then(|v| v.parse::<usize>().ok())
175 .unwrap_or(DEFAULT_MB)
176 .saturating_mul(1024 * 1024)
177}
178
179impl OpenApiRouteRegistry {
180 pub fn new(spec: OpenApiSpec) -> Self {
182 Self::new_with_env(spec)
183 }
184
185 pub fn new_with_env(spec: OpenApiSpec) -> Self {
194 Self::new_with_env_and_persona(spec, None)
195 }
196
197 pub fn new_with_env_and_persona(
199 spec: OpenApiSpec,
200 persona: Option<Arc<mockforge_foundation::intelligent_behavior::Persona>>,
201 ) -> Self {
202 tracing::debug!("Creating OpenAPI route registry");
203 let spec = Arc::new(spec);
204 let routes = Self::generate_routes_with_persona(&spec, persona);
205 let options = ValidationOptions {
206 request_mode: match std::env::var("MOCKFORGE_REQUEST_VALIDATION")
207 .unwrap_or_else(|_| "enforce".into())
208 .to_ascii_lowercase()
209 .as_str()
210 {
211 "off" | "disable" | "disabled" => ValidationMode::Disabled,
212 "warn" | "warning" => ValidationMode::Warn,
213 _ => ValidationMode::Enforce,
214 },
215 aggregate_errors: std::env::var("MOCKFORGE_AGGREGATE_ERRORS")
216 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
217 .unwrap_or(true),
218 validate_responses: std::env::var("MOCKFORGE_RESPONSE_VALIDATION")
219 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
220 .unwrap_or(false),
221 overrides: HashMap::new(),
222 admin_skip_prefixes: Vec::new(),
223 response_template_expand: std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
224 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
225 .unwrap_or(false),
226 validation_status: std::env::var("MOCKFORGE_VALIDATION_STATUS")
227 .ok()
228 .and_then(|s| s.parse::<u16>().ok()),
229 };
230 Self {
231 spec,
232 routes,
233 options,
234 custom_fixture_loader: None,
235 }
236 }
237
238 pub fn new_with_options(spec: OpenApiSpec, options: ValidationOptions) -> Self {
240 Self::new_with_options_and_persona(spec, options, None)
241 }
242
243 pub fn new_with_options_and_persona(
245 spec: OpenApiSpec,
246 options: ValidationOptions,
247 persona: Option<Arc<mockforge_foundation::intelligent_behavior::Persona>>,
248 ) -> Self {
249 tracing::debug!("Creating OpenAPI route registry with custom options");
250 let spec = Arc::new(spec);
251 let routes = Self::generate_routes_with_persona(&spec, persona);
252 Self {
253 spec,
254 routes,
255 options,
256 custom_fixture_loader: None,
257 }
258 }
259
260 pub fn with_custom_fixture_loader(
262 mut self,
263 loader: Arc<crate::custom_fixture::CustomFixtureLoader>,
264 ) -> Self {
265 self.custom_fixture_loader = Some(loader);
266 self
267 }
268
269 pub fn clone_for_validation(&self) -> Self {
274 OpenApiRouteRegistry {
275 spec: self.spec.clone(),
276 routes: self.routes.clone(),
277 options: self.options.clone(),
278 custom_fixture_loader: self.custom_fixture_loader.clone(),
279 }
280 }
281
282 fn generate_routes_with_persona(
284 spec: &Arc<OpenApiSpec>,
285 persona: Option<Arc<mockforge_foundation::intelligent_behavior::Persona>>,
286 ) -> Vec<OpenApiRoute> {
287 let mut routes = Vec::new();
288
289 let all_paths_ops = spec.all_paths_and_operations();
290 tracing::debug!("Generating routes from OpenAPI spec with {} paths", all_paths_ops.len());
291
292 for (path, operations) in all_paths_ops {
293 tracing::debug!("Processing path: {}", path);
294 for (method, operation) in operations {
295 routes.push(OpenApiRoute::from_operation_with_persona(
296 &method,
297 path.clone(),
298 &operation,
299 spec.clone(),
300 persona.clone(),
301 ));
302 }
303 }
304
305 tracing::debug!("Generated {} total routes from OpenAPI spec", routes.len());
306 routes
307 }
308
309 pub fn routes(&self) -> &[OpenApiRoute] {
311 &self.routes
312 }
313
314 pub fn spec(&self) -> &OpenApiSpec {
316 &self.spec
317 }
318
319 fn normalize_path_for_dedup(path: &str) -> String {
323 let mut result = String::with_capacity(path.len());
324 let mut in_brace = false;
325 for ch in path.chars() {
326 if ch == '{' {
327 in_brace = true;
328 result.push_str("{_}");
329 } else if ch == '}' {
330 in_brace = false;
331 } else if !in_brace {
332 result.push(ch);
333 }
334 }
335 result
336 }
337
338 fn deduplicated_routes(&self) -> Vec<(String, &OpenApiRoute)> {
344 let mut result = Vec::new();
345 let mut registered_routes: HashSet<(String, String)> = HashSet::new();
346 let mut canonical_paths: HashMap<String, String> = HashMap::new();
347
348 for route in &self.routes {
349 if !route.is_valid_axum_path() {
350 tracing::warn!(
351 "Skipping route with unsupported path syntax: {} {}",
352 route.method,
353 route.path
354 );
355 continue;
356 }
357 let axum_path = route.axum_path();
358 let normalized = Self::normalize_path_for_dedup(&axum_path);
359 let axum_path = canonical_paths
360 .entry(normalized.clone())
361 .or_insert_with(|| axum_path.clone())
362 .clone();
363 let route_key = (route.method.clone(), normalized);
364 if !registered_routes.insert(route_key) {
365 tracing::debug!(
366 "Skipping duplicate route: {} {} (axum path: {})",
367 route.method,
368 route.path,
369 axum_path
370 );
371 continue;
372 }
373 result.push((axum_path, route));
374 }
375 result
376 }
377
378 fn route_for_method<H, T>(router: Router, path: &str, method: &str, handler: H) -> Router
382 where
383 H: axum::handler::Handler<T, ()>,
384 T: 'static,
385 {
386 match method {
387 "GET" => router.route(path, get(handler)),
388 "POST" => router.route(path, post(handler)),
389 "PUT" => router.route(path, put(handler)),
390 "DELETE" => router.route(path, delete(handler)),
391 "PATCH" => router.route(path, patch(handler)),
392 "HEAD" => router.route(path, head(handler)),
393 "OPTIONS" => router.route(path, options(handler)),
394 _ => router,
395 }
396 }
397
398 pub fn build_router(self) -> Router {
400 let ctx = RouterContext {
401 custom_fixture_loader: self.custom_fixture_loader.clone(),
402 enable_full_validation: true,
403 enable_template_expand: true,
404 add_spec_endpoint: true,
405 ..Default::default()
406 };
407 self.build_router_with_context(ctx)
408 }
409
410 fn build_router_with_context(self, ctx: RouterContext) -> Router {
415 let mut router = Router::new();
416 tracing::debug!("Building router from {} routes", self.routes.len());
417
418 let deduped = self.deduplicated_routes();
419 let ctx = Arc::new(ctx);
420 let validator = Arc::new(self.clone_for_validation());
428 for (axum_path, route) in &deduped {
429 tracing::debug!("Adding route: {} {}", route.method, route.path);
430 let operation = route.operation.clone();
431 let method = route.method.clone();
432 let path_template = route.path.clone();
433 let validator = validator.clone();
434 let route_clone = (*route).clone();
435 let ctx = ctx.clone();
436
437 let mut operation_tags = operation.tags.clone();
439 if let Some(operation_id) = &operation.operation_id {
440 operation_tags.push(operation_id.clone());
441 }
442
443 let handler = move |AxumPath(path_params): AxumPath<HashMap<String, String>>,
445 RawQuery(raw_query): RawQuery,
446 headers: HeaderMap,
447 body: axum::body::Bytes| async move {
448 tracing::debug!("Handling OpenAPI request: {} {}", method, path_template);
449
450 if let Some(ref loader) = ctx.custom_fixture_loader {
452 use crate::request_fingerprint::RequestFingerprint;
453 use axum::http::{Method, Uri};
454
455 let mut request_path = path_template.clone();
457 for (key, value) in &path_params {
458 request_path = request_path.replace(&format!("{{{}}}", key), value);
459 }
460
461 let normalized_request_path =
463 crate::custom_fixture::CustomFixtureLoader::normalize_path(&request_path);
464
465 let query_string =
467 raw_query.as_ref().map(|q| q.to_string()).unwrap_or_default();
468
469 let uri_str = if query_string.is_empty() {
472 normalized_request_path.clone()
473 } else {
474 format!("{}?{}", normalized_request_path, query_string)
475 };
476
477 if let Ok(uri) = uri_str.parse::<Uri>() {
478 let http_method =
479 Method::from_bytes(method.as_bytes()).unwrap_or(Method::GET);
480 let body_slice = if body.is_empty() {
481 None
482 } else {
483 Some(body.as_ref())
484 };
485 let fingerprint =
486 RequestFingerprint::new(http_method, &uri, &headers, body_slice);
487
488 tracing::debug!(
490 "Checking fixture for {} {} (template: '{}', request_path: '{}', normalized: '{}', fingerprint.path: '{}')",
491 method,
492 path_template,
493 path_template,
494 request_path,
495 normalized_request_path,
496 fingerprint.path
497 );
498
499 if let Some(custom_fixture) = loader.load_fixture(&fingerprint) {
500 tracing::debug!(
501 "Using custom fixture for {} {}",
502 method,
503 path_template
504 );
505
506 if custom_fixture.delay_ms > 0 {
508 tokio::time::sleep(tokio::time::Duration::from_millis(
509 custom_fixture.delay_ms,
510 ))
511 .await;
512 }
513
514 let response_body = if custom_fixture.response.is_string() {
516 custom_fixture.response.as_str().unwrap().to_string()
517 } else {
518 serde_json::to_string(&custom_fixture.response)
519 .unwrap_or_else(|_| "{}".to_string())
520 };
521
522 let json_value: Value = serde_json::from_str(&response_body)
524 .unwrap_or_else(|_| serde_json::json!({}));
525
526 let status = axum::http::StatusCode::from_u16(custom_fixture.status)
528 .unwrap_or(axum::http::StatusCode::OK);
529
530 let mut response = (status, Json(json_value)).into_response();
531
532 let response_headers = response.headers_mut();
534 for (key, value) in &custom_fixture.headers {
535 if let (Ok(header_name), Ok(header_value)) = (
536 axum::http::HeaderName::from_bytes(key.as_bytes()),
537 axum::http::HeaderValue::from_str(value),
538 ) {
539 response_headers.insert(header_name, header_value);
540 }
541 }
542
543 if !custom_fixture.headers.contains_key("content-type") {
545 response_headers.insert(
546 axum::http::header::CONTENT_TYPE,
547 axum::http::HeaderValue::from_static("application/json"),
548 );
549 }
550
551 return response;
552 }
553 }
554 }
555
556 if let Some(ref failure_injector) = ctx.failure_injector {
558 if let Some((status_code, error_message)) =
559 failure_injector.process_request(&operation_tags)
560 {
561 let payload = serde_json::json!({
562 "error": error_message,
563 "injected_failure": true
564 });
565 let body_bytes = serde_json::to_vec(&payload)
566 .unwrap_or_else(|_| br#"{"error":"injected failure"}"#.to_vec());
567 return axum::http::Response::builder()
568 .status(
569 axum::http::StatusCode::from_u16(status_code)
570 .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR),
571 )
572 .header(axum::http::header::CONTENT_TYPE, "application/json")
573 .body(axum::body::Body::from(body_bytes))
574 .expect("Response builder should create valid response");
575 }
576 }
577
578 if let Some(ref injector) = ctx.latency_injector {
580 if let Err(e) = injector.inject_latency(&operation_tags).await {
581 tracing::warn!("Failed to inject latency: {}", e);
582 }
583 }
584
585 let scenario = headers
587 .get("X-Mockforge-Scenario")
588 .and_then(|v| v.to_str().ok())
589 .map(|s| s.to_string())
590 .or_else(|| std::env::var("MOCKFORGE_HTTP_SCENARIO").ok());
591
592 let status_override = headers
593 .get("X-Mockforge-Response-Status")
594 .and_then(|v| v.to_str().ok())
595 .and_then(|s| s.parse::<u16>().ok());
596
597 let (selected_status, mock_response) = route_clone
599 .mock_response_with_status_and_scenario_and_override(
600 scenario.as_deref(),
601 status_override,
602 );
603
604 if ctx.enable_full_validation {
606 let mut path_map = Map::new();
608 for (k, v) in &path_params {
609 path_map.insert(k.clone(), Value::String(v.clone()));
610 }
611
612 let mut query_map = Map::new();
614 if let Some(ref q) = raw_query {
615 for (k, v) in url::form_urlencoded::parse(q.as_bytes()) {
616 query_map.insert(k.to_string(), Value::String(v.to_string()));
617 }
618 }
619
620 let mut header_map = Map::new();
622 for p_ref in &operation.parameters {
623 if let Some(openapiv3::Parameter::Header { parameter_data, .. }) =
624 p_ref.as_item()
625 {
626 let name_lc = parameter_data.name.to_ascii_lowercase();
627 if let Ok(hn) = axum::http::HeaderName::from_bytes(name_lc.as_bytes()) {
628 if let Some(val) = headers.get(hn) {
629 if let Ok(s) = val.to_str() {
630 header_map.insert(
631 parameter_data.name.clone(),
632 Value::String(s.to_string()),
633 );
634 }
635 }
636 }
637 }
638 }
639
640 let mut cookie_map = Map::new();
642 if let Some(val) = headers.get(axum::http::header::COOKIE) {
643 if let Ok(s) = val.to_str() {
644 for part in s.split(';') {
645 let part = part.trim();
646 if let Some((k, v)) = part.split_once('=') {
647 cookie_map.insert(k.to_string(), Value::String(v.to_string()));
648 }
649 }
650 }
651 }
652
653 let is_multipart = headers
655 .get(axum::http::header::CONTENT_TYPE)
656 .and_then(|v| v.to_str().ok())
657 .map(|ct| ct.starts_with("multipart/form-data"))
658 .unwrap_or(false);
659
660 #[allow(unused_assignments)]
662 let mut multipart_fields = HashMap::new();
663 let mut _multipart_files = HashMap::new();
664 let mut body_json: Option<Value> = None;
665
666 if is_multipart {
667 match extract_multipart_from_bytes(&body, &headers).await {
669 Ok((fields, files)) => {
670 multipart_fields = fields;
671 _multipart_files = files;
672 let mut body_obj = Map::new();
674 for (k, v) in &multipart_fields {
675 body_obj.insert(k.clone(), v.clone());
676 }
677 if !body_obj.is_empty() {
678 body_json = Some(Value::Object(body_obj));
679 }
680 }
681 Err(e) => {
682 tracing::warn!("Failed to parse multipart data: {}", e);
683 }
684 }
685 } else {
686 body_json = if !body.is_empty() {
688 serde_json::from_slice(&body).ok()
689 } else {
690 None
691 };
692 }
693
694 if let Err(e) = validator.validate_request_with_all(
695 &path_template,
696 &method,
697 &path_map,
698 &query_map,
699 &header_map,
700 &cookie_map,
701 body_json.as_ref(),
702 ) {
703 let status_code =
705 validator.options.validation_status.unwrap_or_else(|| {
706 std::env::var("MOCKFORGE_VALIDATION_STATUS")
707 .ok()
708 .and_then(|s| s.parse::<u16>().ok())
709 .unwrap_or(400)
710 });
711
712 let payload = if status_code == 422 {
713 generate_enhanced_422_response(
715 &validator,
716 &path_template,
717 &method,
718 body_json.as_ref(),
719 &path_map,
720 &query_map,
721 &header_map,
722 &cookie_map,
723 )
724 } else {
725 let msg = format!("{}", e);
727 let detail_val = serde_json::from_str::<Value>(&msg)
728 .unwrap_or(serde_json::json!(msg));
729 json!({
730 "error": "request validation failed",
731 "detail": detail_val,
732 "method": method,
733 "path": path_template,
734 "timestamp": Utc::now().to_rfc3339(),
735 })
736 };
737
738 record_validation_error(&payload);
739
740 let reason = payload
747 .get("detail")
748 .and_then(|d| {
749 if d.is_string() {
750 d.as_str().map(|s| s.to_string())
751 } else {
752 serde_json::to_string(d).ok()
753 }
754 })
755 .unwrap_or_else(|| {
756 payload
757 .get("error")
758 .and_then(|v| v.as_str())
759 .unwrap_or("request validation failed")
760 .to_string()
761 });
762 let category = classify_validation_reason(&reason);
763 mockforge_foundation::conformance_violations::record(
764 mockforge_foundation::conformance_violations::ServerConformanceViolation {
765 timestamp: Utc::now(),
766 method: method.to_string(),
767 path: path_template.clone(),
768 client_ip: "unknown".to_string(),
769 status: status_code,
770 reason,
771 category,
772 },
773 );
774
775 let status = axum::http::StatusCode::from_u16(status_code)
776 .unwrap_or(axum::http::StatusCode::BAD_REQUEST);
777
778 let body_bytes = serde_json::to_vec(&payload)
780 .unwrap_or_else(|_| br#"{"error":"Serialization failed"}"#.to_vec());
781
782 return axum::http::Response::builder()
783 .status(status)
784 .header(axum::http::header::CONTENT_TYPE, "application/json")
785 .body(axum::body::Body::from(body_bytes))
786 .expect("Response builder should create valid response with valid headers and body");
787 }
788 }
789
790 let mut final_response = mock_response.clone();
799 let env_expand: Option<bool> = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
800 .ok()
801 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"));
802 let expand = match env_expand {
803 Some(v) => v,
804 None => {
805 ctx.enable_template_expand || validator.options.response_template_expand
806 }
807 };
808 if expand {
809 if let Some(ref rewriter) = ctx.response_rewriter {
810 rewriter.expand_tokens(&mut final_response);
811 }
812 }
813
814 if ctx.overrides_enabled {
816 if let Some(ref rewriter) = ctx.response_rewriter {
817 let op_tags =
818 operation.operation_id.clone().map(|id| vec![id]).unwrap_or_default();
819 rewriter.apply_overrides(
820 &operation.operation_id.clone().unwrap_or_default(),
821 &op_tags,
822 &path_template,
823 &mut final_response,
824 );
825 }
826 }
827
828 if ctx.enable_full_validation {
830 if validator.options.validate_responses {
832 if let Some((status_code, _response)) = operation
834 .responses
835 .responses
836 .iter()
837 .filter_map(|(status, resp)| match status {
838 openapiv3::StatusCode::Code(code)
839 if *code >= 200 && *code < 300 =>
840 {
841 resp.as_item().map(|r| ((*code), r))
842 }
843 openapiv3::StatusCode::Range(range)
844 if *range >= 200 && *range < 300 =>
845 {
846 resp.as_item().map(|r| (200, r))
847 }
848 _ => None,
849 })
850 .next()
851 {
852 if serde_json::from_value::<Value>(final_response.clone()).is_err() {
854 tracing::warn!(
855 "Response validation failed: invalid JSON for status {}",
856 status_code
857 );
858 }
859 }
860 }
861
862 let mut trace = ResponseGenerationTrace::new();
864 trace.set_final_payload(final_response.clone());
865
866 if let Some((_status_code, response_ref)) = operation
868 .responses
869 .responses
870 .iter()
871 .filter_map(|(status, resp)| match status {
872 openapiv3::StatusCode::Code(code) if *code == selected_status => {
873 resp.as_item().map(|r| ((*code), r))
874 }
875 openapiv3::StatusCode::Range(range)
876 if *range >= 200 && *range < 300 =>
877 {
878 resp.as_item().map(|r| (200, r))
879 }
880 _ => None,
881 })
882 .next()
883 .or_else(|| {
884 operation
886 .responses
887 .responses
888 .iter()
889 .filter_map(|(status, resp)| match status {
890 openapiv3::StatusCode::Code(code)
891 if *code >= 200 && *code < 300 =>
892 {
893 resp.as_item().map(|r| ((*code), r))
894 }
895 _ => None,
896 })
897 .next()
898 })
899 {
900 let response_item = response_ref;
902 if let Some(content) = response_item.content.get("application/json") {
904 if let Some(schema_ref) = &content.schema {
905 if let Some(schema) = schema_ref.as_item() {
907 if let Ok(schema_json) = serde_json::to_value(schema) {
908 let validation_errors =
910 validation_diff(&schema_json, &final_response);
911 trace.set_schema_validation_diff(validation_errors);
912 }
913 }
914 }
915 }
916 }
917
918 let mut response = Json(final_response).into_response();
920 response.extensions_mut().insert(trace);
921 *response.status_mut() = axum::http::StatusCode::from_u16(selected_status)
922 .unwrap_or(axum::http::StatusCode::OK);
923 return response;
924 }
925
926 let mut response = Json(final_response).into_response();
928 *response.status_mut() = axum::http::StatusCode::from_u16(selected_status)
929 .unwrap_or(axum::http::StatusCode::OK);
930 response
931 };
932
933 router = Self::route_for_method(router, axum_path, &route.method, handler);
934 }
935
936 if ctx.add_spec_endpoint {
938 let spec_json = serde_json::to_value(&self.spec.spec).unwrap_or(Value::Null);
939 router = router.route("/openapi.json", get(move || async move { Json(spec_json) }));
940 }
941
942 router.layer(DefaultBodyLimit::max(openapi_body_limit_bytes()))
946 }
947
948 pub fn build_router_with_latency(self, latency_injector: LatencyInjector) -> Router {
950 self.build_router_with_injectors(latency_injector, None)
951 }
952
953 pub fn build_router_with_injectors(
955 self,
956 latency_injector: LatencyInjector,
957 failure_injector: Option<mockforge_foundation::failure_injection::FailureInjector>,
958 ) -> Router {
959 self.build_router_with_injectors_and_overrides(
960 latency_injector,
961 failure_injector,
962 None,
963 false,
964 )
965 }
966
967 pub fn build_router_with_injectors_and_overrides(
971 self,
972 latency_injector: LatencyInjector,
973 failure_injector: Option<mockforge_foundation::failure_injection::FailureInjector>,
974 response_rewriter: Option<Arc<dyn ResponseRewriter>>,
975 overrides_enabled: bool,
976 ) -> Router {
977 let ctx = RouterContext {
978 custom_fixture_loader: self.custom_fixture_loader.clone(),
979 latency_injector: Some(latency_injector),
980 failure_injector,
981 response_rewriter,
982 overrides_enabled,
983 enable_full_validation: true,
984 enable_template_expand: true,
985 add_spec_endpoint: true,
986 ..Default::default()
987 };
988 self.build_router_with_context(ctx)
989 }
990
991 pub fn get_route(&self, path: &str, method: &str) -> Option<&OpenApiRoute> {
993 self.routes.iter().find(|route| route.path == path && route.method == method)
994 }
995
996 pub fn get_routes_for_path(&self, path: &str) -> Vec<&OpenApiRoute> {
998 self.routes.iter().filter(|route| route.path == path).collect()
999 }
1000
1001 pub fn validate_request(&self, path: &str, method: &str, body: Option<&Value>) -> Result<()> {
1003 self.validate_request_with(path, method, &Map::new(), &Map::new(), body)
1004 }
1005
1006 pub fn validate_request_with(
1008 &self,
1009 path: &str,
1010 method: &str,
1011 path_params: &Map<String, Value>,
1012 query_params: &Map<String, Value>,
1013 body: Option<&Value>,
1014 ) -> Result<()> {
1015 self.validate_request_with_all(
1016 path,
1017 method,
1018 path_params,
1019 query_params,
1020 &Map::new(),
1021 &Map::new(),
1022 body,
1023 )
1024 }
1025
1026 #[allow(clippy::too_many_arguments)]
1039 pub fn run_validation_with_recording(
1040 &self,
1041 path_template: &str,
1042 method: &str,
1043 path_params: &Map<String, Value>,
1044 query_params: &Map<String, Value>,
1045 header_map: &Map<String, Value>,
1046 cookie_map: &Map<String, Value>,
1047 body: Option<&Value>,
1048 ) -> std::result::Result<(), (u16, Value)> {
1049 let e = match self.validate_request_with_all(
1050 path_template,
1051 method,
1052 path_params,
1053 query_params,
1054 header_map,
1055 cookie_map,
1056 body,
1057 ) {
1058 Ok(()) => return Ok(()),
1059 Err(e) => e,
1060 };
1061
1062 let status_code = self.options.validation_status.unwrap_or_else(|| {
1063 std::env::var("MOCKFORGE_VALIDATION_STATUS")
1064 .ok()
1065 .and_then(|s| s.parse::<u16>().ok())
1066 .unwrap_or(400)
1067 });
1068
1069 let payload = if status_code == 422 {
1070 generate_enhanced_422_response(
1071 self,
1072 path_template,
1073 method,
1074 body,
1075 path_params,
1076 query_params,
1077 header_map,
1078 cookie_map,
1079 )
1080 } else {
1081 let msg = format!("{}", e);
1082 let detail_val = serde_json::from_str::<Value>(&msg).unwrap_or(serde_json::json!(msg));
1083 json!({
1084 "error": "request validation failed",
1085 "detail": detail_val,
1086 "method": method,
1087 "path": path_template,
1088 "timestamp": Utc::now().to_rfc3339(),
1089 })
1090 };
1091
1092 record_validation_error(&payload);
1093
1094 let reason = payload
1095 .get("detail")
1096 .and_then(|d| {
1097 if d.is_string() {
1098 d.as_str().map(|s| s.to_string())
1099 } else {
1100 serde_json::to_string(d).ok()
1101 }
1102 })
1103 .unwrap_or_else(|| {
1104 payload
1105 .get("error")
1106 .and_then(|v| v.as_str())
1107 .unwrap_or("request validation failed")
1108 .to_string()
1109 });
1110 let category = classify_validation_reason(&reason);
1111 tracing::debug!(
1119 target: "mockforge::conformance",
1120 method = %method,
1121 path = %path_template,
1122 status = status_code,
1123 category = %category,
1124 reason = %reason,
1125 "request conformance violation"
1126 );
1127 mockforge_foundation::conformance_violations::record(
1128 mockforge_foundation::conformance_violations::ServerConformanceViolation {
1129 timestamp: Utc::now(),
1130 method: method.to_string(),
1131 path: path_template.to_string(),
1132 client_ip: "unknown".to_string(),
1133 status: status_code,
1134 reason,
1135 category,
1136 },
1137 );
1138
1139 if mockforge_foundation::unknown_paths::shadow_mode_enabled() {
1146 return Ok(());
1147 }
1148
1149 Err((status_code, payload))
1150 }
1151
1152 #[allow(clippy::too_many_arguments)]
1154 pub fn validate_request_with_all(
1155 &self,
1156 path: &str,
1157 method: &str,
1158 path_params: &Map<String, Value>,
1159 query_params: &Map<String, Value>,
1160 header_params: &Map<String, Value>,
1161 cookie_params: &Map<String, Value>,
1162 body: Option<&Value>,
1163 ) -> Result<()> {
1164 for pref in &self.options.admin_skip_prefixes {
1166 if !pref.is_empty() && path.starts_with(pref) {
1167 return Ok(());
1168 }
1169 }
1170 let env_mode = std::env::var("MOCKFORGE_REQUEST_VALIDATION").ok().map(|v| {
1172 match v.to_ascii_lowercase().as_str() {
1173 "off" | "disable" | "disabled" => ValidationMode::Disabled,
1174 "warn" | "warning" => ValidationMode::Warn,
1175 _ => ValidationMode::Enforce,
1176 }
1177 });
1178 let aggregate = std::env::var("MOCKFORGE_AGGREGATE_ERRORS")
1179 .ok()
1180 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1181 .unwrap_or(self.options.aggregate_errors);
1182 let env_overrides: Option<Map<String, Value>> =
1184 std::env::var("MOCKFORGE_VALIDATION_OVERRIDES_JSON")
1185 .ok()
1186 .and_then(|s| serde_json::from_str::<Value>(&s).ok())
1187 .and_then(|v| v.as_object().cloned());
1188 let mut effective_mode = env_mode.unwrap_or(self.options.request_mode.clone());
1190 if let Some(map) = &env_overrides {
1192 if let Some(v) = map.get(&format!("{} {}", method, path)) {
1193 if let Some(m) = v.as_str() {
1194 effective_mode = match m {
1195 "off" => ValidationMode::Disabled,
1196 "warn" => ValidationMode::Warn,
1197 _ => ValidationMode::Enforce,
1198 };
1199 }
1200 }
1201 }
1202 if let Some(override_mode) = self.options.overrides.get(&format!("{} {}", method, path)) {
1204 effective_mode = override_mode.clone();
1205 }
1206 if matches!(effective_mode, ValidationMode::Disabled) {
1207 return Ok(());
1208 }
1209 if let Some(route) = self.get_route(path, method) {
1210 if matches!(effective_mode, ValidationMode::Disabled) {
1211 return Ok(());
1212 }
1213 let mut errors: Vec<String> = Vec::new();
1214 let mut details: Vec<Value> = Vec::new();
1215 if let Some(schema) = &route.operation.request_body {
1217 if let Some(value) = body {
1218 let request_body = match schema {
1220 openapiv3::ReferenceOr::Item(rb) => Some(rb),
1221 openapiv3::ReferenceOr::Reference { reference } => {
1222 self.spec
1224 .spec
1225 .components
1226 .as_ref()
1227 .and_then(|components| {
1228 components.request_bodies.get(
1229 reference.trim_start_matches("#/components/requestBodies/"),
1230 )
1231 })
1232 .and_then(|rb_ref| rb_ref.as_item())
1233 }
1234 };
1235
1236 if let Some(rb) = request_body {
1237 if let Some(content) = rb.content.get("application/json") {
1238 if let Some(schema_ref) = &content.schema {
1239 match schema_ref {
1241 openapiv3::ReferenceOr::Item(schema) => {
1242 if let Err(validation_error) =
1244 OpenApiSchema::new(schema.clone()).validate(value)
1245 {
1246 let error_msg = validation_error.to_string();
1247 errors.push(format!(
1248 "body validation failed: {}",
1249 error_msg
1250 ));
1251 if aggregate {
1252 details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
1253 }
1254 }
1255 }
1256 openapiv3::ReferenceOr::Reference { reference } => {
1257 if let Some(resolved_schema_ref) =
1259 self.spec.get_schema(reference)
1260 {
1261 if let Err(validation_error) = OpenApiSchema::new(
1262 resolved_schema_ref.schema.clone(),
1263 )
1264 .validate(value)
1265 {
1266 let error_msg = validation_error.to_string();
1267 errors.push(format!(
1268 "body validation failed: {}",
1269 error_msg
1270 ));
1271 if aggregate {
1272 details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
1273 }
1274 }
1275 } else {
1276 errors.push(format!("body validation failed: could not resolve schema reference {}", reference));
1278 if aggregate {
1279 details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve schema reference"}));
1280 }
1281 }
1282 }
1283 }
1284 }
1285 }
1286 } else {
1287 errors.push("body validation failed: could not resolve request body or no application/json content".to_string());
1289 if aggregate {
1290 details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve request body reference"}));
1291 }
1292 }
1293 } else {
1294 errors.push("body: Request body is required but not provided".to_string());
1295 details.push(serde_json::json!({"path":"body","code":"required","message":"Request body is required"}));
1296 }
1297 } else if body.is_some() {
1298 tracing::debug!("Body provided for operation without requestBody; accepting");
1300 }
1301
1302 for p_ref in &route.operation.parameters {
1304 if let Some(p) = p_ref.as_item() {
1305 match p {
1306 openapiv3::Parameter::Path { parameter_data, .. } => {
1307 validate_parameter(
1308 parameter_data,
1309 path_params,
1310 "path",
1311 aggregate,
1312 &mut errors,
1313 &mut details,
1314 );
1315 }
1316 openapiv3::Parameter::Query {
1317 parameter_data,
1318 style,
1319 ..
1320 } => {
1321 let deep_value = if matches!(style, openapiv3::QueryStyle::DeepObject) {
1324 let prefix_bracket = format!("{}[", parameter_data.name);
1325 let mut obj = Map::new();
1326 for (key, val) in query_params.iter() {
1327 if let Some(rest) = key.strip_prefix(&prefix_bracket) {
1328 if let Some(prop) = rest.strip_suffix(']') {
1329 obj.insert(prop.to_string(), val.clone());
1330 }
1331 }
1332 }
1333 if obj.is_empty() {
1334 None
1335 } else {
1336 Some(Value::Object(obj))
1337 }
1338 } else {
1339 None
1340 };
1341 let style_str = match style {
1342 openapiv3::QueryStyle::Form => Some("form"),
1343 openapiv3::QueryStyle::SpaceDelimited => Some("spaceDelimited"),
1344 openapiv3::QueryStyle::PipeDelimited => Some("pipeDelimited"),
1345 openapiv3::QueryStyle::DeepObject => Some("deepObject"),
1346 };
1347 validate_parameter_with_deep_object(
1348 parameter_data,
1349 query_params,
1350 "query",
1351 deep_value,
1352 style_str,
1353 aggregate,
1354 &mut errors,
1355 &mut details,
1356 );
1357 }
1358 openapiv3::Parameter::Header { parameter_data, .. } => {
1359 validate_parameter(
1360 parameter_data,
1361 header_params,
1362 "header",
1363 aggregate,
1364 &mut errors,
1365 &mut details,
1366 );
1367 }
1368 openapiv3::Parameter::Cookie { parameter_data, .. } => {
1369 validate_parameter(
1370 parameter_data,
1371 cookie_params,
1372 "cookie",
1373 aggregate,
1374 &mut errors,
1375 &mut details,
1376 );
1377 }
1378 }
1379 }
1380 }
1381 if errors.is_empty() {
1382 return Ok(());
1383 }
1384 match effective_mode {
1385 ValidationMode::Disabled => Ok(()),
1386 ValidationMode::Warn => {
1387 tracing::warn!("Request validation warnings: {:?}", errors);
1388 Ok(())
1389 }
1390 ValidationMode::Enforce => Err(Error::validation(
1391 serde_json::json!({"errors": errors, "details": details}).to_string(),
1392 )),
1393 }
1394 } else {
1395 Err(Error::internal(format!("Route {} {} not found in OpenAPI spec", method, path)))
1396 }
1397 }
1398
1399 pub fn paths(&self) -> Vec<String> {
1403 let mut paths: Vec<String> = self.routes.iter().map(|route| route.path.clone()).collect();
1404 paths.sort();
1405 paths.dedup();
1406 paths
1407 }
1408
1409 pub fn methods(&self) -> Vec<String> {
1411 let mut methods: Vec<String> =
1412 self.routes.iter().map(|route| route.method.clone()).collect();
1413 methods.sort();
1414 methods.dedup();
1415 methods
1416 }
1417
1418 pub fn get_operation(&self, path: &str, method: &str) -> Option<OpenApiOperation> {
1420 self.get_route(path, method).map(|route| {
1421 OpenApiOperation::from_operation(
1422 &route.method,
1423 route.path.clone(),
1424 &route.operation,
1425 &self.spec,
1426 )
1427 })
1428 }
1429
1430 pub fn extract_path_parameters(&self, path: &str, method: &str) -> HashMap<String, String> {
1432 for route in &self.routes {
1433 if route.method != method {
1434 continue;
1435 }
1436
1437 if let Some(params) = self.match_path_to_route(path, &route.path) {
1438 return params;
1439 }
1440 }
1441 HashMap::new()
1442 }
1443
1444 fn match_path_to_route(
1446 &self,
1447 request_path: &str,
1448 route_pattern: &str,
1449 ) -> Option<HashMap<String, String>> {
1450 let mut params = HashMap::new();
1451
1452 let request_segments: Vec<&str> = request_path.trim_start_matches('/').split('/').collect();
1454 let pattern_segments: Vec<&str> =
1455 route_pattern.trim_start_matches('/').split('/').collect();
1456
1457 if request_segments.len() != pattern_segments.len() {
1458 return None;
1459 }
1460
1461 for (req_seg, pat_seg) in request_segments.iter().zip(pattern_segments.iter()) {
1462 if pat_seg.starts_with('{') && pat_seg.ends_with('}') {
1463 let param_name = &pat_seg[1..pat_seg.len() - 1];
1465 params.insert(param_name.to_string(), req_seg.to_string());
1466 } else if req_seg != pat_seg {
1467 return None;
1469 }
1470 }
1471
1472 Some(params)
1473 }
1474
1475 pub fn convert_path_to_axum(openapi_path: &str) -> String {
1478 openapi_path.to_string()
1480 }
1481
1482 pub fn build_router_with_ai(
1484 &self,
1485 ai_generator: Option<Arc<dyn AiGenerator + Send + Sync>>,
1486 ) -> Router {
1487 let mut router = Router::new();
1488 let deduped = self.deduplicated_routes();
1489 tracing::debug!("Building router with AI support from {} routes", self.routes.len());
1490
1491 let validator = Arc::new(self.clone_for_validation());
1495 for (axum_path, route) in &deduped {
1496 tracing::debug!("Adding AI-enabled route: {} {}", route.method, route.path);
1497
1498 let route_clone = (*route).clone();
1499 let ai_generator_clone = ai_generator.clone();
1500 let validator_clone = validator.clone();
1504
1505 let handler = move |AxumPath(path_params): AxumPath<HashMap<String, String>>,
1507 axum::extract::Query(query_params): axum::extract::Query<
1508 HashMap<String, String>,
1509 >,
1510 headers: HeaderMap,
1511 body: Option<Json<Value>>| {
1512 let route = route_clone.clone();
1513 let ai_generator = ai_generator_clone.clone();
1514 let validator = validator_clone.clone();
1515
1516 async move {
1517 let mut path_map = Map::new();
1522 for (k, v) in &path_params {
1523 path_map.insert(k.clone(), Value::String(v.clone()));
1524 }
1525 let mut query_map = Map::new();
1526 for (k, v) in &query_params {
1527 query_map.insert(k.clone(), Value::String(v.clone()));
1528 }
1529 let mut header_map = Map::new();
1530 for (k, v) in headers.iter() {
1531 if let Ok(s) = v.to_str() {
1532 header_map.insert(k.to_string(), Value::String(s.to_string()));
1533 }
1534 }
1535 let body_val: Option<&Value> = body.as_ref().map(|Json(b)| b);
1536 if let Err((status_code, payload)) = validator.run_validation_with_recording(
1537 &route.path,
1538 &route.method,
1539 &path_map,
1540 &query_map,
1541 &header_map,
1542 &Map::new(),
1543 body_val,
1544 ) {
1545 let status = axum::http::StatusCode::from_u16(status_code)
1546 .unwrap_or(axum::http::StatusCode::BAD_REQUEST);
1547 return (status, Json(payload));
1548 }
1549
1550 tracing::debug!(
1551 "Handling AI request for route: {} {}",
1552 route.method,
1553 route.path
1554 );
1555
1556 let mut context = RequestContext::new(route.method.clone(), route.path.clone());
1558
1559 context.headers = headers
1561 .iter()
1562 .map(|(k, v)| {
1563 (k.to_string(), Value::String(v.to_str().unwrap_or("").to_string()))
1564 })
1565 .collect();
1566
1567 context.body = body.map(|Json(b)| b);
1569
1570 let (status, response) = if let (Some(generator), Some(_ai_config)) =
1572 (ai_generator, &route.ai_config)
1573 {
1574 route
1575 .mock_response_with_status_async(&context, Some(generator.as_ref()))
1576 .await
1577 } else {
1578 route.mock_response_with_status()
1580 };
1581
1582 (
1583 axum::http::StatusCode::from_u16(status)
1584 .unwrap_or(axum::http::StatusCode::OK),
1585 Json(response),
1586 )
1587 }
1588 };
1589
1590 router = Self::route_for_method(router, axum_path, &route.method, handler);
1591 }
1592
1593 router.layer(DefaultBodyLimit::max(openapi_body_limit_bytes()))
1597 }
1598
1599 pub fn build_router_with_mockai(
1610 &self,
1611 mockai: Option<
1612 Arc<
1613 tokio::sync::RwLock<
1614 dyn mockforge_foundation::intelligent_behavior::MockAiBehavior + Send + Sync,
1615 >,
1616 >,
1617 >,
1618 ) -> Router {
1619 use mockforge_foundation::intelligent_behavior::Request as MockAIRequest;
1620
1621 let mut router = Router::new();
1622 let deduped = self.deduplicated_routes();
1623 tracing::debug!("Building router with MockAI support from {} routes", self.routes.len());
1624
1625 let custom_loader = self.custom_fixture_loader.clone();
1626 let validator = Arc::new(self.clone_for_validation());
1630 for (axum_path, route) in &deduped {
1631 tracing::debug!("Adding MockAI-enabled route: {} {}", route.method, route.path);
1632
1633 let route_clone = (*route).clone();
1634 let mockai_clone = mockai.clone();
1635 let custom_loader_clone = custom_loader.clone();
1636 let validator_clone = validator.clone();
1642
1643 let handler = move |AxumPath(path_params): AxumPath<HashMap<String, String>>,
1647 query: axum::extract::Query<HashMap<String, String>>,
1648 headers: HeaderMap,
1649 body: Option<Json<Value>>| {
1650 let route = route_clone.clone();
1651 let mockai = mockai_clone.clone();
1652 let validator = validator_clone.clone();
1653
1654 async move {
1655 let mut path_map = Map::new();
1660 for (k, v) in &path_params {
1661 path_map.insert(k.clone(), Value::String(v.clone()));
1662 }
1663 let mut query_map = Map::new();
1664 for (k, v) in &query.0 {
1665 query_map.insert(k.clone(), Value::String(v.clone()));
1666 }
1667 let mut header_map = Map::new();
1668 for (k, v) in headers.iter() {
1669 if let Ok(s) = v.to_str() {
1670 header_map.insert(k.to_string(), Value::String(s.to_string()));
1671 }
1672 }
1673 let body_val: Option<&Value> = body.as_ref().map(|Json(b)| b);
1674 if let Err((status_code, payload)) = validator.run_validation_with_recording(
1675 &route.path,
1676 &route.method,
1677 &path_map,
1678 &query_map,
1679 &header_map,
1680 &Map::new(),
1681 body_val,
1682 ) {
1683 let status = axum::http::StatusCode::from_u16(status_code)
1684 .unwrap_or(axum::http::StatusCode::BAD_REQUEST);
1685 return (status, Json(payload));
1686 }
1687
1688 tracing::info!(
1689 "[FIXTURE DEBUG] Starting fixture check for {} {} (custom_loader available: {})",
1690 route.method,
1691 route.path,
1692 custom_loader_clone.is_some()
1693 );
1694
1695 if let Some(ref loader) = custom_loader_clone {
1697 use crate::request_fingerprint::RequestFingerprint;
1698 use axum::http::{Method, Uri};
1699
1700 let query_string = if query.0.is_empty() {
1702 String::new()
1703 } else {
1704 query
1705 .0
1706 .iter()
1707 .map(|(k, v)| format!("{}={}", k, v))
1708 .collect::<Vec<_>>()
1709 .join("&")
1710 };
1711
1712 let normalized_request_path =
1714 crate::custom_fixture::CustomFixtureLoader::normalize_path(&route.path);
1715
1716 tracing::info!(
1717 "[FIXTURE DEBUG] Path normalization: original='{}', normalized='{}'",
1718 route.path,
1719 normalized_request_path
1720 );
1721
1722 let uri_str = if query_string.is_empty() {
1724 normalized_request_path.clone()
1725 } else {
1726 format!("{}?{}", normalized_request_path, query_string)
1727 };
1728
1729 tracing::info!(
1730 "[FIXTURE DEBUG] URI construction: uri_str='{}', query_string='{}'",
1731 uri_str,
1732 query_string
1733 );
1734
1735 if let Ok(uri) = uri_str.parse::<Uri>() {
1736 let http_method =
1737 Method::from_bytes(route.method.as_bytes()).unwrap_or(Method::GET);
1738
1739 let body_bytes =
1741 body.as_ref().and_then(|Json(b)| serde_json::to_vec(b).ok());
1742 let body_slice = body_bytes.as_deref();
1743
1744 let fingerprint =
1745 RequestFingerprint::new(http_method, &uri, &headers, body_slice);
1746
1747 tracing::info!(
1748 "[FIXTURE DEBUG] RequestFingerprint created: method='{}', path='{}', query='{}', body_hash={:?}",
1749 fingerprint.method,
1750 fingerprint.path,
1751 fingerprint.query,
1752 fingerprint.body_hash
1753 );
1754
1755 let available_fixtures = loader.has_fixture(&fingerprint);
1757 tracing::info!(
1758 "[FIXTURE DEBUG] Fixture check result: has_fixture={}",
1759 available_fixtures
1760 );
1761
1762 if let Some(custom_fixture) = loader.load_fixture(&fingerprint) {
1763 tracing::info!(
1764 "[FIXTURE DEBUG] ✅ FIXTURE MATCHED! Using custom fixture for {} {} (status: {}, path: '{}')",
1765 route.method,
1766 route.path,
1767 custom_fixture.status,
1768 custom_fixture.path
1769 );
1770
1771 if custom_fixture.delay_ms > 0 {
1773 tokio::time::sleep(tokio::time::Duration::from_millis(
1774 custom_fixture.delay_ms,
1775 ))
1776 .await;
1777 }
1778
1779 let response_body = if custom_fixture.response.is_string() {
1781 custom_fixture.response.as_str().unwrap().to_string()
1782 } else {
1783 serde_json::to_string(&custom_fixture.response)
1784 .unwrap_or_else(|_| "{}".to_string())
1785 };
1786
1787 let json_value: Value = serde_json::from_str(&response_body)
1789 .unwrap_or_else(|_| serde_json::json!({}));
1790
1791 let status =
1793 axum::http::StatusCode::from_u16(custom_fixture.status)
1794 .unwrap_or(axum::http::StatusCode::OK);
1795
1796 return (status, Json(json_value));
1798 } else {
1799 tracing::warn!(
1800 "[FIXTURE DEBUG] ❌ No fixture match found for {} {} (fingerprint.path='{}', normalized='{}')",
1801 route.method,
1802 route.path,
1803 fingerprint.path,
1804 normalized_request_path
1805 );
1806 }
1807 } else {
1808 tracing::warn!("[FIXTURE DEBUG] Failed to parse URI: '{}'", uri_str);
1809 }
1810 } else {
1811 tracing::warn!(
1812 "[FIXTURE DEBUG] Custom fixture loader not available for {} {}",
1813 route.method,
1814 route.path
1815 );
1816 }
1817
1818 tracing::debug!(
1819 "Handling MockAI request for route: {} {}",
1820 route.method,
1821 route.path
1822 );
1823
1824 let mockai_query = query.0;
1826
1827 let method_upper = route.method.to_uppercase();
1832 let should_use_mockai =
1833 matches!(method_upper.as_str(), "POST" | "PUT" | "PATCH" | "DELETE");
1834
1835 if should_use_mockai {
1836 if let Some(mockai_arc) = mockai {
1837 let mockai_guard = mockai_arc.read().await;
1838
1839 let mut mockai_headers = HashMap::new();
1841 for (k, v) in headers.iter() {
1842 mockai_headers
1843 .insert(k.to_string(), v.to_str().unwrap_or("").to_string());
1844 }
1845
1846 let mockai_request = MockAIRequest {
1847 method: route.method.clone(),
1848 path: route.path.clone(),
1849 body: body.as_ref().map(|Json(b)| b.clone()),
1850 query_params: mockai_query,
1851 headers: mockai_headers,
1852 };
1853
1854 match mockai_guard.process_request(&mockai_request).await {
1856 Ok(mockai_response) => {
1857 let is_empty = mockai_response.body.is_object()
1859 && mockai_response
1860 .body
1861 .as_object()
1862 .map(|obj| obj.is_empty())
1863 .unwrap_or(false);
1864
1865 if is_empty {
1866 tracing::debug!(
1867 "MockAI returned empty object for {} {}, falling back to OpenAPI response generation",
1868 route.method,
1869 route.path
1870 );
1871 } else {
1873 let spec_status = route.find_first_available_status_code();
1877 tracing::debug!(
1878 "MockAI generated response for {} {}, using spec status: {} (MockAI suggested: {})",
1879 route.method,
1880 route.path,
1881 spec_status,
1882 mockai_response.status_code
1883 );
1884 return (
1885 axum::http::StatusCode::from_u16(spec_status)
1886 .unwrap_or(axum::http::StatusCode::OK),
1887 Json(mockai_response.body),
1888 );
1889 }
1890 }
1891 Err(e) => {
1892 tracing::warn!(
1893 "MockAI processing failed for {} {}: {}, falling back to standard response",
1894 route.method,
1895 route.path,
1896 e
1897 );
1898 }
1900 }
1901 }
1902 } else {
1903 tracing::debug!(
1904 "Skipping MockAI for {} request {} - using OpenAPI response generation",
1905 method_upper,
1906 route.path
1907 );
1908 }
1909
1910 let status_override = headers
1912 .get("X-Mockforge-Response-Status")
1913 .and_then(|v| v.to_str().ok())
1914 .and_then(|s| s.parse::<u16>().ok());
1915
1916 let scenario = headers
1918 .get("X-Mockforge-Scenario")
1919 .and_then(|v| v.to_str().ok())
1920 .map(|s| s.to_string())
1921 .or_else(|| std::env::var("MOCKFORGE_HTTP_SCENARIO").ok());
1922
1923 let (status, response) = route
1925 .mock_response_with_status_and_scenario_and_override(
1926 scenario.as_deref(),
1927 status_override,
1928 );
1929 (
1930 axum::http::StatusCode::from_u16(status)
1931 .unwrap_or(axum::http::StatusCode::OK),
1932 Json(response),
1933 )
1934 }
1935 };
1936
1937 router = Self::route_for_method(router, axum_path, &route.method, handler);
1938 }
1939
1940 router.layer(DefaultBodyLimit::max(openapi_body_limit_bytes()))
1943 }
1944}
1945
1946async fn extract_multipart_from_bytes(
1951 body: &axum::body::Bytes,
1952 headers: &HeaderMap,
1953) -> Result<(HashMap<String, Value>, HashMap<String, String>)> {
1954 let boundary = headers
1956 .get(axum::http::header::CONTENT_TYPE)
1957 .and_then(|v| v.to_str().ok())
1958 .and_then(|ct| {
1959 ct.split(';').find_map(|part| {
1960 let part = part.trim();
1961 if part.starts_with("boundary=") {
1962 Some(part.strip_prefix("boundary=").unwrap_or("").trim_matches('"'))
1963 } else {
1964 None
1965 }
1966 })
1967 })
1968 .ok_or_else(|| Error::internal("Missing boundary in Content-Type header"))?;
1969
1970 let mut fields = HashMap::new();
1971 let mut files = HashMap::new();
1972
1973 let boundary_prefix = format!("--{}", boundary).into_bytes();
1976 let boundary_line = format!("\r\n--{}\r\n", boundary).into_bytes();
1977 let end_boundary = format!("\r\n--{}--\r\n", boundary).into_bytes();
1978
1979 let mut pos = 0;
1981 let mut parts = Vec::new();
1982
1983 if body.starts_with(&boundary_prefix) {
1985 if let Some(first_crlf) = body.iter().position(|&b| b == b'\r') {
1986 pos = first_crlf + 2; }
1988 }
1989
1990 while let Some(boundary_pos) = body[pos..]
1992 .windows(boundary_line.len())
1993 .position(|window| window == boundary_line.as_slice())
1994 {
1995 let actual_pos = pos + boundary_pos;
1996 if actual_pos > pos {
1997 parts.push((pos, actual_pos));
1998 }
1999 pos = actual_pos + boundary_line.len();
2000 }
2001
2002 if let Some(end_pos) = body[pos..]
2004 .windows(end_boundary.len())
2005 .position(|window| window == end_boundary.as_slice())
2006 {
2007 let actual_end = pos + end_pos;
2008 if actual_end > pos {
2009 parts.push((pos, actual_end));
2010 }
2011 } else if pos < body.len() {
2012 parts.push((pos, body.len()));
2014 }
2015
2016 for (start, end) in parts {
2018 let part_data = &body[start..end];
2019
2020 let separator = b"\r\n\r\n";
2022 if let Some(sep_pos) =
2023 part_data.windows(separator.len()).position(|window| window == separator)
2024 {
2025 let header_bytes = &part_data[..sep_pos];
2026 let body_start = sep_pos + separator.len();
2027 let body_data = &part_data[body_start..];
2028
2029 let header_str = String::from_utf8_lossy(header_bytes);
2031 let mut field_name = None;
2032 let mut filename = None;
2033
2034 for header_line in header_str.lines() {
2035 if header_line.starts_with("Content-Disposition:") {
2036 if let Some(name_start) = header_line.find("name=\"") {
2038 let name_start = name_start + 6;
2039 if let Some(name_end) = header_line[name_start..].find('"') {
2040 field_name =
2041 Some(header_line[name_start..name_start + name_end].to_string());
2042 }
2043 }
2044
2045 if let Some(file_start) = header_line.find("filename=\"") {
2047 let file_start = file_start + 10;
2048 if let Some(file_end) = header_line[file_start..].find('"') {
2049 filename =
2050 Some(header_line[file_start..file_start + file_end].to_string());
2051 }
2052 }
2053 }
2054 }
2055
2056 if let Some(name) = field_name {
2057 if let Some(file) = filename {
2058 let temp_dir = std::env::temp_dir().join("mockforge-uploads");
2060 std::fs::create_dir_all(&temp_dir)
2061 .map_err(|e| Error::io_with_context("temp directory", e.to_string()))?;
2062
2063 let file_path = temp_dir.join(format!("{}_{}", uuid::Uuid::new_v4(), file));
2064 std::fs::write(&file_path, body_data)
2065 .map_err(|e| Error::io_with_context("file", e.to_string()))?;
2066
2067 let file_path_str = file_path.to_string_lossy().to_string();
2068 files.insert(name.clone(), file_path_str.clone());
2069 fields.insert(name, Value::String(file_path_str));
2070 } else {
2071 let body_str = body_data
2074 .strip_suffix(b"\r\n")
2075 .or_else(|| body_data.strip_suffix(b"\n"))
2076 .unwrap_or(body_data);
2077
2078 if let Ok(field_value) = String::from_utf8(body_str.to_vec()) {
2079 fields.insert(name, Value::String(field_value.trim().to_string()));
2080 } else {
2081 use base64::{engine::general_purpose, Engine as _};
2083 fields.insert(
2084 name,
2085 Value::String(general_purpose::STANDARD.encode(body_str)),
2086 );
2087 }
2088 }
2089 }
2090 }
2091 }
2092
2093 Ok((fields, files))
2094}
2095
2096static LAST_ERRORS: Lazy<Mutex<VecDeque<Value>>> =
2097 Lazy::new(|| Mutex::new(VecDeque::with_capacity(20)));
2098
2099pub fn classify_validation_reason(reason: &str) -> String {
2107 let r = reason.to_ascii_lowercase();
2108 if r.contains("required")
2109 && (r.contains("param") || r.contains("query") || r.contains("header"))
2110 {
2111 return "parameters".into();
2112 }
2113 if r.contains("schema") || r.contains("body") || r.contains("json") {
2114 return "request-body".into();
2115 }
2116 if r.contains("content-type") || r.contains("content type") {
2117 return "content-types".into();
2118 }
2119 if r.contains("header") {
2120 return "headers".into();
2121 }
2122 if r.contains("cookie") {
2123 return "cookies".into();
2124 }
2125 if r.contains("method") {
2126 return "http-methods".into();
2127 }
2128 if r.contains("auth") || r.contains("security") {
2129 return "security".into();
2130 }
2131 if r.contains("enum") || r.contains("min") || r.contains("max") || r.contains("pattern") {
2132 return "constraints".into();
2133 }
2134 String::new()
2135}
2136
2137pub fn record_validation_error(v: &Value) {
2139 if let Ok(mut q) = LAST_ERRORS.lock() {
2140 if q.len() >= 20 {
2141 q.pop_front();
2142 }
2143 q.push_back(v.clone());
2144 }
2145 }
2147
2148pub fn get_last_validation_error() -> Option<Value> {
2150 LAST_ERRORS.lock().ok()?.back().cloned()
2151}
2152
2153pub fn get_validation_errors() -> Vec<Value> {
2155 LAST_ERRORS.lock().map(|q| q.iter().cloned().collect()).unwrap_or_default()
2156}
2157
2158fn coerce_value_for_schema(value: &Value, schema: &openapiv3::Schema) -> Value {
2163 match value {
2165 Value::String(s) => {
2166 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
2168 &schema.schema_kind
2169 {
2170 if s.contains(',') {
2171 let parts: Vec<&str> = s.split(',').map(|s| s.trim()).collect();
2173 let mut array_values = Vec::new();
2174
2175 for part in parts {
2176 if let Some(items_schema) = &array_type.items {
2178 if let Some(items_schema_obj) = items_schema.as_item() {
2179 let part_value = Value::String(part.to_string());
2180 let coerced_part =
2181 coerce_value_for_schema(&part_value, items_schema_obj);
2182 array_values.push(coerced_part);
2183 } else {
2184 array_values.push(Value::String(part.to_string()));
2186 }
2187 } else {
2188 array_values.push(Value::String(part.to_string()));
2190 }
2191 }
2192 return Value::Array(array_values);
2193 }
2194 }
2195
2196 match &schema.schema_kind {
2198 openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
2199 value.clone()
2201 }
2202 openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => {
2203 if let Ok(n) = s.parse::<f64>() {
2205 if let Some(num) = serde_json::Number::from_f64(n) {
2206 return Value::Number(num);
2207 }
2208 }
2209 value.clone()
2210 }
2211 openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => {
2212 if let Ok(n) = s.parse::<i64>() {
2214 if let Some(num) = serde_json::Number::from_f64(n as f64) {
2215 return Value::Number(num);
2216 }
2217 }
2218 value.clone()
2219 }
2220 openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => {
2221 match s.to_lowercase().as_str() {
2223 "true" | "1" | "yes" | "on" => Value::Bool(true),
2224 "false" | "0" | "no" | "off" => Value::Bool(false),
2225 _ => value.clone(),
2226 }
2227 }
2228 _ => {
2229 value.clone()
2231 }
2232 }
2233 }
2234 _ => value.clone(),
2235 }
2236}
2237
2238fn coerce_by_style(value: &Value, schema: &openapiv3::Schema, style: Option<&str>) -> Value {
2240 match value {
2242 Value::String(s) => {
2243 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
2245 &schema.schema_kind
2246 {
2247 let delimiter = match style {
2248 Some("spaceDelimited") => " ",
2249 Some("pipeDelimited") => "|",
2250 Some("form") | None => ",", _ => ",", };
2253
2254 if s.contains(delimiter) {
2255 let parts: Vec<&str> = s.split(delimiter).map(|s| s.trim()).collect();
2257 let mut array_values = Vec::new();
2258
2259 for part in parts {
2260 if let Some(items_schema) = &array_type.items {
2262 if let Some(items_schema_obj) = items_schema.as_item() {
2263 let part_value = Value::String(part.to_string());
2264 let coerced_part =
2265 coerce_by_style(&part_value, items_schema_obj, style);
2266 array_values.push(coerced_part);
2267 } else {
2268 array_values.push(Value::String(part.to_string()));
2270 }
2271 } else {
2272 array_values.push(Value::String(part.to_string()));
2274 }
2275 }
2276 return Value::Array(array_values);
2277 }
2278 }
2279
2280 if let Ok(n) = s.parse::<f64>() {
2282 if let Some(num) = serde_json::Number::from_f64(n) {
2283 return Value::Number(num);
2284 }
2285 }
2286 match s.to_lowercase().as_str() {
2288 "true" | "1" | "yes" | "on" => return Value::Bool(true),
2289 "false" | "0" | "no" | "off" => return Value::Bool(false),
2290 _ => {}
2291 }
2292 value.clone()
2294 }
2295 _ => value.clone(),
2296 }
2297}
2298
2299fn build_deep_object(name: &str, params: &Map<String, Value>) -> Option<Value> {
2301 let prefix = format!("{}[", name);
2302 let mut obj = Map::new();
2303 for (k, v) in params.iter() {
2304 if let Some(rest) = k.strip_prefix(&prefix) {
2305 if let Some(key) = rest.strip_suffix(']') {
2306 obj.insert(key.to_string(), v.clone());
2307 }
2308 }
2309 }
2310 if obj.is_empty() {
2311 None
2312 } else {
2313 Some(Value::Object(obj))
2314 }
2315}
2316
2317#[allow(clippy::too_many_arguments)]
2323fn generate_enhanced_422_response(
2324 validator: &OpenApiRouteRegistry,
2325 path_template: &str,
2326 method: &str,
2327 body: Option<&Value>,
2328 path_params: &Map<String, Value>,
2329 query_params: &Map<String, Value>,
2330 header_params: &Map<String, Value>,
2331 cookie_params: &Map<String, Value>,
2332) -> Value {
2333 let mut field_errors = Vec::new();
2334
2335 if let Some(route) = validator.get_route(path_template, method) {
2337 if let Some(schema) = &route.operation.request_body {
2339 if let Some(value) = body {
2340 if let Some(content) =
2341 schema.as_item().and_then(|rb| rb.content.get("application/json"))
2342 {
2343 if let Some(_schema_ref) = &content.schema {
2344 if serde_json::from_value::<Value>(value.clone()).is_err() {
2346 field_errors.push(json!({
2347 "path": "body",
2348 "message": "invalid JSON"
2349 }));
2350 }
2351 }
2352 }
2353 } else {
2354 field_errors.push(json!({
2355 "path": "body",
2356 "expected": "object",
2357 "found": "missing",
2358 "message": "Request body is required but not provided"
2359 }));
2360 }
2361 }
2362
2363 for param_ref in &route.operation.parameters {
2365 if let Some(param) = param_ref.as_item() {
2366 match param {
2367 openapiv3::Parameter::Path { parameter_data, .. } => {
2368 validate_parameter_detailed(
2369 parameter_data,
2370 path_params,
2371 "path",
2372 "path parameter",
2373 &mut field_errors,
2374 );
2375 }
2376 openapiv3::Parameter::Query { parameter_data, .. } => {
2377 let deep_value = if Some("form") == Some("deepObject") {
2378 build_deep_object(¶meter_data.name, query_params)
2379 } else {
2380 None
2381 };
2382 validate_parameter_detailed_with_deep(
2383 parameter_data,
2384 query_params,
2385 "query",
2386 "query parameter",
2387 deep_value,
2388 &mut field_errors,
2389 );
2390 }
2391 openapiv3::Parameter::Header { parameter_data, .. } => {
2392 validate_parameter_detailed(
2393 parameter_data,
2394 header_params,
2395 "header",
2396 "header parameter",
2397 &mut field_errors,
2398 );
2399 }
2400 openapiv3::Parameter::Cookie { parameter_data, .. } => {
2401 validate_parameter_detailed(
2402 parameter_data,
2403 cookie_params,
2404 "cookie",
2405 "cookie parameter",
2406 &mut field_errors,
2407 );
2408 }
2409 }
2410 }
2411 }
2412 }
2413
2414 json!({
2416 "error": "Schema validation failed",
2417 "details": field_errors,
2418 "method": method,
2419 "path": path_template,
2420 "timestamp": Utc::now().to_rfc3339(),
2421 "validation_type": "openapi_schema"
2422 })
2423}
2424
2425fn validate_parameter(
2427 parameter_data: &openapiv3::ParameterData,
2428 params_map: &Map<String, Value>,
2429 prefix: &str,
2430 aggregate: bool,
2431 errors: &mut Vec<String>,
2432 details: &mut Vec<Value>,
2433) {
2434 match params_map.get(¶meter_data.name) {
2435 Some(v) => {
2436 if let ParameterSchemaOrContent::Schema(s) = ¶meter_data.format {
2437 if let Some(schema) = s.as_item() {
2438 let coerced = coerce_value_for_schema(v, schema);
2439 if let Err(validation_error) =
2441 OpenApiSchema::new(schema.clone()).validate(&coerced)
2442 {
2443 let error_msg = validation_error.to_string();
2444 errors.push(format!(
2445 "{} parameter '{}' validation failed: {}",
2446 prefix, parameter_data.name, error_msg
2447 ));
2448 if aggregate {
2449 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
2450 }
2451 }
2452 }
2453 }
2454 }
2455 None => {
2456 if parameter_data.required {
2457 errors.push(format!(
2458 "missing required {} parameter '{}'",
2459 prefix, parameter_data.name
2460 ));
2461 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
2462 }
2463 }
2464 }
2465}
2466
2467#[allow(clippy::too_many_arguments)]
2469fn validate_parameter_with_deep_object(
2470 parameter_data: &openapiv3::ParameterData,
2471 params_map: &Map<String, Value>,
2472 prefix: &str,
2473 deep_value: Option<Value>,
2474 style: Option<&str>,
2475 aggregate: bool,
2476 errors: &mut Vec<String>,
2477 details: &mut Vec<Value>,
2478) {
2479 match deep_value.as_ref().or_else(|| params_map.get(¶meter_data.name)) {
2480 Some(v) => {
2481 if let ParameterSchemaOrContent::Schema(s) = ¶meter_data.format {
2482 if let Some(schema) = s.as_item() {
2483 let coerced = coerce_by_style(v, schema, style); if let Err(validation_error) =
2486 OpenApiSchema::new(schema.clone()).validate(&coerced)
2487 {
2488 let error_msg = validation_error.to_string();
2489 errors.push(format!(
2490 "{} parameter '{}' validation failed: {}",
2491 prefix, parameter_data.name, error_msg
2492 ));
2493 if aggregate {
2494 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
2495 }
2496 }
2497 }
2498 }
2499 }
2500 None => {
2501 if parameter_data.required {
2502 errors.push(format!(
2503 "missing required {} parameter '{}'",
2504 prefix, parameter_data.name
2505 ));
2506 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
2507 }
2508 }
2509 }
2510}
2511
2512fn validate_parameter_detailed(
2514 parameter_data: &openapiv3::ParameterData,
2515 params_map: &Map<String, Value>,
2516 location: &str,
2517 value_type: &str,
2518 field_errors: &mut Vec<Value>,
2519) {
2520 match params_map.get(¶meter_data.name) {
2521 Some(value) => {
2522 if let ParameterSchemaOrContent::Schema(schema) = ¶meter_data.format {
2523 let details: Vec<Value> = Vec::new();
2525 let param_path = format!("{}.{}", location, parameter_data.name);
2526
2527 if let Some(schema_ref) = schema.as_item() {
2529 let coerced_value = coerce_value_for_schema(value, schema_ref);
2530 if let Err(validation_error) =
2532 OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
2533 {
2534 field_errors.push(json!({
2535 "path": param_path,
2536 "expected": "valid according to schema",
2537 "found": coerced_value,
2538 "message": validation_error.to_string()
2539 }));
2540 }
2541 }
2542
2543 for detail in details {
2544 field_errors.push(json!({
2545 "path": detail["path"],
2546 "expected": detail["expected_type"],
2547 "found": detail["value"],
2548 "message": detail["message"]
2549 }));
2550 }
2551 }
2552 }
2553 None => {
2554 if parameter_data.required {
2555 field_errors.push(json!({
2556 "path": format!("{}.{}", location, parameter_data.name),
2557 "expected": "value",
2558 "found": "missing",
2559 "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
2560 }));
2561 }
2562 }
2563 }
2564}
2565
2566fn validate_parameter_detailed_with_deep(
2568 parameter_data: &openapiv3::ParameterData,
2569 params_map: &Map<String, Value>,
2570 location: &str,
2571 value_type: &str,
2572 deep_value: Option<Value>,
2573 field_errors: &mut Vec<Value>,
2574) {
2575 match deep_value.as_ref().or_else(|| params_map.get(¶meter_data.name)) {
2576 Some(value) => {
2577 if let ParameterSchemaOrContent::Schema(schema) = ¶meter_data.format {
2578 let details: Vec<Value> = Vec::new();
2580 let param_path = format!("{}.{}", location, parameter_data.name);
2581
2582 if let Some(schema_ref) = schema.as_item() {
2584 let coerced_value = coerce_by_style(value, schema_ref, Some("form")); if let Err(validation_error) =
2587 OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
2588 {
2589 field_errors.push(json!({
2590 "path": param_path,
2591 "expected": "valid according to schema",
2592 "found": coerced_value,
2593 "message": validation_error.to_string()
2594 }));
2595 }
2596 }
2597
2598 for detail in details {
2599 field_errors.push(json!({
2600 "path": detail["path"],
2601 "expected": detail["expected_type"],
2602 "found": detail["value"],
2603 "message": detail["message"]
2604 }));
2605 }
2606 }
2607 }
2608 None => {
2609 if parameter_data.required {
2610 field_errors.push(json!({
2611 "path": format!("{}.{}", location, parameter_data.name),
2612 "expected": "value",
2613 "found": "missing",
2614 "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
2615 }));
2616 }
2617 }
2618 }
2619}
2620
2621pub async fn create_registry_from_file<P: AsRef<std::path::Path>>(
2623 path: P,
2624) -> Result<OpenApiRouteRegistry> {
2625 let spec = OpenApiSpec::from_file(path).await?;
2626 spec.validate()?;
2627 Ok(OpenApiRouteRegistry::new(spec))
2628}
2629
2630pub fn create_registry_from_json(json: Value) -> Result<OpenApiRouteRegistry> {
2632 let spec = OpenApiSpec::from_json(json)?;
2633 spec.validate()?;
2634 Ok(OpenApiRouteRegistry::new(spec))
2635}
2636
2637#[cfg(test)]
2638mod tests {
2639 use super::*;
2640 use serde_json::json;
2641 use tempfile::TempDir;
2642
2643 #[tokio::test]
2644 async fn test_registry_creation() {
2645 let spec_json = json!({
2646 "openapi": "3.0.0",
2647 "info": {
2648 "title": "Test API",
2649 "version": "1.0.0"
2650 },
2651 "paths": {
2652 "/users": {
2653 "get": {
2654 "summary": "Get users",
2655 "responses": {
2656 "200": {
2657 "description": "Success",
2658 "content": {
2659 "application/json": {
2660 "schema": {
2661 "type": "array",
2662 "items": {
2663 "type": "object",
2664 "properties": {
2665 "id": {"type": "integer"},
2666 "name": {"type": "string"}
2667 }
2668 }
2669 }
2670 }
2671 }
2672 }
2673 }
2674 },
2675 "post": {
2676 "summary": "Create user",
2677 "requestBody": {
2678 "content": {
2679 "application/json": {
2680 "schema": {
2681 "type": "object",
2682 "properties": {
2683 "name": {"type": "string"}
2684 },
2685 "required": ["name"]
2686 }
2687 }
2688 }
2689 },
2690 "responses": {
2691 "201": {
2692 "description": "Created",
2693 "content": {
2694 "application/json": {
2695 "schema": {
2696 "type": "object",
2697 "properties": {
2698 "id": {"type": "integer"},
2699 "name": {"type": "string"}
2700 }
2701 }
2702 }
2703 }
2704 }
2705 }
2706 }
2707 },
2708 "/users/{id}": {
2709 "get": {
2710 "summary": "Get user by ID",
2711 "parameters": [
2712 {
2713 "name": "id",
2714 "in": "path",
2715 "required": true,
2716 "schema": {"type": "integer"}
2717 }
2718 ],
2719 "responses": {
2720 "200": {
2721 "description": "Success",
2722 "content": {
2723 "application/json": {
2724 "schema": {
2725 "type": "object",
2726 "properties": {
2727 "id": {"type": "integer"},
2728 "name": {"type": "string"}
2729 }
2730 }
2731 }
2732 }
2733 }
2734 }
2735 }
2736 }
2737 }
2738 });
2739
2740 let registry = create_registry_from_json(spec_json).unwrap();
2741
2742 assert_eq!(registry.paths().len(), 2);
2744 assert!(registry.paths().contains(&"/users".to_string()));
2745 assert!(registry.paths().contains(&"/users/{id}".to_string()));
2746
2747 assert_eq!(registry.methods().len(), 2);
2748 assert!(registry.methods().contains(&"GET".to_string()));
2749 assert!(registry.methods().contains(&"POST".to_string()));
2750
2751 let get_users_route = registry.get_route("/users", "GET").unwrap();
2753 assert_eq!(get_users_route.method, "GET");
2754 assert_eq!(get_users_route.path, "/users");
2755
2756 let post_users_route = registry.get_route("/users", "POST").unwrap();
2757 assert_eq!(post_users_route.method, "POST");
2758 assert!(post_users_route.operation.request_body.is_some());
2759
2760 let user_by_id_route = registry.get_route("/users/{id}", "GET").unwrap();
2762 assert_eq!(user_by_id_route.axum_path(), "/users/{id}");
2763 }
2764
2765 #[tokio::test]
2766 async fn test_validate_request_with_params_and_formats() {
2767 let spec_json = json!({
2768 "openapi": "3.0.0",
2769 "info": { "title": "Test API", "version": "1.0.0" },
2770 "paths": {
2771 "/users/{id}": {
2772 "post": {
2773 "parameters": [
2774 { "name": "id", "in": "path", "required": true, "schema": {"type": "string"} },
2775 { "name": "q", "in": "query", "required": false, "schema": {"type": "integer"} }
2776 ],
2777 "requestBody": {
2778 "content": {
2779 "application/json": {
2780 "schema": {
2781 "type": "object",
2782 "required": ["email", "website"],
2783 "properties": {
2784 "email": {"type": "string", "format": "email"},
2785 "website": {"type": "string", "format": "uri"}
2786 }
2787 }
2788 }
2789 }
2790 },
2791 "responses": {"200": {"description": "ok"}}
2792 }
2793 }
2794 }
2795 });
2796
2797 let registry = create_registry_from_json(spec_json).unwrap();
2798 let mut path_params = Map::new();
2799 path_params.insert("id".to_string(), json!("abc"));
2800 let mut query_params = Map::new();
2801 query_params.insert("q".to_string(), json!(123));
2802
2803 let body = json!({"email":"a@b.co","website":"https://example.com"});
2805 assert!(registry
2806 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2807 .is_ok());
2808
2809 let bad_email = json!({"email":"not-an-email","website":"https://example.com"});
2811 assert!(registry
2812 .validate_request_with(
2813 "/users/{id}",
2814 "POST",
2815 &path_params,
2816 &query_params,
2817 Some(&bad_email)
2818 )
2819 .is_err());
2820
2821 let empty_path_params = Map::new();
2823 assert!(registry
2824 .validate_request_with(
2825 "/users/{id}",
2826 "POST",
2827 &empty_path_params,
2828 &query_params,
2829 Some(&body)
2830 )
2831 .is_err());
2832 }
2833
2834 #[tokio::test]
2835 async fn test_ref_resolution_for_params_and_body() {
2836 let spec_json = json!({
2837 "openapi": "3.0.0",
2838 "info": { "title": "Ref API", "version": "1.0.0" },
2839 "components": {
2840 "schemas": {
2841 "EmailWebsite": {
2842 "type": "object",
2843 "required": ["email", "website"],
2844 "properties": {
2845 "email": {"type": "string", "format": "email"},
2846 "website": {"type": "string", "format": "uri"}
2847 }
2848 }
2849 },
2850 "parameters": {
2851 "PathId": {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}},
2852 "QueryQ": {"name": "q", "in": "query", "required": false, "schema": {"type": "integer"}}
2853 },
2854 "requestBodies": {
2855 "CreateUser": {
2856 "content": {
2857 "application/json": {
2858 "schema": {"$ref": "#/components/schemas/EmailWebsite"}
2859 }
2860 }
2861 }
2862 }
2863 },
2864 "paths": {
2865 "/users/{id}": {
2866 "post": {
2867 "parameters": [
2868 {"$ref": "#/components/parameters/PathId"},
2869 {"$ref": "#/components/parameters/QueryQ"}
2870 ],
2871 "requestBody": {"$ref": "#/components/requestBodies/CreateUser"},
2872 "responses": {"200": {"description": "ok"}}
2873 }
2874 }
2875 }
2876 });
2877
2878 let registry = create_registry_from_json(spec_json).unwrap();
2879 let mut path_params = Map::new();
2880 path_params.insert("id".to_string(), json!("abc"));
2881 let mut query_params = Map::new();
2882 query_params.insert("q".to_string(), json!(7));
2883
2884 let body = json!({"email":"user@example.com","website":"https://example.com"});
2885 assert!(registry
2886 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2887 .is_ok());
2888
2889 let bad = json!({"email":"nope","website":"https://example.com"});
2890 assert!(registry
2891 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&bad))
2892 .is_err());
2893 }
2894
2895 #[tokio::test]
2896 async fn test_header_cookie_and_query_coercion() {
2897 let spec_json = json!({
2898 "openapi": "3.0.0",
2899 "info": { "title": "Params API", "version": "1.0.0" },
2900 "paths": {
2901 "/items": {
2902 "get": {
2903 "parameters": [
2904 {"name": "X-Flag", "in": "header", "required": true, "schema": {"type": "boolean"}},
2905 {"name": "session", "in": "cookie", "required": true, "schema": {"type": "string"}},
2906 {"name": "ids", "in": "query", "required": false, "schema": {"type": "array", "items": {"type": "integer"}}}
2907 ],
2908 "responses": {"200": {"description": "ok"}}
2909 }
2910 }
2911 }
2912 });
2913
2914 let registry = create_registry_from_json(spec_json).unwrap();
2915
2916 let path_params = Map::new();
2917 let mut query_params = Map::new();
2918 query_params.insert("ids".to_string(), json!("1,2,3"));
2920 let mut header_params = Map::new();
2921 header_params.insert("X-Flag".to_string(), json!("true"));
2922 let mut cookie_params = Map::new();
2923 cookie_params.insert("session".to_string(), json!("abc123"));
2924
2925 assert!(registry
2926 .validate_request_with_all(
2927 "/items",
2928 "GET",
2929 &path_params,
2930 &query_params,
2931 &header_params,
2932 &cookie_params,
2933 None
2934 )
2935 .is_ok());
2936
2937 let empty_cookie = Map::new();
2939 assert!(registry
2940 .validate_request_with_all(
2941 "/items",
2942 "GET",
2943 &path_params,
2944 &query_params,
2945 &header_params,
2946 &empty_cookie,
2947 None
2948 )
2949 .is_err());
2950
2951 let mut bad_header = Map::new();
2953 bad_header.insert("X-Flag".to_string(), json!("notabool"));
2954 assert!(registry
2955 .validate_request_with_all(
2956 "/items",
2957 "GET",
2958 &path_params,
2959 &query_params,
2960 &bad_header,
2961 &cookie_params,
2962 None
2963 )
2964 .is_err());
2965 }
2966
2967 #[tokio::test]
2968 async fn test_query_styles_space_pipe_deepobject() {
2969 let spec_json = json!({
2970 "openapi": "3.0.0",
2971 "info": { "title": "Query Styles API", "version": "1.0.0" },
2972 "paths": {"/search": {"get": {
2973 "parameters": [
2974 {"name":"tags","in":"query","style":"spaceDelimited","schema":{"type":"array","items":{"type":"string"}}},
2975 {"name":"ids","in":"query","style":"pipeDelimited","schema":{"type":"array","items":{"type":"integer"}}},
2976 {"name":"filter","in":"query","style":"deepObject","schema":{"type":"object","properties":{"color":{"type":"string"}},"required":["color"]}}
2977 ],
2978 "responses": {"200": {"description":"ok"}}
2979 }} }
2980 });
2981
2982 let registry = create_registry_from_json(spec_json).unwrap();
2983
2984 let path_params = Map::new();
2985 let mut query = Map::new();
2986 query.insert("tags".into(), json!("alpha beta gamma"));
2987 query.insert("ids".into(), json!("1|2|3"));
2988 query.insert("filter[color]".into(), json!("red"));
2989
2990 assert!(registry
2991 .validate_request_with("/search", "GET", &path_params, &query, None)
2992 .is_ok());
2993 }
2994
2995 #[tokio::test]
2996 async fn test_oneof_anyof_allof_validation() {
2997 let spec_json = json!({
2998 "openapi": "3.0.0",
2999 "info": { "title": "Composite API", "version": "1.0.0" },
3000 "paths": {
3001 "/composite": {
3002 "post": {
3003 "requestBody": {
3004 "content": {
3005 "application/json": {
3006 "schema": {
3007 "allOf": [
3008 {"type": "object", "required": ["base"], "properties": {"base": {"type": "string"}}}
3009 ],
3010 "oneOf": [
3011 {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"], "not": {"required": ["b"]}},
3012 {"type": "object", "properties": {"b": {"type": "integer"}}, "required": ["b"], "not": {"required": ["a"]}}
3013 ],
3014 "anyOf": [
3015 {"type": "object", "properties": {"flag": {"type": "boolean"}}, "required": ["flag"]},
3016 {"type": "object", "properties": {"extra": {"type": "string"}}, "required": ["extra"]}
3017 ]
3018 }
3019 }
3020 }
3021 },
3022 "responses": {"200": {"description": "ok"}}
3023 }
3024 }
3025 }
3026 });
3027
3028 let registry = create_registry_from_json(spec_json).unwrap();
3029 let ok = json!({"base": "x", "a": 1, "flag": true});
3031 assert!(registry
3032 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&ok))
3033 .is_ok());
3034
3035 let bad_oneof = json!({"base": "x", "a": 1, "b": 2, "flag": false});
3037 assert!(registry
3038 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_oneof))
3039 .is_err());
3040
3041 let bad_anyof = json!({"base": "x", "a": 1});
3043 assert!(registry
3044 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_anyof))
3045 .is_err());
3046
3047 let bad_allof = json!({"a": 1, "flag": true});
3049 assert!(registry
3050 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_allof))
3051 .is_err());
3052 }
3053
3054 #[tokio::test]
3055 async fn test_overrides_warn_mode_allows_invalid() {
3056 let spec_json = json!({
3058 "openapi": "3.0.0",
3059 "info": { "title": "Overrides API", "version": "1.0.0" },
3060 "paths": {"/things": {"post": {
3061 "parameters": [{"name":"q","in":"query","required":true,"schema":{"type":"integer"}}],
3062 "responses": {"200": {"description":"ok"}}
3063 }}}
3064 });
3065
3066 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3067 let mut overrides = HashMap::new();
3068 overrides.insert("POST /things".to_string(), ValidationMode::Warn);
3069 let registry = OpenApiRouteRegistry::new_with_options(
3070 spec,
3071 ValidationOptions {
3072 request_mode: ValidationMode::Enforce,
3073 aggregate_errors: true,
3074 validate_responses: false,
3075 overrides,
3076 admin_skip_prefixes: vec![],
3077 response_template_expand: false,
3078 validation_status: None,
3079 },
3080 );
3081
3082 let ok = registry.validate_request_with("/things", "POST", &Map::new(), &Map::new(), None);
3084 assert!(ok.is_ok());
3085 }
3086
3087 #[tokio::test]
3088 async fn test_admin_skip_prefix_short_circuit() {
3089 let spec_json = json!({
3090 "openapi": "3.0.0",
3091 "info": { "title": "Skip API", "version": "1.0.0" },
3092 "paths": {}
3093 });
3094 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3095 let registry = OpenApiRouteRegistry::new_with_options(
3096 spec,
3097 ValidationOptions {
3098 request_mode: ValidationMode::Enforce,
3099 aggregate_errors: true,
3100 validate_responses: false,
3101 overrides: HashMap::new(),
3102 admin_skip_prefixes: vec!["/admin".into()],
3103 response_template_expand: false,
3104 validation_status: None,
3105 },
3106 );
3107
3108 let res = registry.validate_request_with_all(
3110 "/admin/__mockforge/health",
3111 "GET",
3112 &Map::new(),
3113 &Map::new(),
3114 &Map::new(),
3115 &Map::new(),
3116 None,
3117 );
3118 assert!(res.is_ok());
3119 }
3120
3121 #[test]
3122 fn test_path_conversion() {
3123 assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users"), "/users");
3124 assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users/{id}"), "/users/{id}");
3125 assert_eq!(
3126 OpenApiRouteRegistry::convert_path_to_axum("/users/{id}/posts/{postId}"),
3127 "/users/{id}/posts/{postId}"
3128 );
3129 }
3130
3131 #[test]
3132 fn test_validation_options_default() {
3133 let options = ValidationOptions::default();
3134 assert!(matches!(options.request_mode, ValidationMode::Enforce));
3135 assert!(options.aggregate_errors);
3136 assert!(!options.validate_responses);
3137 assert!(options.overrides.is_empty());
3138 assert!(options.admin_skip_prefixes.is_empty());
3139 assert!(!options.response_template_expand);
3140 assert!(options.validation_status.is_none());
3141 }
3142
3143 #[test]
3144 fn test_validation_mode_variants() {
3145 let disabled = ValidationMode::Disabled;
3147 let warn = ValidationMode::Warn;
3148 let enforce = ValidationMode::Enforce;
3149 let default = ValidationMode::default();
3150
3151 assert!(matches!(default, ValidationMode::Warn));
3153
3154 assert!(!matches!(disabled, ValidationMode::Warn));
3156 assert!(!matches!(warn, ValidationMode::Enforce));
3157 assert!(!matches!(enforce, ValidationMode::Disabled));
3158 }
3159
3160 #[test]
3161 fn test_registry_spec_accessor() {
3162 let spec_json = json!({
3163 "openapi": "3.0.0",
3164 "info": {
3165 "title": "Test API",
3166 "version": "1.0.0"
3167 },
3168 "paths": {}
3169 });
3170 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3171 let registry = OpenApiRouteRegistry::new(spec.clone());
3172
3173 let accessed_spec = registry.spec();
3175 assert_eq!(accessed_spec.title(), "Test API");
3176 }
3177
3178 #[test]
3179 fn test_clone_for_validation() {
3180 let spec_json = json!({
3181 "openapi": "3.0.0",
3182 "info": {
3183 "title": "Test API",
3184 "version": "1.0.0"
3185 },
3186 "paths": {
3187 "/users": {
3188 "get": {
3189 "responses": {
3190 "200": {
3191 "description": "Success"
3192 }
3193 }
3194 }
3195 }
3196 }
3197 });
3198 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3199 let registry = OpenApiRouteRegistry::new(spec);
3200
3201 let cloned = registry.clone_for_validation();
3203 assert_eq!(cloned.routes().len(), registry.routes().len());
3204 assert_eq!(cloned.spec().title(), registry.spec().title());
3205 }
3206
3207 #[test]
3208 fn test_with_custom_fixture_loader() {
3209 let temp_dir = TempDir::new().unwrap();
3210 let spec_json = json!({
3211 "openapi": "3.0.0",
3212 "info": {
3213 "title": "Test API",
3214 "version": "1.0.0"
3215 },
3216 "paths": {}
3217 });
3218 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3219 let registry = OpenApiRouteRegistry::new(spec);
3220 let original_routes_len = registry.routes().len();
3221
3222 let custom_loader = Arc::new(crate::custom_fixture::CustomFixtureLoader::new(
3224 temp_dir.path().to_path_buf(),
3225 true,
3226 ));
3227 let registry_with_loader = registry.with_custom_fixture_loader(custom_loader);
3228
3229 assert_eq!(registry_with_loader.routes().len(), original_routes_len);
3231 }
3232
3233 #[test]
3234 fn test_get_route() {
3235 let spec_json = json!({
3236 "openapi": "3.0.0",
3237 "info": {
3238 "title": "Test API",
3239 "version": "1.0.0"
3240 },
3241 "paths": {
3242 "/users": {
3243 "get": {
3244 "operationId": "getUsers",
3245 "responses": {
3246 "200": {
3247 "description": "Success"
3248 }
3249 }
3250 },
3251 "post": {
3252 "operationId": "createUser",
3253 "responses": {
3254 "201": {
3255 "description": "Created"
3256 }
3257 }
3258 }
3259 }
3260 }
3261 });
3262 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3263 let registry = OpenApiRouteRegistry::new(spec);
3264
3265 let route = registry.get_route("/users", "GET");
3267 assert!(route.is_some());
3268 assert_eq!(route.unwrap().method, "GET");
3269 assert_eq!(route.unwrap().path, "/users");
3270
3271 let route = registry.get_route("/nonexistent", "GET");
3273 assert!(route.is_none());
3274
3275 let route = registry.get_route("/users", "POST");
3277 assert!(route.is_some());
3278 assert_eq!(route.unwrap().method, "POST");
3279 }
3280
3281 #[test]
3282 fn test_get_routes_for_path() {
3283 let spec_json = json!({
3284 "openapi": "3.0.0",
3285 "info": {
3286 "title": "Test API",
3287 "version": "1.0.0"
3288 },
3289 "paths": {
3290 "/users": {
3291 "get": {
3292 "responses": {
3293 "200": {
3294 "description": "Success"
3295 }
3296 }
3297 },
3298 "post": {
3299 "responses": {
3300 "201": {
3301 "description": "Created"
3302 }
3303 }
3304 },
3305 "put": {
3306 "responses": {
3307 "200": {
3308 "description": "Success"
3309 }
3310 }
3311 }
3312 },
3313 "/posts": {
3314 "get": {
3315 "responses": {
3316 "200": {
3317 "description": "Success"
3318 }
3319 }
3320 }
3321 }
3322 }
3323 });
3324 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3325 let registry = OpenApiRouteRegistry::new(spec);
3326
3327 let routes = registry.get_routes_for_path("/users");
3329 assert_eq!(routes.len(), 3);
3330 let methods: Vec<&str> = routes.iter().map(|r| r.method.as_str()).collect();
3331 assert!(methods.contains(&"GET"));
3332 assert!(methods.contains(&"POST"));
3333 assert!(methods.contains(&"PUT"));
3334
3335 let routes = registry.get_routes_for_path("/posts");
3337 assert_eq!(routes.len(), 1);
3338 assert_eq!(routes[0].method, "GET");
3339
3340 let routes = registry.get_routes_for_path("/nonexistent");
3342 assert!(routes.is_empty());
3343 }
3344
3345 #[test]
3346 fn test_new_vs_new_with_options() {
3347 let spec_json = json!({
3348 "openapi": "3.0.0",
3349 "info": {
3350 "title": "Test API",
3351 "version": "1.0.0"
3352 },
3353 "paths": {}
3354 });
3355 let spec1 = OpenApiSpec::from_json(spec_json.clone()).unwrap();
3356 let spec2 = OpenApiSpec::from_json(spec_json).unwrap();
3357
3358 let registry1 = OpenApiRouteRegistry::new(spec1);
3360 assert_eq!(registry1.spec().title(), "Test API");
3361
3362 let options = ValidationOptions {
3364 request_mode: ValidationMode::Disabled,
3365 aggregate_errors: false,
3366 validate_responses: true,
3367 overrides: HashMap::new(),
3368 admin_skip_prefixes: vec!["/admin".to_string()],
3369 response_template_expand: true,
3370 validation_status: Some(422),
3371 };
3372 let registry2 = OpenApiRouteRegistry::new_with_options(spec2, options);
3373 assert_eq!(registry2.spec().title(), "Test API");
3374 }
3375
3376 #[test]
3377 fn test_new_with_env_vs_new() {
3378 let spec_json = json!({
3379 "openapi": "3.0.0",
3380 "info": {
3381 "title": "Test API",
3382 "version": "1.0.0"
3383 },
3384 "paths": {}
3385 });
3386 let spec1 = OpenApiSpec::from_json(spec_json.clone()).unwrap();
3387 let spec2 = OpenApiSpec::from_json(spec_json).unwrap();
3388
3389 let registry1 = OpenApiRouteRegistry::new(spec1);
3391
3392 let registry2 = OpenApiRouteRegistry::new_with_env(spec2);
3394
3395 assert_eq!(registry1.spec().title(), "Test API");
3397 assert_eq!(registry2.spec().title(), "Test API");
3398 }
3399
3400 #[test]
3401 fn test_validation_options_custom() {
3402 let options = ValidationOptions {
3403 request_mode: ValidationMode::Warn,
3404 aggregate_errors: false,
3405 validate_responses: true,
3406 overrides: {
3407 let mut map = HashMap::new();
3408 map.insert("getUsers".to_string(), ValidationMode::Disabled);
3409 map
3410 },
3411 admin_skip_prefixes: vec!["/admin".to_string(), "/internal".to_string()],
3412 response_template_expand: true,
3413 validation_status: Some(422),
3414 };
3415
3416 assert!(matches!(options.request_mode, ValidationMode::Warn));
3417 assert!(!options.aggregate_errors);
3418 assert!(options.validate_responses);
3419 assert_eq!(options.overrides.len(), 1);
3420 assert_eq!(options.admin_skip_prefixes.len(), 2);
3421 assert!(options.response_template_expand);
3422 assert_eq!(options.validation_status, Some(422));
3423 }
3424
3425 #[test]
3426 fn test_validation_mode_default_standalone() {
3427 let mode = ValidationMode::default();
3428 assert!(matches!(mode, ValidationMode::Warn));
3429 }
3430
3431 #[test]
3432 fn test_validation_mode_clone() {
3433 let mode1 = ValidationMode::Enforce;
3434 let mode2 = mode1.clone();
3435 assert!(matches!(mode1, ValidationMode::Enforce));
3436 assert!(matches!(mode2, ValidationMode::Enforce));
3437 }
3438
3439 #[test]
3440 fn test_validation_mode_debug() {
3441 let mode = ValidationMode::Disabled;
3442 let debug_str = format!("{:?}", mode);
3443 assert!(debug_str.contains("Disabled") || debug_str.contains("ValidationMode"));
3444 }
3445
3446 #[test]
3447 fn test_validation_options_clone() {
3448 let options1 = ValidationOptions {
3449 request_mode: ValidationMode::Warn,
3450 aggregate_errors: true,
3451 validate_responses: false,
3452 overrides: HashMap::new(),
3453 admin_skip_prefixes: vec![],
3454 response_template_expand: false,
3455 validation_status: None,
3456 };
3457 let options2 = options1.clone();
3458 assert!(matches!(options2.request_mode, ValidationMode::Warn));
3459 assert_eq!(options1.aggregate_errors, options2.aggregate_errors);
3460 }
3461
3462 #[test]
3463 fn test_validation_options_debug() {
3464 let options = ValidationOptions::default();
3465 let debug_str = format!("{:?}", options);
3466 assert!(debug_str.contains("ValidationOptions"));
3467 }
3468
3469 #[test]
3470 fn test_validation_options_with_all_fields() {
3471 let mut overrides = HashMap::new();
3472 overrides.insert("op1".to_string(), ValidationMode::Disabled);
3473 overrides.insert("op2".to_string(), ValidationMode::Warn);
3474
3475 let options = ValidationOptions {
3476 request_mode: ValidationMode::Enforce,
3477 aggregate_errors: false,
3478 validate_responses: true,
3479 overrides: overrides.clone(),
3480 admin_skip_prefixes: vec!["/admin".to_string(), "/internal".to_string()],
3481 response_template_expand: true,
3482 validation_status: Some(422),
3483 };
3484
3485 assert!(matches!(options.request_mode, ValidationMode::Enforce));
3486 assert!(!options.aggregate_errors);
3487 assert!(options.validate_responses);
3488 assert_eq!(options.overrides.len(), 2);
3489 assert_eq!(options.admin_skip_prefixes.len(), 2);
3490 assert!(options.response_template_expand);
3491 assert_eq!(options.validation_status, Some(422));
3492 }
3493
3494 #[test]
3495 fn test_openapi_route_registry_clone() {
3496 let spec_json = json!({
3497 "openapi": "3.0.0",
3498 "info": { "title": "Test API", "version": "1.0.0" },
3499 "paths": {}
3500 });
3501 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3502 let registry1 = OpenApiRouteRegistry::new(spec);
3503 let registry2 = registry1.clone();
3504 assert_eq!(registry1.spec().title(), registry2.spec().title());
3505 }
3506
3507 #[test]
3508 fn test_validation_mode_serialization() {
3509 let mode = ValidationMode::Enforce;
3510 let json = serde_json::to_string(&mode).unwrap();
3511 assert!(json.contains("Enforce") || json.contains("enforce"));
3512 }
3513
3514 #[test]
3515 fn test_validation_mode_deserialization() {
3516 let json = r#""Disabled""#;
3517 let mode: ValidationMode = serde_json::from_str(json).unwrap();
3518 assert!(matches!(mode, ValidationMode::Disabled));
3519 }
3520
3521 #[test]
3522 fn test_validation_options_default_values() {
3523 let options = ValidationOptions::default();
3524 assert!(matches!(options.request_mode, ValidationMode::Enforce));
3525 assert!(options.aggregate_errors);
3526 assert!(!options.validate_responses);
3527 assert!(options.overrides.is_empty());
3528 assert!(options.admin_skip_prefixes.is_empty());
3529 assert!(!options.response_template_expand);
3530 assert_eq!(options.validation_status, None);
3531 }
3532
3533 #[test]
3534 fn test_validation_mode_all_variants() {
3535 let disabled = ValidationMode::Disabled;
3536 let warn = ValidationMode::Warn;
3537 let enforce = ValidationMode::Enforce;
3538
3539 assert!(matches!(disabled, ValidationMode::Disabled));
3540 assert!(matches!(warn, ValidationMode::Warn));
3541 assert!(matches!(enforce, ValidationMode::Enforce));
3542 }
3543
3544 #[test]
3545 fn test_validation_options_with_overrides() {
3546 let mut overrides = HashMap::new();
3547 overrides.insert("operation1".to_string(), ValidationMode::Disabled);
3548 overrides.insert("operation2".to_string(), ValidationMode::Warn);
3549
3550 let options = ValidationOptions {
3551 request_mode: ValidationMode::Enforce,
3552 aggregate_errors: true,
3553 validate_responses: false,
3554 overrides,
3555 admin_skip_prefixes: vec![],
3556 response_template_expand: false,
3557 validation_status: None,
3558 };
3559
3560 assert_eq!(options.overrides.len(), 2);
3561 assert!(matches!(options.overrides.get("operation1"), Some(ValidationMode::Disabled)));
3562 assert!(matches!(options.overrides.get("operation2"), Some(ValidationMode::Warn)));
3563 }
3564
3565 #[test]
3566 fn test_validation_options_with_admin_skip_prefixes() {
3567 let options = ValidationOptions {
3568 request_mode: ValidationMode::Enforce,
3569 aggregate_errors: true,
3570 validate_responses: false,
3571 overrides: HashMap::new(),
3572 admin_skip_prefixes: vec![
3573 "/admin".to_string(),
3574 "/internal".to_string(),
3575 "/debug".to_string(),
3576 ],
3577 response_template_expand: false,
3578 validation_status: None,
3579 };
3580
3581 assert_eq!(options.admin_skip_prefixes.len(), 3);
3582 assert!(options.admin_skip_prefixes.contains(&"/admin".to_string()));
3583 assert!(options.admin_skip_prefixes.contains(&"/internal".to_string()));
3584 assert!(options.admin_skip_prefixes.contains(&"/debug".to_string()));
3585 }
3586
3587 #[test]
3588 fn test_validation_options_with_validation_status() {
3589 let options1 = ValidationOptions {
3590 request_mode: ValidationMode::Enforce,
3591 aggregate_errors: true,
3592 validate_responses: false,
3593 overrides: HashMap::new(),
3594 admin_skip_prefixes: vec![],
3595 response_template_expand: false,
3596 validation_status: Some(400),
3597 };
3598
3599 let options2 = ValidationOptions {
3600 request_mode: ValidationMode::Enforce,
3601 aggregate_errors: true,
3602 validate_responses: false,
3603 overrides: HashMap::new(),
3604 admin_skip_prefixes: vec![],
3605 response_template_expand: false,
3606 validation_status: Some(422),
3607 };
3608
3609 assert_eq!(options1.validation_status, Some(400));
3610 assert_eq!(options2.validation_status, Some(422));
3611 }
3612
3613 #[test]
3614 fn test_validate_request_with_disabled_mode() {
3615 let spec_json = json!({
3617 "openapi": "3.0.0",
3618 "info": {"title": "Test API", "version": "1.0.0"},
3619 "paths": {
3620 "/users": {
3621 "get": {
3622 "responses": {"200": {"description": "OK"}}
3623 }
3624 }
3625 }
3626 });
3627 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3628 let options = ValidationOptions {
3629 request_mode: ValidationMode::Disabled,
3630 ..Default::default()
3631 };
3632 let registry = OpenApiRouteRegistry::new_with_options(spec, options);
3633
3634 let result = registry.validate_request_with_all(
3636 "/users",
3637 "GET",
3638 &Map::new(),
3639 &Map::new(),
3640 &Map::new(),
3641 &Map::new(),
3642 None,
3643 );
3644 assert!(result.is_ok());
3645 }
3646
3647 #[test]
3648 fn test_validate_request_with_warn_mode() {
3649 let spec_json = json!({
3651 "openapi": "3.0.0",
3652 "info": {"title": "Test API", "version": "1.0.0"},
3653 "paths": {
3654 "/users": {
3655 "post": {
3656 "requestBody": {
3657 "required": true,
3658 "content": {
3659 "application/json": {
3660 "schema": {
3661 "type": "object",
3662 "required": ["name"],
3663 "properties": {
3664 "name": {"type": "string"}
3665 }
3666 }
3667 }
3668 }
3669 },
3670 "responses": {"200": {"description": "OK"}}
3671 }
3672 }
3673 }
3674 });
3675 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3676 let options = ValidationOptions {
3677 request_mode: ValidationMode::Warn,
3678 ..Default::default()
3679 };
3680 let registry = OpenApiRouteRegistry::new_with_options(spec, options);
3681
3682 let result = registry.validate_request_with_all(
3684 "/users",
3685 "POST",
3686 &Map::new(),
3687 &Map::new(),
3688 &Map::new(),
3689 &Map::new(),
3690 None, );
3692 assert!(result.is_ok()); }
3694
3695 #[test]
3696 fn test_validate_request_body_validation_error() {
3697 let spec_json = json!({
3699 "openapi": "3.0.0",
3700 "info": {"title": "Test API", "version": "1.0.0"},
3701 "paths": {
3702 "/users": {
3703 "post": {
3704 "requestBody": {
3705 "required": true,
3706 "content": {
3707 "application/json": {
3708 "schema": {
3709 "type": "object",
3710 "required": ["name"],
3711 "properties": {
3712 "name": {"type": "string"}
3713 }
3714 }
3715 }
3716 }
3717 },
3718 "responses": {"200": {"description": "OK"}}
3719 }
3720 }
3721 }
3722 });
3723 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3724 let registry = OpenApiRouteRegistry::new(spec);
3725
3726 let result = registry.validate_request_with_all(
3728 "/users",
3729 "POST",
3730 &Map::new(),
3731 &Map::new(),
3732 &Map::new(),
3733 &Map::new(),
3734 None, );
3736 assert!(result.is_err());
3737 }
3738
3739 #[test]
3740 fn test_validate_request_body_schema_validation_error() {
3741 let spec_json = json!({
3743 "openapi": "3.0.0",
3744 "info": {"title": "Test API", "version": "1.0.0"},
3745 "paths": {
3746 "/users": {
3747 "post": {
3748 "requestBody": {
3749 "required": true,
3750 "content": {
3751 "application/json": {
3752 "schema": {
3753 "type": "object",
3754 "required": ["name"],
3755 "properties": {
3756 "name": {"type": "string"}
3757 }
3758 }
3759 }
3760 }
3761 },
3762 "responses": {"200": {"description": "OK"}}
3763 }
3764 }
3765 }
3766 });
3767 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3768 let registry = OpenApiRouteRegistry::new(spec);
3769
3770 let invalid_body = json!({}); let result = registry.validate_request_with_all(
3773 "/users",
3774 "POST",
3775 &Map::new(),
3776 &Map::new(),
3777 &Map::new(),
3778 &Map::new(),
3779 Some(&invalid_body),
3780 );
3781 assert!(result.is_err());
3782 }
3783
3784 #[test]
3785 fn test_validate_request_body_referenced_schema_error() {
3786 let spec_json = json!({
3788 "openapi": "3.0.0",
3789 "info": {"title": "Test API", "version": "1.0.0"},
3790 "paths": {
3791 "/users": {
3792 "post": {
3793 "requestBody": {
3794 "required": true,
3795 "content": {
3796 "application/json": {
3797 "schema": {
3798 "$ref": "#/components/schemas/NonExistentSchema"
3799 }
3800 }
3801 }
3802 },
3803 "responses": {"200": {"description": "OK"}}
3804 }
3805 }
3806 },
3807 "components": {}
3808 });
3809 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3810 let registry = OpenApiRouteRegistry::new(spec);
3811
3812 let body = json!({"name": "test"});
3814 let result = registry.validate_request_with_all(
3815 "/users",
3816 "POST",
3817 &Map::new(),
3818 &Map::new(),
3819 &Map::new(),
3820 &Map::new(),
3821 Some(&body),
3822 );
3823 assert!(result.is_err());
3824 }
3825
3826 #[test]
3827 fn test_validate_request_body_referenced_request_body_error() {
3828 let spec_json = json!({
3830 "openapi": "3.0.0",
3831 "info": {"title": "Test API", "version": "1.0.0"},
3832 "paths": {
3833 "/users": {
3834 "post": {
3835 "requestBody": {
3836 "$ref": "#/components/requestBodies/NonExistentRequestBody"
3837 },
3838 "responses": {"200": {"description": "OK"}}
3839 }
3840 }
3841 },
3842 "components": {}
3843 });
3844 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3845 let registry = OpenApiRouteRegistry::new(spec);
3846
3847 let body = json!({"name": "test"});
3849 let result = registry.validate_request_with_all(
3850 "/users",
3851 "POST",
3852 &Map::new(),
3853 &Map::new(),
3854 &Map::new(),
3855 &Map::new(),
3856 Some(&body),
3857 );
3858 assert!(result.is_err());
3859 }
3860
3861 #[test]
3862 fn test_validate_request_body_provided_when_not_expected() {
3863 let spec_json = json!({
3865 "openapi": "3.0.0",
3866 "info": {"title": "Test API", "version": "1.0.0"},
3867 "paths": {
3868 "/users": {
3869 "get": {
3870 "responses": {"200": {"description": "OK"}}
3871 }
3872 }
3873 }
3874 });
3875 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3876 let registry = OpenApiRouteRegistry::new(spec);
3877
3878 let body = json!({"extra": "data"});
3880 let result = registry.validate_request_with_all(
3881 "/users",
3882 "GET",
3883 &Map::new(),
3884 &Map::new(),
3885 &Map::new(),
3886 &Map::new(),
3887 Some(&body),
3888 );
3889 assert!(result.is_ok());
3891 }
3892
3893 #[test]
3894 fn test_get_operation() {
3895 let spec_json = json!({
3897 "openapi": "3.0.0",
3898 "info": {"title": "Test API", "version": "1.0.0"},
3899 "paths": {
3900 "/users": {
3901 "get": {
3902 "operationId": "getUsers",
3903 "summary": "Get users",
3904 "responses": {"200": {"description": "OK"}}
3905 }
3906 }
3907 }
3908 });
3909 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3910 let registry = OpenApiRouteRegistry::new(spec);
3911
3912 let operation = registry.get_operation("/users", "GET");
3914 assert!(operation.is_some());
3915 assert_eq!(operation.unwrap().method, "GET");
3916
3917 assert!(registry.get_operation("/nonexistent", "GET").is_none());
3919 }
3920
3921 #[test]
3922 fn test_extract_path_parameters() {
3923 let spec_json = json!({
3925 "openapi": "3.0.0",
3926 "info": {"title": "Test API", "version": "1.0.0"},
3927 "paths": {
3928 "/users/{id}": {
3929 "get": {
3930 "parameters": [
3931 {
3932 "name": "id",
3933 "in": "path",
3934 "required": true,
3935 "schema": {"type": "string"}
3936 }
3937 ],
3938 "responses": {"200": {"description": "OK"}}
3939 }
3940 }
3941 }
3942 });
3943 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3944 let registry = OpenApiRouteRegistry::new(spec);
3945
3946 let params = registry.extract_path_parameters("/users/123", "GET");
3948 assert_eq!(params.get("id"), Some(&"123".to_string()));
3949
3950 let empty_params = registry.extract_path_parameters("/users", "GET");
3952 assert!(empty_params.is_empty());
3953 }
3954
3955 #[test]
3956 fn test_extract_path_parameters_multiple_params() {
3957 let spec_json = json!({
3959 "openapi": "3.0.0",
3960 "info": {"title": "Test API", "version": "1.0.0"},
3961 "paths": {
3962 "/users/{userId}/posts/{postId}": {
3963 "get": {
3964 "parameters": [
3965 {
3966 "name": "userId",
3967 "in": "path",
3968 "required": true,
3969 "schema": {"type": "string"}
3970 },
3971 {
3972 "name": "postId",
3973 "in": "path",
3974 "required": true,
3975 "schema": {"type": "string"}
3976 }
3977 ],
3978 "responses": {"200": {"description": "OK"}}
3979 }
3980 }
3981 }
3982 });
3983 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3984 let registry = OpenApiRouteRegistry::new(spec);
3985
3986 let params = registry.extract_path_parameters("/users/123/posts/456", "GET");
3988 assert_eq!(params.get("userId"), Some(&"123".to_string()));
3989 assert_eq!(params.get("postId"), Some(&"456".to_string()));
3990 }
3991
3992 #[test]
3993 fn test_validate_request_route_not_found() {
3994 let spec_json = json!({
3996 "openapi": "3.0.0",
3997 "info": {"title": "Test API", "version": "1.0.0"},
3998 "paths": {
3999 "/users": {
4000 "get": {
4001 "responses": {"200": {"description": "OK"}}
4002 }
4003 }
4004 }
4005 });
4006 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4007 let registry = OpenApiRouteRegistry::new(spec);
4008
4009 let result = registry.validate_request_with_all(
4011 "/nonexistent",
4012 "GET",
4013 &Map::new(),
4014 &Map::new(),
4015 &Map::new(),
4016 &Map::new(),
4017 None,
4018 );
4019 assert!(result.is_err());
4020 assert!(result.unwrap_err().to_string().contains("not found"));
4021 }
4022
4023 #[test]
4024 fn test_validate_request_with_path_parameters() {
4025 let spec_json = json!({
4027 "openapi": "3.0.0",
4028 "info": {"title": "Test API", "version": "1.0.0"},
4029 "paths": {
4030 "/users/{id}": {
4031 "get": {
4032 "parameters": [
4033 {
4034 "name": "id",
4035 "in": "path",
4036 "required": true,
4037 "schema": {"type": "string", "minLength": 1}
4038 }
4039 ],
4040 "responses": {"200": {"description": "OK"}}
4041 }
4042 }
4043 }
4044 });
4045 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4046 let registry = OpenApiRouteRegistry::new(spec);
4047
4048 let mut path_params = Map::new();
4050 path_params.insert("id".to_string(), json!("123"));
4051 let result = registry.validate_request_with_all(
4052 "/users/{id}",
4053 "GET",
4054 &path_params,
4055 &Map::new(),
4056 &Map::new(),
4057 &Map::new(),
4058 None,
4059 );
4060 assert!(result.is_ok());
4061 }
4062
4063 #[test]
4064 fn test_validate_request_with_query_parameters() {
4065 let spec_json = json!({
4067 "openapi": "3.0.0",
4068 "info": {"title": "Test API", "version": "1.0.0"},
4069 "paths": {
4070 "/users": {
4071 "get": {
4072 "parameters": [
4073 {
4074 "name": "page",
4075 "in": "query",
4076 "required": true,
4077 "schema": {"type": "integer", "minimum": 1}
4078 }
4079 ],
4080 "responses": {"200": {"description": "OK"}}
4081 }
4082 }
4083 }
4084 });
4085 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4086 let registry = OpenApiRouteRegistry::new(spec);
4087
4088 let mut query_params = Map::new();
4090 query_params.insert("page".to_string(), json!(1));
4091 let result = registry.validate_request_with_all(
4092 "/users",
4093 "GET",
4094 &Map::new(),
4095 &query_params,
4096 &Map::new(),
4097 &Map::new(),
4098 None,
4099 );
4100 assert!(result.is_ok());
4101 }
4102
4103 #[test]
4104 fn test_validate_request_with_header_parameters() {
4105 let spec_json = json!({
4107 "openapi": "3.0.0",
4108 "info": {"title": "Test API", "version": "1.0.0"},
4109 "paths": {
4110 "/users": {
4111 "get": {
4112 "parameters": [
4113 {
4114 "name": "X-API-Key",
4115 "in": "header",
4116 "required": true,
4117 "schema": {"type": "string"}
4118 }
4119 ],
4120 "responses": {"200": {"description": "OK"}}
4121 }
4122 }
4123 }
4124 });
4125 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4126 let registry = OpenApiRouteRegistry::new(spec);
4127
4128 let mut header_params = Map::new();
4130 header_params.insert("X-API-Key".to_string(), json!("secret-key"));
4131 let result = registry.validate_request_with_all(
4132 "/users",
4133 "GET",
4134 &Map::new(),
4135 &Map::new(),
4136 &header_params,
4137 &Map::new(),
4138 None,
4139 );
4140 assert!(result.is_ok());
4141 }
4142
4143 #[test]
4144 fn test_validate_request_with_cookie_parameters() {
4145 let spec_json = json!({
4147 "openapi": "3.0.0",
4148 "info": {"title": "Test API", "version": "1.0.0"},
4149 "paths": {
4150 "/users": {
4151 "get": {
4152 "parameters": [
4153 {
4154 "name": "sessionId",
4155 "in": "cookie",
4156 "required": true,
4157 "schema": {"type": "string"}
4158 }
4159 ],
4160 "responses": {"200": {"description": "OK"}}
4161 }
4162 }
4163 }
4164 });
4165 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4166 let registry = OpenApiRouteRegistry::new(spec);
4167
4168 let mut cookie_params = Map::new();
4170 cookie_params.insert("sessionId".to_string(), json!("abc123"));
4171 let result = registry.validate_request_with_all(
4172 "/users",
4173 "GET",
4174 &Map::new(),
4175 &Map::new(),
4176 &Map::new(),
4177 &cookie_params,
4178 None,
4179 );
4180 assert!(result.is_ok());
4181 }
4182
4183 #[test]
4184 fn test_validate_request_no_errors_early_return() {
4185 let spec_json = json!({
4187 "openapi": "3.0.0",
4188 "info": {"title": "Test API", "version": "1.0.0"},
4189 "paths": {
4190 "/users": {
4191 "get": {
4192 "responses": {"200": {"description": "OK"}}
4193 }
4194 }
4195 }
4196 });
4197 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4198 let registry = OpenApiRouteRegistry::new(spec);
4199
4200 let result = registry.validate_request_with_all(
4202 "/users",
4203 "GET",
4204 &Map::new(),
4205 &Map::new(),
4206 &Map::new(),
4207 &Map::new(),
4208 None,
4209 );
4210 assert!(result.is_ok());
4211 }
4212
4213 #[test]
4214 fn test_validate_request_query_parameter_different_styles() {
4215 let spec_json = json!({
4217 "openapi": "3.0.0",
4218 "info": {"title": "Test API", "version": "1.0.0"},
4219 "paths": {
4220 "/users": {
4221 "get": {
4222 "parameters": [
4223 {
4224 "name": "tags",
4225 "in": "query",
4226 "style": "pipeDelimited",
4227 "schema": {
4228 "type": "array",
4229 "items": {"type": "string"}
4230 }
4231 }
4232 ],
4233 "responses": {"200": {"description": "OK"}}
4234 }
4235 }
4236 }
4237 });
4238 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4239 let registry = OpenApiRouteRegistry::new(spec);
4240
4241 let mut query_params = Map::new();
4243 query_params.insert("tags".to_string(), json!(["tag1", "tag2"]));
4244 let result = registry.validate_request_with_all(
4245 "/users",
4246 "GET",
4247 &Map::new(),
4248 &query_params,
4249 &Map::new(),
4250 &Map::new(),
4251 None,
4252 );
4253 assert!(result.is_ok() || result.is_err()); }
4256}