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