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