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