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::templating::expand_tokens as core_expand_tokens;
25use crate::{latency::LatencyInjector, overrides::Overrides, Error, Result};
26use axum::extract::{Path as AxumPath, RawQuery};
27use axum::http::HeaderMap;
28use axum::response::IntoResponse;
29use axum::routing::*;
30use axum::{Json, Router};
31use chrono::Utc;
32use once_cell::sync::Lazy;
33use openapiv3::ParameterSchemaOrContent;
34use serde_json::{json, Map, Value};
35use std::collections::{HashMap, VecDeque};
36use std::sync::{Arc, Mutex};
37use tracing;
38
39#[derive(Debug, Clone)]
41pub struct OpenApiRouteRegistry {
42 spec: Arc<OpenApiSpec>,
44 routes: Vec<OpenApiRoute>,
46 options: ValidationOptions,
48}
49
50#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)]
52pub enum ValidationMode {
53 Disabled,
55 #[default]
57 Warn,
58 Enforce,
60}
61
62#[derive(Debug, Clone)]
64pub struct ValidationOptions {
65 pub request_mode: ValidationMode,
67 pub aggregate_errors: bool,
69 pub validate_responses: bool,
71 pub overrides: std::collections::HashMap<String, ValidationMode>,
73 pub admin_skip_prefixes: Vec<String>,
75 pub response_template_expand: bool,
77 pub validation_status: Option<u16>,
79}
80
81impl Default for ValidationOptions {
82 fn default() -> Self {
83 Self {
84 request_mode: ValidationMode::Enforce,
85 aggregate_errors: true,
86 validate_responses: false,
87 overrides: std::collections::HashMap::new(),
88 admin_skip_prefixes: Vec::new(),
89 response_template_expand: false,
90 validation_status: None,
91 }
92 }
93}
94
95impl OpenApiRouteRegistry {
96 pub fn new(spec: OpenApiSpec) -> Self {
98 Self::new_with_env(spec)
99 }
100
101 pub fn new_with_env(spec: OpenApiSpec) -> Self {
110 tracing::debug!("Creating OpenAPI route registry");
111 let spec = Arc::new(spec);
112 let routes = Self::generate_routes(&spec);
113 let options = ValidationOptions {
114 request_mode: match std::env::var("MOCKFORGE_REQUEST_VALIDATION")
115 .unwrap_or_else(|_| "enforce".into())
116 .to_ascii_lowercase()
117 .as_str()
118 {
119 "off" | "disable" | "disabled" => ValidationMode::Disabled,
120 "warn" | "warning" => ValidationMode::Warn,
121 _ => ValidationMode::Enforce,
122 },
123 aggregate_errors: std::env::var("MOCKFORGE_AGGREGATE_ERRORS")
124 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
125 .unwrap_or(true),
126 validate_responses: std::env::var("MOCKFORGE_RESPONSE_VALIDATION")
127 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
128 .unwrap_or(false),
129 overrides: std::collections::HashMap::new(),
130 admin_skip_prefixes: Vec::new(),
131 response_template_expand: std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
132 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
133 .unwrap_or(false),
134 validation_status: std::env::var("MOCKFORGE_VALIDATION_STATUS")
135 .ok()
136 .and_then(|s| s.parse::<u16>().ok()),
137 };
138 Self {
139 spec,
140 routes,
141 options,
142 }
143 }
144
145 pub fn new_with_options(spec: OpenApiSpec, options: ValidationOptions) -> Self {
147 tracing::debug!("Creating OpenAPI route registry with custom options");
148 let spec = Arc::new(spec);
149 let routes = Self::generate_routes(&spec);
150 Self {
151 spec,
152 routes,
153 options,
154 }
155 }
156
157 pub fn clone_for_validation(&self) -> Self {
162 OpenApiRouteRegistry {
163 spec: self.spec.clone(),
164 routes: self.routes.clone(),
165 options: self.options.clone(),
166 }
167 }
168
169 fn generate_routes(spec: &Arc<OpenApiSpec>) -> Vec<OpenApiRoute> {
171 let mut routes = Vec::new();
172
173 let all_paths_ops = spec.all_paths_and_operations();
174 tracing::debug!("Generating routes from OpenAPI spec with {} paths", all_paths_ops.len());
175
176 for (path, operations) in all_paths_ops {
177 tracing::debug!("Processing path: {}", path);
178 for (method, operation) in operations {
179 routes.push(OpenApiRoute::from_operation(
180 &method,
181 path.clone(),
182 &operation,
183 spec.clone(),
184 ));
185 }
186 }
187
188 tracing::debug!("Generated {} total routes from OpenAPI spec", routes.len());
189 routes
190 }
191
192 pub fn routes(&self) -> &[OpenApiRoute] {
194 &self.routes
195 }
196
197 pub fn spec(&self) -> &OpenApiSpec {
199 &self.spec
200 }
201
202 pub fn build_router(self) -> Router {
204 let mut router = Router::new();
205 tracing::debug!("Building router from {} routes", self.routes.len());
206
207 for route in &self.routes {
209 tracing::debug!("Adding route: {} {}", route.method, route.path);
210 let axum_path = route.axum_path();
211 let operation = route.operation.clone();
212 let method = route.method.clone();
213 let path_template = route.path.clone();
214 let validator = self.clone_for_validation();
215 let route_clone = route.clone();
216
217 let handler = move |AxumPath(path_params): AxumPath<
219 std::collections::HashMap<String, String>,
220 >,
221 RawQuery(raw_query): RawQuery,
222 headers: HeaderMap,
223 body: axum::body::Bytes| async move {
224 tracing::debug!("Handling OpenAPI request: {} {}", method, path_template);
225
226 let scenario = headers
229 .get("X-Mockforge-Scenario")
230 .and_then(|v| v.to_str().ok())
231 .map(|s| s.to_string())
232 .or_else(|| std::env::var("MOCKFORGE_HTTP_SCENARIO").ok());
233
234 let (selected_status, mock_response) =
236 route_clone.mock_response_with_status_and_scenario(scenario.as_deref());
237 let mut path_map = serde_json::Map::new();
240 for (k, v) in path_params {
241 path_map.insert(k, Value::String(v));
242 }
243
244 let mut query_map = Map::new();
246 if let Some(q) = raw_query {
247 for (k, v) in url::form_urlencoded::parse(q.as_bytes()) {
248 query_map.insert(k.to_string(), Value::String(v.to_string()));
249 }
250 }
251
252 let mut header_map = Map::new();
254 for p_ref in &operation.parameters {
255 if let Some(openapiv3::Parameter::Header { parameter_data, .. }) =
256 p_ref.as_item()
257 {
258 let name_lc = parameter_data.name.to_ascii_lowercase();
259 if let Ok(hn) = axum::http::HeaderName::from_bytes(name_lc.as_bytes()) {
260 if let Some(val) = headers.get(hn) {
261 if let Ok(s) = val.to_str() {
262 header_map.insert(
263 parameter_data.name.clone(),
264 Value::String(s.to_string()),
265 );
266 }
267 }
268 }
269 }
270 }
271
272 let mut cookie_map = Map::new();
274 if let Some(val) = headers.get(axum::http::header::COOKIE) {
275 if let Ok(s) = val.to_str() {
276 for part in s.split(';') {
277 let part = part.trim();
278 if let Some((k, v)) = part.split_once('=') {
279 cookie_map.insert(k.to_string(), Value::String(v.to_string()));
280 }
281 }
282 }
283 }
284
285 let is_multipart = headers
287 .get(axum::http::header::CONTENT_TYPE)
288 .and_then(|v| v.to_str().ok())
289 .map(|ct| ct.starts_with("multipart/form-data"))
290 .unwrap_or(false);
291
292 let mut multipart_fields = std::collections::HashMap::new();
294 let mut multipart_files = std::collections::HashMap::new();
295 let mut body_json: Option<Value> = None;
296
297 if is_multipart {
298 match extract_multipart_from_bytes(&body, &headers).await {
300 Ok((fields, files)) => {
301 multipart_fields = fields;
302 multipart_files = files;
303 let mut body_obj = serde_json::Map::new();
305 for (k, v) in &multipart_fields {
306 body_obj.insert(k.clone(), v.clone());
307 }
308 if !body_obj.is_empty() {
309 body_json = Some(Value::Object(body_obj));
310 }
311 }
312 Err(e) => {
313 tracing::warn!("Failed to parse multipart data: {}", e);
314 }
315 }
316 } else {
317 body_json = if !body.is_empty() {
319 serde_json::from_slice(&body).ok()
320 } else {
321 None
322 };
323 }
324
325 if let Err(e) = validator.validate_request_with_all(
326 &path_template,
327 &method,
328 &path_map,
329 &query_map,
330 &header_map,
331 &cookie_map,
332 body_json.as_ref(),
333 ) {
334 let status_code = validator.options.validation_status.unwrap_or_else(|| {
336 std::env::var("MOCKFORGE_VALIDATION_STATUS")
337 .ok()
338 .and_then(|s| s.parse::<u16>().ok())
339 .unwrap_or(400)
340 });
341
342 let payload = if status_code == 422 {
343 let empty_params = serde_json::Map::new();
347 generate_enhanced_422_response(
348 &validator,
349 &path_template,
350 &method,
351 body_json.as_ref(),
352 &empty_params, &empty_params, &empty_params, &empty_params, )
357 } else {
358 let msg = format!("{}", e);
360 let detail_val = serde_json::from_str::<serde_json::Value>(&msg)
361 .unwrap_or(serde_json::json!(msg));
362 json!({
363 "error": "request validation failed",
364 "detail": detail_val,
365 "method": method,
366 "path": path_template,
367 "timestamp": Utc::now().to_rfc3339(),
368 })
369 };
370
371 record_validation_error(&payload);
372 let status = axum::http::StatusCode::from_u16(status_code)
373 .unwrap_or(axum::http::StatusCode::BAD_REQUEST);
374
375 let body_bytes = serde_json::to_vec(&payload)
377 .unwrap_or_else(|_| br#"{"error":"Serialization failed"}"#.to_vec());
378
379 return axum::http::Response::builder()
380 .status(status)
381 .header(axum::http::header::CONTENT_TYPE, "application/json")
382 .body(axum::body::Body::from(body_bytes))
383 .expect("Response builder should create valid response with valid headers and body");
384 }
385
386 let mut final_response = mock_response.clone();
388 let env_expand = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
389 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
390 .unwrap_or(false);
391 let expand = validator.options.response_template_expand || env_expand;
392 if expand {
393 final_response = core_expand_tokens(&final_response);
394 }
395
396 if validator.options.validate_responses {
398 if let Some((status_code, _response)) = operation
400 .responses
401 .responses
402 .iter()
403 .filter_map(|(status, resp)| match status {
404 openapiv3::StatusCode::Code(code) if *code >= 200 && *code < 300 => {
405 resp.as_item().map(|r| ((*code), r))
406 }
407 openapiv3::StatusCode::Range(range)
408 if *range >= 200 && *range < 300 =>
409 {
410 resp.as_item().map(|r| (200, r))
411 }
412 _ => None,
413 })
414 .next()
415 {
416 if serde_json::from_value::<serde_json::Value>(final_response.clone())
418 .is_err()
419 {
420 tracing::warn!(
421 "Response validation failed: invalid JSON for status {}",
422 status_code
423 );
424 }
425 }
426 }
427
428 let mut response = Json(final_response).into_response();
430 *response.status_mut() = axum::http::StatusCode::from_u16(selected_status)
431 .unwrap_or(axum::http::StatusCode::OK);
432 response
433 };
434
435 router = match route.method.as_str() {
437 "GET" => router.route(&axum_path, get(handler)),
438 "POST" => router.route(&axum_path, post(handler)),
439 "PUT" => router.route(&axum_path, put(handler)),
440 "DELETE" => router.route(&axum_path, delete(handler)),
441 "PATCH" => router.route(&axum_path, patch(handler)),
442 "HEAD" => router.route(&axum_path, head(handler)),
443 "OPTIONS" => router.route(&axum_path, options(handler)),
444 _ => router, };
446 }
447
448 let spec_json = serde_json::to_value(&self.spec.spec).unwrap_or(Value::Null);
450 router = router.route("/openapi.json", get(move || async move { Json(spec_json) }));
451
452 router
453 }
454
455 pub fn build_router_with_latency(self, latency_injector: LatencyInjector) -> Router {
457 self.build_router_with_injectors(latency_injector, None)
458 }
459
460 pub fn build_router_with_injectors(
462 self,
463 latency_injector: LatencyInjector,
464 failure_injector: Option<crate::FailureInjector>,
465 ) -> Router {
466 self.build_router_with_injectors_and_overrides(
467 latency_injector,
468 failure_injector,
469 None,
470 false,
471 )
472 }
473
474 pub fn build_router_with_injectors_and_overrides(
476 self,
477 latency_injector: LatencyInjector,
478 failure_injector: Option<crate::FailureInjector>,
479 overrides: Option<Overrides>,
480 overrides_enabled: bool,
481 ) -> Router {
482 let mut router = Router::new();
483
484 for route in &self.routes {
486 let axum_path = route.axum_path();
487 let operation = route.operation.clone();
488 let method = route.method.clone();
489 let method_str = method.clone();
490 let method_for_router = method_str.clone();
491 let path_template = route.path.clone();
492 let validator = self.clone_for_validation();
493 let route_clone = route.clone();
494 let injector = latency_injector.clone();
495 let failure_injector = failure_injector.clone();
496 let route_overrides = overrides.clone();
497
498 let mut operation_tags = operation.tags.clone();
500 if let Some(operation_id) = &operation.operation_id {
501 operation_tags.push(operation_id.clone());
502 }
503
504 let handler = move |AxumPath(path_params): AxumPath<
506 std::collections::HashMap<String, String>,
507 >,
508 RawQuery(raw_query): RawQuery,
509 headers: HeaderMap,
510 body: axum::body::Bytes| async move {
511 if let Some(ref failure_injector) = failure_injector {
513 if let Some((status_code, error_message)) =
514 failure_injector.process_request(&operation_tags)
515 {
516 return (
517 axum::http::StatusCode::from_u16(status_code)
518 .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR),
519 axum::Json(serde_json::json!({
520 "error": error_message,
521 "injected_failure": true
522 })),
523 );
524 }
525 }
526
527 if let Err(e) = injector.inject_latency(&operation_tags).await {
529 tracing::warn!("Failed to inject latency: {}", e);
530 }
531
532 let scenario = headers
535 .get("X-Mockforge-Scenario")
536 .and_then(|v| v.to_str().ok())
537 .map(|s| s.to_string())
538 .or_else(|| std::env::var("MOCKFORGE_HTTP_SCENARIO").ok());
539
540 let mut path_map = Map::new();
543 for (k, v) in path_params {
544 path_map.insert(k, Value::String(v));
545 }
546
547 let mut query_map = Map::new();
549 if let Some(q) = raw_query {
550 for (k, v) in url::form_urlencoded::parse(q.as_bytes()) {
551 query_map.insert(k.to_string(), Value::String(v.to_string()));
552 }
553 }
554
555 let mut header_map = Map::new();
557 for p_ref in &operation.parameters {
558 if let Some(openapiv3::Parameter::Header { parameter_data, .. }) =
559 p_ref.as_item()
560 {
561 let name_lc = parameter_data.name.to_ascii_lowercase();
562 if let Ok(hn) = axum::http::HeaderName::from_bytes(name_lc.as_bytes()) {
563 if let Some(val) = headers.get(hn) {
564 if let Ok(s) = val.to_str() {
565 header_map.insert(
566 parameter_data.name.clone(),
567 Value::String(s.to_string()),
568 );
569 }
570 }
571 }
572 }
573 }
574
575 let mut cookie_map = Map::new();
577 if let Some(val) = headers.get(axum::http::header::COOKIE) {
578 if let Ok(s) = val.to_str() {
579 for part in s.split(';') {
580 let part = part.trim();
581 if let Some((k, v)) = part.split_once('=') {
582 cookie_map.insert(k.to_string(), Value::String(v.to_string()));
583 }
584 }
585 }
586 }
587
588 let is_multipart = headers
590 .get(axum::http::header::CONTENT_TYPE)
591 .and_then(|v| v.to_str().ok())
592 .map(|ct| ct.starts_with("multipart/form-data"))
593 .unwrap_or(false);
594
595 let mut multipart_fields = std::collections::HashMap::new();
597 let mut multipart_files = std::collections::HashMap::new();
598 let mut body_json: Option<Value> = None;
599
600 if is_multipart {
601 match extract_multipart_from_bytes(&body, &headers).await {
603 Ok((fields, files)) => {
604 multipart_fields = fields;
605 multipart_files = files;
606 let mut body_obj = serde_json::Map::new();
608 for (k, v) in &multipart_fields {
609 body_obj.insert(k.clone(), v.clone());
610 }
611 if !body_obj.is_empty() {
612 body_json = Some(Value::Object(body_obj));
613 }
614 }
615 Err(e) => {
616 tracing::warn!("Failed to parse multipart data: {}", e);
617 }
618 }
619 } else {
620 body_json = if !body.is_empty() {
622 serde_json::from_slice(&body).ok()
623 } else {
624 None
625 };
626 }
627
628 if let Err(e) = validator.validate_request_with_all(
629 &path_template,
630 &method_str,
631 &path_map,
632 &query_map,
633 &header_map,
634 &cookie_map,
635 body_json.as_ref(),
636 ) {
637 let msg = format!("{}", e);
638 let detail_val = serde_json::from_str::<serde_json::Value>(&msg)
639 .unwrap_or(serde_json::json!(msg));
640 let payload = serde_json::json!({
641 "error": "request validation failed",
642 "detail": detail_val,
643 "method": method_str,
644 "path": path_template,
645 "timestamp": Utc::now().to_rfc3339(),
646 });
647 record_validation_error(&payload);
648 let status_code = validator.options.validation_status.unwrap_or_else(|| {
650 std::env::var("MOCKFORGE_VALIDATION_STATUS")
651 .ok()
652 .and_then(|s| s.parse::<u16>().ok())
653 .unwrap_or(400)
654 });
655 return (
656 axum::http::StatusCode::from_u16(status_code)
657 .unwrap_or(axum::http::StatusCode::BAD_REQUEST),
658 Json(payload),
659 );
660 }
661
662 let (selected_status, mock_response) =
664 route_clone.mock_response_with_status_and_scenario(scenario.as_deref());
665
666 let mut response = mock_response.clone();
668 let env_expand = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
669 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
670 .unwrap_or(false);
671 let expand = validator.options.response_template_expand || env_expand;
672 if expand {
673 response = core_expand_tokens(&response);
674 }
675
676 if let Some(ref overrides) = route_overrides {
678 if overrides_enabled {
679 let operation_tags =
681 operation.operation_id.clone().map(|id| vec![id]).unwrap_or_default();
682 overrides.apply(
683 &operation.operation_id.unwrap_or_default(),
684 &operation_tags,
685 &path_template,
686 &mut response,
687 );
688 }
689 }
690
691 (
693 axum::http::StatusCode::from_u16(selected_status)
694 .unwrap_or(axum::http::StatusCode::OK),
695 Json(response),
696 )
697 };
698
699 router = match method_for_router.as_str() {
701 "GET" => router.route(&axum_path, get(handler)),
702 "POST" => router.route(&axum_path, post(handler)),
703 "PUT" => router.route(&axum_path, put(handler)),
704 "PATCH" => router.route(&axum_path, patch(handler)),
705 "DELETE" => router.route(&axum_path, delete(handler)),
706 "HEAD" => router.route(&axum_path, head(handler)),
707 "OPTIONS" => router.route(&axum_path, options(handler)),
708 _ => router.route(&axum_path, get(handler)), };
710 }
711
712 let spec_json = serde_json::to_value(&self.spec.spec).unwrap_or(Value::Null);
714 router = router.route("/openapi.json", get(move || async move { Json(spec_json) }));
715
716 router
717 }
718
719 pub fn get_route(&self, path: &str, method: &str) -> Option<&OpenApiRoute> {
721 self.routes.iter().find(|route| route.path == path && route.method == method)
722 }
723
724 pub fn get_routes_for_path(&self, path: &str) -> Vec<&OpenApiRoute> {
726 self.routes.iter().filter(|route| route.path == path).collect()
727 }
728
729 pub fn validate_request(&self, path: &str, method: &str, body: Option<&Value>) -> Result<()> {
731 self.validate_request_with(path, method, &Map::new(), &Map::new(), body)
732 }
733
734 pub fn validate_request_with(
736 &self,
737 path: &str,
738 method: &str,
739 path_params: &Map<String, Value>,
740 query_params: &Map<String, Value>,
741 body: Option<&Value>,
742 ) -> Result<()> {
743 self.validate_request_with_all(
744 path,
745 method,
746 path_params,
747 query_params,
748 &Map::new(),
749 &Map::new(),
750 body,
751 )
752 }
753
754 #[allow(clippy::too_many_arguments)]
756 pub fn validate_request_with_all(
757 &self,
758 path: &str,
759 method: &str,
760 path_params: &Map<String, Value>,
761 query_params: &Map<String, Value>,
762 header_params: &Map<String, Value>,
763 cookie_params: &Map<String, Value>,
764 body: Option<&Value>,
765 ) -> Result<()> {
766 for pref in &self.options.admin_skip_prefixes {
768 if !pref.is_empty() && path.starts_with(pref) {
769 return Ok(());
770 }
771 }
772 let env_mode = std::env::var("MOCKFORGE_REQUEST_VALIDATION").ok().map(|v| {
774 match v.to_ascii_lowercase().as_str() {
775 "off" | "disable" | "disabled" => ValidationMode::Disabled,
776 "warn" | "warning" => ValidationMode::Warn,
777 _ => ValidationMode::Enforce,
778 }
779 });
780 let aggregate = std::env::var("MOCKFORGE_AGGREGATE_ERRORS")
781 .ok()
782 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
783 .unwrap_or(self.options.aggregate_errors);
784 let env_overrides: Option<serde_json::Map<String, serde_json::Value>> =
786 std::env::var("MOCKFORGE_VALIDATION_OVERRIDES_JSON")
787 .ok()
788 .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
789 .and_then(|v| v.as_object().cloned());
790 let mut effective_mode = env_mode.unwrap_or(self.options.request_mode.clone());
792 if let Some(map) = &env_overrides {
794 if let Some(v) = map.get(&format!("{} {}", method, path)) {
795 if let Some(m) = v.as_str() {
796 effective_mode = match m {
797 "off" => ValidationMode::Disabled,
798 "warn" => ValidationMode::Warn,
799 _ => ValidationMode::Enforce,
800 };
801 }
802 }
803 }
804 if let Some(override_mode) = self.options.overrides.get(&format!("{} {}", method, path)) {
806 effective_mode = override_mode.clone();
807 }
808 if matches!(effective_mode, ValidationMode::Disabled) {
809 return Ok(());
810 }
811 if let Some(route) = self.get_route(path, method) {
812 if matches!(effective_mode, ValidationMode::Disabled) {
813 return Ok(());
814 }
815 let mut errors: Vec<String> = Vec::new();
816 let mut details: Vec<serde_json::Value> = Vec::new();
817 if let Some(schema) = &route.operation.request_body {
819 if let Some(value) = body {
820 let request_body = match schema {
822 openapiv3::ReferenceOr::Item(rb) => Some(rb),
823 openapiv3::ReferenceOr::Reference { reference } => {
824 self.spec
826 .spec
827 .components
828 .as_ref()
829 .and_then(|components| {
830 components.request_bodies.get(
831 reference.trim_start_matches("#/components/requestBodies/"),
832 )
833 })
834 .and_then(|rb_ref| rb_ref.as_item())
835 }
836 };
837
838 if let Some(rb) = request_body {
839 if let Some(content) = rb.content.get("application/json") {
840 if let Some(schema_ref) = &content.schema {
841 match schema_ref {
843 openapiv3::ReferenceOr::Item(schema) => {
844 if let Err(validation_error) =
846 OpenApiSchema::new(schema.clone()).validate(value)
847 {
848 let error_msg = validation_error.to_string();
849 errors.push(format!(
850 "body validation failed: {}",
851 error_msg
852 ));
853 if aggregate {
854 details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
855 }
856 }
857 }
858 openapiv3::ReferenceOr::Reference { reference } => {
859 if let Some(resolved_schema_ref) =
861 self.spec.get_schema(reference)
862 {
863 if let Err(validation_error) = OpenApiSchema::new(
864 resolved_schema_ref.schema.clone(),
865 )
866 .validate(value)
867 {
868 let error_msg = validation_error.to_string();
869 errors.push(format!(
870 "body validation failed: {}",
871 error_msg
872 ));
873 if aggregate {
874 details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
875 }
876 }
877 } else {
878 errors.push(format!("body validation failed: could not resolve schema reference {}", reference));
880 if aggregate {
881 details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve schema reference"}));
882 }
883 }
884 }
885 }
886 }
887 }
888 } else {
889 errors.push("body validation failed: could not resolve request body or no application/json content".to_string());
891 if aggregate {
892 details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve request body reference"}));
893 }
894 }
895 } else {
896 errors.push("body: Request body is required but not provided".to_string());
897 details.push(serde_json::json!({"path":"body","code":"required","message":"Request body is required"}));
898 }
899 } else if body.is_some() {
900 tracing::debug!("Body provided for operation without requestBody; accepting");
902 }
903
904 for p_ref in &route.operation.parameters {
906 if let Some(p) = p_ref.as_item() {
907 match p {
908 openapiv3::Parameter::Path { parameter_data, .. } => {
909 validate_parameter(
910 parameter_data,
911 path_params,
912 "path",
913 aggregate,
914 &mut errors,
915 &mut details,
916 );
917 }
918 openapiv3::Parameter::Query {
919 parameter_data,
920 style,
921 ..
922 } => {
923 let deep_value = None; let style_str = match style {
926 openapiv3::QueryStyle::Form => Some("form"),
927 openapiv3::QueryStyle::SpaceDelimited => Some("spaceDelimited"),
928 openapiv3::QueryStyle::PipeDelimited => Some("pipeDelimited"),
929 openapiv3::QueryStyle::DeepObject => Some("deepObject"),
930 };
931 validate_parameter_with_deep_object(
932 parameter_data,
933 query_params,
934 "query",
935 deep_value,
936 style_str,
937 aggregate,
938 &mut errors,
939 &mut details,
940 );
941 }
942 openapiv3::Parameter::Header { parameter_data, .. } => {
943 validate_parameter(
944 parameter_data,
945 header_params,
946 "header",
947 aggregate,
948 &mut errors,
949 &mut details,
950 );
951 }
952 openapiv3::Parameter::Cookie { parameter_data, .. } => {
953 validate_parameter(
954 parameter_data,
955 cookie_params,
956 "cookie",
957 aggregate,
958 &mut errors,
959 &mut details,
960 );
961 }
962 }
963 }
964 }
965 if errors.is_empty() {
966 return Ok(());
967 }
968 match effective_mode {
969 ValidationMode::Disabled => Ok(()),
970 ValidationMode::Warn => {
971 tracing::warn!("Request validation warnings: {:?}", errors);
972 Ok(())
973 }
974 ValidationMode::Enforce => Err(Error::validation(
975 serde_json::json!({"errors": errors, "details": details}).to_string(),
976 )),
977 }
978 } else {
979 Err(Error::generic(format!("Route {} {} not found in OpenAPI spec", method, path)))
980 }
981 }
982
983 pub fn paths(&self) -> Vec<String> {
987 let mut paths: Vec<String> = self.routes.iter().map(|route| route.path.clone()).collect();
988 paths.sort();
989 paths.dedup();
990 paths
991 }
992
993 pub fn methods(&self) -> Vec<String> {
995 let mut methods: Vec<String> =
996 self.routes.iter().map(|route| route.method.clone()).collect();
997 methods.sort();
998 methods.dedup();
999 methods
1000 }
1001
1002 pub fn get_operation(&self, path: &str, method: &str) -> Option<OpenApiOperation> {
1004 self.get_route(path, method).map(|route| {
1005 OpenApiOperation::from_operation(
1006 &route.method,
1007 route.path.clone(),
1008 &route.operation,
1009 &self.spec,
1010 )
1011 })
1012 }
1013
1014 pub fn extract_path_parameters(
1016 &self,
1017 path: &str,
1018 method: &str,
1019 ) -> std::collections::HashMap<String, String> {
1020 for route in &self.routes {
1021 if route.method != method {
1022 continue;
1023 }
1024
1025 if let Some(params) = self.match_path_to_route(path, &route.path) {
1026 return params;
1027 }
1028 }
1029 std::collections::HashMap::new()
1030 }
1031
1032 fn match_path_to_route(
1034 &self,
1035 request_path: &str,
1036 route_pattern: &str,
1037 ) -> Option<std::collections::HashMap<String, String>> {
1038 let mut params = std::collections::HashMap::new();
1039
1040 let request_segments: Vec<&str> = request_path.trim_start_matches('/').split('/').collect();
1042 let pattern_segments: Vec<&str> =
1043 route_pattern.trim_start_matches('/').split('/').collect();
1044
1045 if request_segments.len() != pattern_segments.len() {
1046 return None;
1047 }
1048
1049 for (req_seg, pat_seg) in request_segments.iter().zip(pattern_segments.iter()) {
1050 if pat_seg.starts_with('{') && pat_seg.ends_with('}') {
1051 let param_name = &pat_seg[1..pat_seg.len() - 1];
1053 params.insert(param_name.to_string(), req_seg.to_string());
1054 } else if req_seg != pat_seg {
1055 return None;
1057 }
1058 }
1059
1060 Some(params)
1061 }
1062
1063 pub fn convert_path_to_axum(openapi_path: &str) -> String {
1066 openapi_path.to_string()
1068 }
1069
1070 pub fn build_router_with_ai(
1072 &self,
1073 ai_generator: Option<std::sync::Arc<dyn AiGenerator + Send + Sync>>,
1074 ) -> Router {
1075 use axum::routing::{delete, get, patch, post, put};
1076
1077 let mut router = Router::new();
1078 tracing::debug!("Building router with AI support from {} routes", self.routes.len());
1079
1080 for route in &self.routes {
1081 tracing::debug!("Adding AI-enabled route: {} {}", route.method, route.path);
1082
1083 let route_clone = route.clone();
1084 let ai_generator_clone = ai_generator.clone();
1085
1086 let handler = move |headers: HeaderMap, body: Option<Json<Value>>| {
1088 let route = route_clone.clone();
1089 let ai_generator = ai_generator_clone.clone();
1090
1091 async move {
1092 tracing::debug!(
1093 "Handling AI request for route: {} {}",
1094 route.method,
1095 route.path
1096 );
1097
1098 let mut context = RequestContext::new(route.method.clone(), route.path.clone());
1100
1101 context.headers = headers
1103 .iter()
1104 .map(|(k, v)| {
1105 (k.to_string(), Value::String(v.to_str().unwrap_or("").to_string()))
1106 })
1107 .collect();
1108
1109 context.body = body.map(|Json(b)| b);
1111
1112 let (status, response) = if let (Some(generator), Some(_ai_config)) =
1114 (ai_generator, &route.ai_config)
1115 {
1116 route
1117 .mock_response_with_status_async(&context, Some(generator.as_ref()))
1118 .await
1119 } else {
1120 route.mock_response_with_status()
1122 };
1123
1124 (
1125 axum::http::StatusCode::from_u16(status)
1126 .unwrap_or(axum::http::StatusCode::OK),
1127 axum::response::Json(response),
1128 )
1129 }
1130 };
1131
1132 match route.method.as_str() {
1133 "GET" => {
1134 router = router.route(&route.path, get(handler));
1135 }
1136 "POST" => {
1137 router = router.route(&route.path, post(handler));
1138 }
1139 "PUT" => {
1140 router = router.route(&route.path, put(handler));
1141 }
1142 "DELETE" => {
1143 router = router.route(&route.path, delete(handler));
1144 }
1145 "PATCH" => {
1146 router = router.route(&route.path, patch(handler));
1147 }
1148 _ => tracing::warn!("Unsupported HTTP method for AI: {}", route.method),
1149 }
1150 }
1151
1152 router
1153 }
1154
1155 pub fn build_router_with_mockai(
1166 &self,
1167 mockai: Option<std::sync::Arc<tokio::sync::RwLock<crate::intelligent_behavior::MockAI>>>,
1168 ) -> Router {
1169 use crate::intelligent_behavior::{Request as MockAIRequest, Response as MockAIResponse};
1170 use axum::extract::Query;
1171 use axum::routing::{delete, get, patch, post, put};
1172
1173 let mut router = Router::new();
1174 tracing::debug!("Building router with MockAI support from {} routes", self.routes.len());
1175
1176 for route in &self.routes {
1177 tracing::debug!("Adding MockAI-enabled route: {} {}", route.method, route.path);
1178
1179 let route_clone = route.clone();
1180 let mockai_clone = mockai.clone();
1181
1182 let handler = move |query: axum::extract::Query<HashMap<String, String>>,
1186 headers: HeaderMap,
1187 body: Option<Json<Value>>| {
1188 let route = route_clone.clone();
1189 let mockai = mockai_clone.clone();
1190
1191 async move {
1192 tracing::debug!(
1193 "Handling MockAI request for route: {} {}",
1194 route.method,
1195 route.path
1196 );
1197
1198 let mockai_query = query.0;
1200
1201 if let Some(mockai_arc) = mockai {
1203 let mockai_guard = mockai_arc.read().await;
1204
1205 let mut mockai_headers = HashMap::new();
1207 for (k, v) in headers.iter() {
1208 mockai_headers
1209 .insert(k.to_string(), v.to_str().unwrap_or("").to_string());
1210 }
1211
1212 let mockai_request = MockAIRequest {
1213 method: route.method.clone(),
1214 path: route.path.clone(),
1215 body: body.as_ref().map(|Json(b)| b.clone()),
1216 query_params: mockai_query,
1217 headers: mockai_headers,
1218 };
1219
1220 match mockai_guard.process_request(&mockai_request).await {
1222 Ok(mockai_response) => {
1223 tracing::debug!(
1224 "MockAI generated response with status: {}",
1225 mockai_response.status_code
1226 );
1227 return (
1228 axum::http::StatusCode::from_u16(mockai_response.status_code)
1229 .unwrap_or(axum::http::StatusCode::OK),
1230 axum::response::Json(mockai_response.body),
1231 );
1232 }
1233 Err(e) => {
1234 tracing::warn!(
1235 "MockAI processing failed for {} {}: {}, falling back to standard response",
1236 route.method,
1237 route.path,
1238 e
1239 );
1240 }
1242 }
1243 }
1244
1245 let (status, response) = route.mock_response_with_status();
1247 (
1248 axum::http::StatusCode::from_u16(status)
1249 .unwrap_or(axum::http::StatusCode::OK),
1250 axum::response::Json(response),
1251 )
1252 }
1253 };
1254
1255 match route.method.as_str() {
1256 "GET" => {
1257 router = router.route(&route.path, get(handler));
1258 }
1259 "POST" => {
1260 router = router.route(&route.path, post(handler));
1261 }
1262 "PUT" => {
1263 router = router.route(&route.path, put(handler));
1264 }
1265 "DELETE" => {
1266 router = router.route(&route.path, delete(handler));
1267 }
1268 "PATCH" => {
1269 router = router.route(&route.path, patch(handler));
1270 }
1271 _ => tracing::warn!("Unsupported HTTP method for MockAI: {}", route.method),
1272 }
1273 }
1274
1275 router
1276 }
1277}
1278
1279async fn extract_multipart_from_bytes(
1284 body: &axum::body::Bytes,
1285 headers: &HeaderMap,
1286) -> Result<(
1287 std::collections::HashMap<String, Value>,
1288 std::collections::HashMap<String, String>,
1289)> {
1290 let boundary = headers
1292 .get(axum::http::header::CONTENT_TYPE)
1293 .and_then(|v| v.to_str().ok())
1294 .and_then(|ct| {
1295 ct.split(';').find_map(|part| {
1296 let part = part.trim();
1297 if part.starts_with("boundary=") {
1298 Some(part.strip_prefix("boundary=").unwrap_or("").trim_matches('"'))
1299 } else {
1300 None
1301 }
1302 })
1303 })
1304 .ok_or_else(|| Error::generic("Missing boundary in Content-Type header"))?;
1305
1306 let mut fields = std::collections::HashMap::new();
1307 let mut files = std::collections::HashMap::new();
1308
1309 let boundary_prefix = format!("--{}", boundary).into_bytes();
1312 let boundary_line = format!("\r\n--{}\r\n", boundary).into_bytes();
1313 let end_boundary = format!("\r\n--{}--\r\n", boundary).into_bytes();
1314
1315 let mut pos = 0;
1317 let mut parts = Vec::new();
1318
1319 if body.starts_with(&boundary_prefix) {
1321 if let Some(first_crlf) = body.iter().position(|&b| b == b'\r') {
1322 pos = first_crlf + 2; }
1324 }
1325
1326 while let Some(boundary_pos) = body[pos..]
1328 .windows(boundary_line.len())
1329 .position(|window| window == boundary_line.as_slice())
1330 {
1331 let actual_pos = pos + boundary_pos;
1332 if actual_pos > pos {
1333 parts.push((pos, actual_pos));
1334 }
1335 pos = actual_pos + boundary_line.len();
1336 }
1337
1338 if let Some(end_pos) = body[pos..]
1340 .windows(end_boundary.len())
1341 .position(|window| window == end_boundary.as_slice())
1342 {
1343 let actual_end = pos + end_pos;
1344 if actual_end > pos {
1345 parts.push((pos, actual_end));
1346 }
1347 } else if pos < body.len() {
1348 parts.push((pos, body.len()));
1350 }
1351
1352 for (start, end) in parts {
1354 let part_data = &body[start..end];
1355
1356 let separator = b"\r\n\r\n";
1358 if let Some(sep_pos) =
1359 part_data.windows(separator.len()).position(|window| window == separator)
1360 {
1361 let header_bytes = &part_data[..sep_pos];
1362 let body_start = sep_pos + separator.len();
1363 let body_data = &part_data[body_start..];
1364
1365 let header_str = String::from_utf8_lossy(header_bytes);
1367 let mut field_name = None;
1368 let mut filename = None;
1369
1370 for header_line in header_str.lines() {
1371 if header_line.starts_with("Content-Disposition:") {
1372 if let Some(name_start) = header_line.find("name=\"") {
1374 let name_start = name_start + 6;
1375 if let Some(name_end) = header_line[name_start..].find('"') {
1376 field_name =
1377 Some(header_line[name_start..name_start + name_end].to_string());
1378 }
1379 }
1380
1381 if let Some(file_start) = header_line.find("filename=\"") {
1383 let file_start = file_start + 10;
1384 if let Some(file_end) = header_line[file_start..].find('"') {
1385 filename =
1386 Some(header_line[file_start..file_start + file_end].to_string());
1387 }
1388 }
1389 }
1390 }
1391
1392 if let Some(name) = field_name {
1393 if let Some(file) = filename {
1394 let temp_dir = std::env::temp_dir().join("mockforge-uploads");
1396 std::fs::create_dir_all(&temp_dir).map_err(|e| {
1397 Error::generic(format!("Failed to create temp directory: {}", e))
1398 })?;
1399
1400 let file_path = temp_dir.join(format!("{}_{}", uuid::Uuid::new_v4(), file));
1401 std::fs::write(&file_path, body_data)
1402 .map_err(|e| Error::generic(format!("Failed to write file: {}", e)))?;
1403
1404 let file_path_str = file_path.to_string_lossy().to_string();
1405 files.insert(name.clone(), file_path_str.clone());
1406 fields.insert(name, Value::String(file_path_str));
1407 } else {
1408 let body_str = body_data
1411 .strip_suffix(b"\r\n")
1412 .or_else(|| body_data.strip_suffix(b"\n"))
1413 .unwrap_or(body_data);
1414
1415 if let Ok(field_value) = String::from_utf8(body_str.to_vec()) {
1416 fields.insert(name, Value::String(field_value.trim().to_string()));
1417 } else {
1418 use base64::{engine::general_purpose, Engine as _};
1420 fields.insert(
1421 name,
1422 Value::String(general_purpose::STANDARD.encode(body_str)),
1423 );
1424 }
1425 }
1426 }
1427 }
1428 }
1429
1430 Ok((fields, files))
1431}
1432
1433static LAST_ERRORS: Lazy<Mutex<VecDeque<serde_json::Value>>> =
1434 Lazy::new(|| Mutex::new(VecDeque::with_capacity(20)));
1435
1436pub fn record_validation_error(v: &serde_json::Value) {
1438 if let Ok(mut q) = LAST_ERRORS.lock() {
1439 if q.len() >= 20 {
1440 q.pop_front();
1441 }
1442 q.push_back(v.clone());
1443 }
1444 }
1446
1447pub fn get_last_validation_error() -> Option<serde_json::Value> {
1449 LAST_ERRORS.lock().ok()?.back().cloned()
1450}
1451
1452pub fn get_validation_errors() -> Vec<serde_json::Value> {
1454 LAST_ERRORS.lock().map(|q| q.iter().cloned().collect()).unwrap_or_default()
1455}
1456
1457fn coerce_value_for_schema(value: &Value, schema: &openapiv3::Schema) -> Value {
1462 match value {
1464 Value::String(s) => {
1465 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
1467 &schema.schema_kind
1468 {
1469 if s.contains(',') {
1470 let parts: Vec<&str> = s.split(',').map(|s| s.trim()).collect();
1472 let mut array_values = Vec::new();
1473
1474 for part in parts {
1475 if let Some(items_schema) = &array_type.items {
1477 if let Some(items_schema_obj) = items_schema.as_item() {
1478 let part_value = Value::String(part.to_string());
1479 let coerced_part =
1480 coerce_value_for_schema(&part_value, items_schema_obj);
1481 array_values.push(coerced_part);
1482 } else {
1483 array_values.push(Value::String(part.to_string()));
1485 }
1486 } else {
1487 array_values.push(Value::String(part.to_string()));
1489 }
1490 }
1491 return Value::Array(array_values);
1492 }
1493 }
1494
1495 match &schema.schema_kind {
1497 openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
1498 value.clone()
1500 }
1501 openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => {
1502 if let Ok(n) = s.parse::<f64>() {
1504 if let Some(num) = serde_json::Number::from_f64(n) {
1505 return Value::Number(num);
1506 }
1507 }
1508 value.clone()
1509 }
1510 openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => {
1511 if let Ok(n) = s.parse::<i64>() {
1513 if let Some(num) = serde_json::Number::from_f64(n as f64) {
1514 return Value::Number(num);
1515 }
1516 }
1517 value.clone()
1518 }
1519 openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => {
1520 match s.to_lowercase().as_str() {
1522 "true" | "1" | "yes" | "on" => Value::Bool(true),
1523 "false" | "0" | "no" | "off" => Value::Bool(false),
1524 _ => value.clone(),
1525 }
1526 }
1527 _ => {
1528 value.clone()
1530 }
1531 }
1532 }
1533 _ => value.clone(),
1534 }
1535}
1536
1537fn coerce_by_style(value: &Value, schema: &openapiv3::Schema, style: Option<&str>) -> Value {
1539 match value {
1541 Value::String(s) => {
1542 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
1544 &schema.schema_kind
1545 {
1546 let delimiter = match style {
1547 Some("spaceDelimited") => " ",
1548 Some("pipeDelimited") => "|",
1549 Some("form") | None => ",", _ => ",", };
1552
1553 if s.contains(delimiter) {
1554 let parts: Vec<&str> = s.split(delimiter).map(|s| s.trim()).collect();
1556 let mut array_values = Vec::new();
1557
1558 for part in parts {
1559 if let Some(items_schema) = &array_type.items {
1561 if let Some(items_schema_obj) = items_schema.as_item() {
1562 let part_value = Value::String(part.to_string());
1563 let coerced_part =
1564 coerce_by_style(&part_value, items_schema_obj, style);
1565 array_values.push(coerced_part);
1566 } else {
1567 array_values.push(Value::String(part.to_string()));
1569 }
1570 } else {
1571 array_values.push(Value::String(part.to_string()));
1573 }
1574 }
1575 return Value::Array(array_values);
1576 }
1577 }
1578
1579 if let Ok(n) = s.parse::<f64>() {
1581 if let Some(num) = serde_json::Number::from_f64(n) {
1582 return Value::Number(num);
1583 }
1584 }
1585 match s.to_lowercase().as_str() {
1587 "true" | "1" | "yes" | "on" => return Value::Bool(true),
1588 "false" | "0" | "no" | "off" => return Value::Bool(false),
1589 _ => {}
1590 }
1591 value.clone()
1593 }
1594 _ => value.clone(),
1595 }
1596}
1597
1598fn build_deep_object(name: &str, params: &Map<String, Value>) -> Option<Value> {
1600 let prefix = format!("{}[", name);
1601 let mut obj = Map::new();
1602 for (k, v) in params.iter() {
1603 if let Some(rest) = k.strip_prefix(&prefix) {
1604 if let Some(key) = rest.strip_suffix(']') {
1605 obj.insert(key.to_string(), v.clone());
1606 }
1607 }
1608 }
1609 if obj.is_empty() {
1610 None
1611 } else {
1612 Some(Value::Object(obj))
1613 }
1614}
1615
1616#[allow(clippy::too_many_arguments)]
1622fn generate_enhanced_422_response(
1623 validator: &OpenApiRouteRegistry,
1624 path_template: &str,
1625 method: &str,
1626 body: Option<&Value>,
1627 path_params: &serde_json::Map<String, Value>,
1628 query_params: &serde_json::Map<String, Value>,
1629 header_params: &serde_json::Map<String, Value>,
1630 cookie_params: &serde_json::Map<String, Value>,
1631) -> Value {
1632 let mut field_errors = Vec::new();
1633
1634 if let Some(route) = validator.get_route(path_template, method) {
1636 if let Some(schema) = &route.operation.request_body {
1638 if let Some(value) = body {
1639 if let Some(content) =
1640 schema.as_item().and_then(|rb| rb.content.get("application/json"))
1641 {
1642 if let Some(_schema_ref) = &content.schema {
1643 if serde_json::from_value::<serde_json::Value>(value.clone()).is_err() {
1645 field_errors.push(json!({
1646 "path": "body",
1647 "message": "invalid JSON"
1648 }));
1649 }
1650 }
1651 }
1652 } else {
1653 field_errors.push(json!({
1654 "path": "body",
1655 "expected": "object",
1656 "found": "missing",
1657 "message": "Request body is required but not provided"
1658 }));
1659 }
1660 }
1661
1662 for param_ref in &route.operation.parameters {
1664 if let Some(param) = param_ref.as_item() {
1665 match param {
1666 openapiv3::Parameter::Path { parameter_data, .. } => {
1667 validate_parameter_detailed(
1668 parameter_data,
1669 path_params,
1670 "path",
1671 "path parameter",
1672 &mut field_errors,
1673 );
1674 }
1675 openapiv3::Parameter::Query { parameter_data, .. } => {
1676 let deep_value = if Some("form") == Some("deepObject") {
1677 build_deep_object(¶meter_data.name, query_params)
1678 } else {
1679 None
1680 };
1681 validate_parameter_detailed_with_deep(
1682 parameter_data,
1683 query_params,
1684 "query",
1685 "query parameter",
1686 deep_value,
1687 &mut field_errors,
1688 );
1689 }
1690 openapiv3::Parameter::Header { parameter_data, .. } => {
1691 validate_parameter_detailed(
1692 parameter_data,
1693 header_params,
1694 "header",
1695 "header parameter",
1696 &mut field_errors,
1697 );
1698 }
1699 openapiv3::Parameter::Cookie { parameter_data, .. } => {
1700 validate_parameter_detailed(
1701 parameter_data,
1702 cookie_params,
1703 "cookie",
1704 "cookie parameter",
1705 &mut field_errors,
1706 );
1707 }
1708 }
1709 }
1710 }
1711 }
1712
1713 json!({
1715 "error": "Schema validation failed",
1716 "details": field_errors,
1717 "method": method,
1718 "path": path_template,
1719 "timestamp": Utc::now().to_rfc3339(),
1720 "validation_type": "openapi_schema"
1721 })
1722}
1723
1724fn validate_parameter(
1726 parameter_data: &openapiv3::ParameterData,
1727 params_map: &Map<String, Value>,
1728 prefix: &str,
1729 aggregate: bool,
1730 errors: &mut Vec<String>,
1731 details: &mut Vec<serde_json::Value>,
1732) {
1733 match params_map.get(¶meter_data.name) {
1734 Some(v) => {
1735 if let ParameterSchemaOrContent::Schema(s) = ¶meter_data.format {
1736 if let Some(schema) = s.as_item() {
1737 let coerced = coerce_value_for_schema(v, schema);
1738 if let Err(validation_error) =
1740 OpenApiSchema::new(schema.clone()).validate(&coerced)
1741 {
1742 let error_msg = validation_error.to_string();
1743 errors.push(format!(
1744 "{} parameter '{}' validation failed: {}",
1745 prefix, parameter_data.name, error_msg
1746 ));
1747 if aggregate {
1748 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
1749 }
1750 }
1751 }
1752 }
1753 }
1754 None => {
1755 if parameter_data.required {
1756 errors.push(format!(
1757 "missing required {} parameter '{}'",
1758 prefix, parameter_data.name
1759 ));
1760 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
1761 }
1762 }
1763 }
1764}
1765
1766#[allow(clippy::too_many_arguments)]
1768fn validate_parameter_with_deep_object(
1769 parameter_data: &openapiv3::ParameterData,
1770 params_map: &Map<String, Value>,
1771 prefix: &str,
1772 deep_value: Option<Value>,
1773 style: Option<&str>,
1774 aggregate: bool,
1775 errors: &mut Vec<String>,
1776 details: &mut Vec<serde_json::Value>,
1777) {
1778 match deep_value.as_ref().or_else(|| params_map.get(¶meter_data.name)) {
1779 Some(v) => {
1780 if let ParameterSchemaOrContent::Schema(s) = ¶meter_data.format {
1781 if let Some(schema) = s.as_item() {
1782 let coerced = coerce_by_style(v, schema, style); if let Err(validation_error) =
1785 OpenApiSchema::new(schema.clone()).validate(&coerced)
1786 {
1787 let error_msg = validation_error.to_string();
1788 errors.push(format!(
1789 "{} parameter '{}' validation failed: {}",
1790 prefix, parameter_data.name, error_msg
1791 ));
1792 if aggregate {
1793 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
1794 }
1795 }
1796 }
1797 }
1798 }
1799 None => {
1800 if parameter_data.required {
1801 errors.push(format!(
1802 "missing required {} parameter '{}'",
1803 prefix, parameter_data.name
1804 ));
1805 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
1806 }
1807 }
1808 }
1809}
1810
1811fn validate_parameter_detailed(
1813 parameter_data: &openapiv3::ParameterData,
1814 params_map: &Map<String, Value>,
1815 location: &str,
1816 value_type: &str,
1817 field_errors: &mut Vec<Value>,
1818) {
1819 match params_map.get(¶meter_data.name) {
1820 Some(value) => {
1821 if let ParameterSchemaOrContent::Schema(schema) = ¶meter_data.format {
1822 let details: Vec<serde_json::Value> = Vec::new();
1824 let param_path = format!("{}.{}", location, parameter_data.name);
1825
1826 if let Some(schema_ref) = schema.as_item() {
1828 let coerced_value = coerce_value_for_schema(value, schema_ref);
1829 if let Err(validation_error) =
1831 OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
1832 {
1833 field_errors.push(json!({
1834 "path": param_path,
1835 "expected": "valid according to schema",
1836 "found": coerced_value,
1837 "message": validation_error.to_string()
1838 }));
1839 }
1840 }
1841
1842 for detail in details {
1843 field_errors.push(json!({
1844 "path": detail["path"],
1845 "expected": detail["expected_type"],
1846 "found": detail["value"],
1847 "message": detail["message"]
1848 }));
1849 }
1850 }
1851 }
1852 None => {
1853 if parameter_data.required {
1854 field_errors.push(json!({
1855 "path": format!("{}.{}", location, parameter_data.name),
1856 "expected": "value",
1857 "found": "missing",
1858 "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
1859 }));
1860 }
1861 }
1862 }
1863}
1864
1865fn validate_parameter_detailed_with_deep(
1867 parameter_data: &openapiv3::ParameterData,
1868 params_map: &Map<String, Value>,
1869 location: &str,
1870 value_type: &str,
1871 deep_value: Option<Value>,
1872 field_errors: &mut Vec<Value>,
1873) {
1874 match deep_value.as_ref().or_else(|| params_map.get(¶meter_data.name)) {
1875 Some(value) => {
1876 if let ParameterSchemaOrContent::Schema(schema) = ¶meter_data.format {
1877 let details: Vec<serde_json::Value> = Vec::new();
1879 let param_path = format!("{}.{}", location, parameter_data.name);
1880
1881 if let Some(schema_ref) = schema.as_item() {
1883 let coerced_value = coerce_by_style(value, schema_ref, Some("form")); if let Err(validation_error) =
1886 OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
1887 {
1888 field_errors.push(json!({
1889 "path": param_path,
1890 "expected": "valid according to schema",
1891 "found": coerced_value,
1892 "message": validation_error.to_string()
1893 }));
1894 }
1895 }
1896
1897 for detail in details {
1898 field_errors.push(json!({
1899 "path": detail["path"],
1900 "expected": detail["expected_type"],
1901 "found": detail["value"],
1902 "message": detail["message"]
1903 }));
1904 }
1905 }
1906 }
1907 None => {
1908 if parameter_data.required {
1909 field_errors.push(json!({
1910 "path": format!("{}.{}", location, parameter_data.name),
1911 "expected": "value",
1912 "found": "missing",
1913 "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
1914 }));
1915 }
1916 }
1917 }
1918}
1919
1920pub async fn create_registry_from_file<P: AsRef<std::path::Path>>(
1922 path: P,
1923) -> Result<OpenApiRouteRegistry> {
1924 let spec = OpenApiSpec::from_file(path).await?;
1925 spec.validate()?;
1926 Ok(OpenApiRouteRegistry::new(spec))
1927}
1928
1929pub fn create_registry_from_json(json: Value) -> Result<OpenApiRouteRegistry> {
1931 let spec = OpenApiSpec::from_json(json)?;
1932 spec.validate()?;
1933 Ok(OpenApiRouteRegistry::new(spec))
1934}
1935
1936#[cfg(test)]
1937mod tests {
1938 use super::*;
1939 use serde_json::json;
1940
1941 #[tokio::test]
1942 async fn test_registry_creation() {
1943 let spec_json = json!({
1944 "openapi": "3.0.0",
1945 "info": {
1946 "title": "Test API",
1947 "version": "1.0.0"
1948 },
1949 "paths": {
1950 "/users": {
1951 "get": {
1952 "summary": "Get users",
1953 "responses": {
1954 "200": {
1955 "description": "Success",
1956 "content": {
1957 "application/json": {
1958 "schema": {
1959 "type": "array",
1960 "items": {
1961 "type": "object",
1962 "properties": {
1963 "id": {"type": "integer"},
1964 "name": {"type": "string"}
1965 }
1966 }
1967 }
1968 }
1969 }
1970 }
1971 }
1972 },
1973 "post": {
1974 "summary": "Create user",
1975 "requestBody": {
1976 "content": {
1977 "application/json": {
1978 "schema": {
1979 "type": "object",
1980 "properties": {
1981 "name": {"type": "string"}
1982 },
1983 "required": ["name"]
1984 }
1985 }
1986 }
1987 },
1988 "responses": {
1989 "201": {
1990 "description": "Created",
1991 "content": {
1992 "application/json": {
1993 "schema": {
1994 "type": "object",
1995 "properties": {
1996 "id": {"type": "integer"},
1997 "name": {"type": "string"}
1998 }
1999 }
2000 }
2001 }
2002 }
2003 }
2004 }
2005 },
2006 "/users/{id}": {
2007 "get": {
2008 "summary": "Get user by ID",
2009 "parameters": [
2010 {
2011 "name": "id",
2012 "in": "path",
2013 "required": true,
2014 "schema": {"type": "integer"}
2015 }
2016 ],
2017 "responses": {
2018 "200": {
2019 "description": "Success",
2020 "content": {
2021 "application/json": {
2022 "schema": {
2023 "type": "object",
2024 "properties": {
2025 "id": {"type": "integer"},
2026 "name": {"type": "string"}
2027 }
2028 }
2029 }
2030 }
2031 }
2032 }
2033 }
2034 }
2035 }
2036 });
2037
2038 let registry = create_registry_from_json(spec_json).unwrap();
2039
2040 assert_eq!(registry.paths().len(), 2);
2042 assert!(registry.paths().contains(&"/users".to_string()));
2043 assert!(registry.paths().contains(&"/users/{id}".to_string()));
2044
2045 assert_eq!(registry.methods().len(), 2);
2046 assert!(registry.methods().contains(&"GET".to_string()));
2047 assert!(registry.methods().contains(&"POST".to_string()));
2048
2049 let get_users_route = registry.get_route("/users", "GET").unwrap();
2051 assert_eq!(get_users_route.method, "GET");
2052 assert_eq!(get_users_route.path, "/users");
2053
2054 let post_users_route = registry.get_route("/users", "POST").unwrap();
2055 assert_eq!(post_users_route.method, "POST");
2056 assert!(post_users_route.operation.request_body.is_some());
2057
2058 let user_by_id_route = registry.get_route("/users/{id}", "GET").unwrap();
2060 assert_eq!(user_by_id_route.axum_path(), "/users/{id}");
2061 }
2062
2063 #[tokio::test]
2064 async fn test_validate_request_with_params_and_formats() {
2065 let spec_json = json!({
2066 "openapi": "3.0.0",
2067 "info": { "title": "Test API", "version": "1.0.0" },
2068 "paths": {
2069 "/users/{id}": {
2070 "post": {
2071 "parameters": [
2072 { "name": "id", "in": "path", "required": true, "schema": {"type": "string"} },
2073 { "name": "q", "in": "query", "required": false, "schema": {"type": "integer"} }
2074 ],
2075 "requestBody": {
2076 "content": {
2077 "application/json": {
2078 "schema": {
2079 "type": "object",
2080 "required": ["email", "website"],
2081 "properties": {
2082 "email": {"type": "string", "format": "email"},
2083 "website": {"type": "string", "format": "uri"}
2084 }
2085 }
2086 }
2087 }
2088 },
2089 "responses": {"200": {"description": "ok"}}
2090 }
2091 }
2092 }
2093 });
2094
2095 let registry = create_registry_from_json(spec_json).unwrap();
2096 let mut path_params = serde_json::Map::new();
2097 path_params.insert("id".to_string(), json!("abc"));
2098 let mut query_params = serde_json::Map::new();
2099 query_params.insert("q".to_string(), json!(123));
2100
2101 let body = json!({"email":"a@b.co","website":"https://example.com"});
2103 assert!(registry
2104 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2105 .is_ok());
2106
2107 let bad_email = json!({"email":"not-an-email","website":"https://example.com"});
2109 assert!(registry
2110 .validate_request_with(
2111 "/users/{id}",
2112 "POST",
2113 &path_params,
2114 &query_params,
2115 Some(&bad_email)
2116 )
2117 .is_err());
2118
2119 let empty_path_params = serde_json::Map::new();
2121 assert!(registry
2122 .validate_request_with(
2123 "/users/{id}",
2124 "POST",
2125 &empty_path_params,
2126 &query_params,
2127 Some(&body)
2128 )
2129 .is_err());
2130 }
2131
2132 #[tokio::test]
2133 async fn test_ref_resolution_for_params_and_body() {
2134 let spec_json = json!({
2135 "openapi": "3.0.0",
2136 "info": { "title": "Ref API", "version": "1.0.0" },
2137 "components": {
2138 "schemas": {
2139 "EmailWebsite": {
2140 "type": "object",
2141 "required": ["email", "website"],
2142 "properties": {
2143 "email": {"type": "string", "format": "email"},
2144 "website": {"type": "string", "format": "uri"}
2145 }
2146 }
2147 },
2148 "parameters": {
2149 "PathId": {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}},
2150 "QueryQ": {"name": "q", "in": "query", "required": false, "schema": {"type": "integer"}}
2151 },
2152 "requestBodies": {
2153 "CreateUser": {
2154 "content": {
2155 "application/json": {
2156 "schema": {"$ref": "#/components/schemas/EmailWebsite"}
2157 }
2158 }
2159 }
2160 }
2161 },
2162 "paths": {
2163 "/users/{id}": {
2164 "post": {
2165 "parameters": [
2166 {"$ref": "#/components/parameters/PathId"},
2167 {"$ref": "#/components/parameters/QueryQ"}
2168 ],
2169 "requestBody": {"$ref": "#/components/requestBodies/CreateUser"},
2170 "responses": {"200": {"description": "ok"}}
2171 }
2172 }
2173 }
2174 });
2175
2176 let registry = create_registry_from_json(spec_json).unwrap();
2177 let mut path_params = serde_json::Map::new();
2178 path_params.insert("id".to_string(), json!("abc"));
2179 let mut query_params = serde_json::Map::new();
2180 query_params.insert("q".to_string(), json!(7));
2181
2182 let body = json!({"email":"user@example.com","website":"https://example.com"});
2183 assert!(registry
2184 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2185 .is_ok());
2186
2187 let bad = json!({"email":"nope","website":"https://example.com"});
2188 assert!(registry
2189 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&bad))
2190 .is_err());
2191 }
2192
2193 #[tokio::test]
2194 async fn test_header_cookie_and_query_coercion() {
2195 let spec_json = json!({
2196 "openapi": "3.0.0",
2197 "info": { "title": "Params API", "version": "1.0.0" },
2198 "paths": {
2199 "/items": {
2200 "get": {
2201 "parameters": [
2202 {"name": "X-Flag", "in": "header", "required": true, "schema": {"type": "boolean"}},
2203 {"name": "session", "in": "cookie", "required": true, "schema": {"type": "string"}},
2204 {"name": "ids", "in": "query", "required": false, "schema": {"type": "array", "items": {"type": "integer"}}}
2205 ],
2206 "responses": {"200": {"description": "ok"}}
2207 }
2208 }
2209 }
2210 });
2211
2212 let registry = create_registry_from_json(spec_json).unwrap();
2213
2214 let path_params = serde_json::Map::new();
2215 let mut query_params = serde_json::Map::new();
2216 query_params.insert("ids".to_string(), json!("1,2,3"));
2218 let mut header_params = serde_json::Map::new();
2219 header_params.insert("X-Flag".to_string(), json!("true"));
2220 let mut cookie_params = serde_json::Map::new();
2221 cookie_params.insert("session".to_string(), json!("abc123"));
2222
2223 assert!(registry
2224 .validate_request_with_all(
2225 "/items",
2226 "GET",
2227 &path_params,
2228 &query_params,
2229 &header_params,
2230 &cookie_params,
2231 None
2232 )
2233 .is_ok());
2234
2235 let empty_cookie = serde_json::Map::new();
2237 assert!(registry
2238 .validate_request_with_all(
2239 "/items",
2240 "GET",
2241 &path_params,
2242 &query_params,
2243 &header_params,
2244 &empty_cookie,
2245 None
2246 )
2247 .is_err());
2248
2249 let mut bad_header = serde_json::Map::new();
2251 bad_header.insert("X-Flag".to_string(), json!("notabool"));
2252 assert!(registry
2253 .validate_request_with_all(
2254 "/items",
2255 "GET",
2256 &path_params,
2257 &query_params,
2258 &bad_header,
2259 &cookie_params,
2260 None
2261 )
2262 .is_err());
2263 }
2264
2265 #[tokio::test]
2266 async fn test_query_styles_space_pipe_deepobject() {
2267 let spec_json = json!({
2268 "openapi": "3.0.0",
2269 "info": { "title": "Query Styles API", "version": "1.0.0" },
2270 "paths": {"/search": {"get": {
2271 "parameters": [
2272 {"name":"tags","in":"query","style":"spaceDelimited","schema":{"type":"array","items":{"type":"string"}}},
2273 {"name":"ids","in":"query","style":"pipeDelimited","schema":{"type":"array","items":{"type":"integer"}}},
2274 {"name":"filter","in":"query","style":"deepObject","schema":{"type":"object","properties":{"color":{"type":"string"}},"required":["color"]}}
2275 ],
2276 "responses": {"200": {"description":"ok"}}
2277 }} }
2278 });
2279
2280 let registry = create_registry_from_json(spec_json).unwrap();
2281
2282 let path_params = Map::new();
2283 let mut query = Map::new();
2284 query.insert("tags".into(), json!("alpha beta gamma"));
2285 query.insert("ids".into(), json!("1|2|3"));
2286 query.insert("filter[color]".into(), json!("red"));
2287
2288 assert!(registry
2289 .validate_request_with("/search", "GET", &path_params, &query, None)
2290 .is_ok());
2291 }
2292
2293 #[tokio::test]
2294 async fn test_oneof_anyof_allof_validation() {
2295 let spec_json = json!({
2296 "openapi": "3.0.0",
2297 "info": { "title": "Composite API", "version": "1.0.0" },
2298 "paths": {
2299 "/composite": {
2300 "post": {
2301 "requestBody": {
2302 "content": {
2303 "application/json": {
2304 "schema": {
2305 "allOf": [
2306 {"type": "object", "required": ["base"], "properties": {"base": {"type": "string"}}}
2307 ],
2308 "oneOf": [
2309 {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"], "not": {"required": ["b"]}},
2310 {"type": "object", "properties": {"b": {"type": "integer"}}, "required": ["b"], "not": {"required": ["a"]}}
2311 ],
2312 "anyOf": [
2313 {"type": "object", "properties": {"flag": {"type": "boolean"}}, "required": ["flag"]},
2314 {"type": "object", "properties": {"extra": {"type": "string"}}, "required": ["extra"]}
2315 ]
2316 }
2317 }
2318 }
2319 },
2320 "responses": {"200": {"description": "ok"}}
2321 }
2322 }
2323 }
2324 });
2325
2326 let registry = create_registry_from_json(spec_json).unwrap();
2327 let ok = json!({"base": "x", "a": 1, "flag": true});
2329 assert!(registry
2330 .validate_request_with(
2331 "/composite",
2332 "POST",
2333 &serde_json::Map::new(),
2334 &serde_json::Map::new(),
2335 Some(&ok)
2336 )
2337 .is_ok());
2338
2339 let bad_oneof = json!({"base": "x", "a": 1, "b": 2, "flag": false});
2341 assert!(registry
2342 .validate_request_with(
2343 "/composite",
2344 "POST",
2345 &serde_json::Map::new(),
2346 &serde_json::Map::new(),
2347 Some(&bad_oneof)
2348 )
2349 .is_err());
2350
2351 let bad_anyof = json!({"base": "x", "a": 1});
2353 assert!(registry
2354 .validate_request_with(
2355 "/composite",
2356 "POST",
2357 &serde_json::Map::new(),
2358 &serde_json::Map::new(),
2359 Some(&bad_anyof)
2360 )
2361 .is_err());
2362
2363 let bad_allof = json!({"a": 1, "flag": true});
2365 assert!(registry
2366 .validate_request_with(
2367 "/composite",
2368 "POST",
2369 &serde_json::Map::new(),
2370 &serde_json::Map::new(),
2371 Some(&bad_allof)
2372 )
2373 .is_err());
2374 }
2375
2376 #[tokio::test]
2377 async fn test_overrides_warn_mode_allows_invalid() {
2378 let spec_json = json!({
2380 "openapi": "3.0.0",
2381 "info": { "title": "Overrides API", "version": "1.0.0" },
2382 "paths": {"/things": {"post": {
2383 "parameters": [{"name":"q","in":"query","required":true,"schema":{"type":"integer"}}],
2384 "responses": {"200": {"description":"ok"}}
2385 }}}
2386 });
2387
2388 let spec = OpenApiSpec::from_json(spec_json).unwrap();
2389 let mut overrides = std::collections::HashMap::new();
2390 overrides.insert("POST /things".to_string(), ValidationMode::Warn);
2391 let registry = OpenApiRouteRegistry::new_with_options(
2392 spec,
2393 ValidationOptions {
2394 request_mode: ValidationMode::Enforce,
2395 aggregate_errors: true,
2396 validate_responses: false,
2397 overrides,
2398 admin_skip_prefixes: vec![],
2399 response_template_expand: false,
2400 validation_status: None,
2401 },
2402 );
2403
2404 let ok = registry.validate_request_with("/things", "POST", &Map::new(), &Map::new(), None);
2406 assert!(ok.is_ok());
2407 }
2408
2409 #[tokio::test]
2410 async fn test_admin_skip_prefix_short_circuit() {
2411 let spec_json = json!({
2412 "openapi": "3.0.0",
2413 "info": { "title": "Skip API", "version": "1.0.0" },
2414 "paths": {}
2415 });
2416 let spec = OpenApiSpec::from_json(spec_json).unwrap();
2417 let registry = OpenApiRouteRegistry::new_with_options(
2418 spec,
2419 ValidationOptions {
2420 request_mode: ValidationMode::Enforce,
2421 aggregate_errors: true,
2422 validate_responses: false,
2423 overrides: std::collections::HashMap::new(),
2424 admin_skip_prefixes: vec!["/admin".into()],
2425 response_template_expand: false,
2426 validation_status: None,
2427 },
2428 );
2429
2430 let res = registry.validate_request_with_all(
2432 "/admin/__mockforge/health",
2433 "GET",
2434 &Map::new(),
2435 &Map::new(),
2436 &Map::new(),
2437 &Map::new(),
2438 None,
2439 );
2440 assert!(res.is_ok());
2441 }
2442
2443 #[test]
2444 fn test_path_conversion() {
2445 assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users"), "/users");
2446 assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users/{id}"), "/users/{id}");
2447 assert_eq!(
2448 OpenApiRouteRegistry::convert_path_to_axum("/users/{id}/posts/{postId}"),
2449 "/users/{id}/posts/{postId}"
2450 );
2451 }
2452}