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