mockforge_core/openapi/validation.rs
1//! OpenAPI request/response validation
2//!
3//! This module provides validation functionality for requests and responses
4//! against OpenAPI specifications.
5
6use crate::Result;
7use indexmap::IndexMap;
8use jsonschema::{self, Draft};
9use openapiv3::{
10 Header, MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr,
11 Response, Responses,
12};
13use serde_json::Value;
14use std::collections::HashMap;
15
16/// Request validation result
17#[derive(Debug, Clone)]
18pub struct RequestValidationResult {
19 /// Whether the request is valid
20 pub valid: bool,
21 /// Validation errors
22 pub errors: Vec<String>,
23}
24
25impl RequestValidationResult {
26 /// Create a successful validation result
27 pub fn valid() -> Self {
28 Self {
29 valid: true,
30 errors: Vec::new(),
31 }
32 }
33
34 /// Create a failed validation result
35 pub fn invalid(errors: Vec<String>) -> Self {
36 Self {
37 valid: false,
38 errors,
39 }
40 }
41}
42
43/// Response validation result
44#[derive(Debug, Clone)]
45pub struct ResponseValidationResult {
46 /// Whether the response is valid
47 pub valid: bool,
48 /// Validation errors
49 pub errors: Vec<String>,
50}
51
52impl ResponseValidationResult {
53 /// Create a successful validation result
54 pub fn valid() -> Self {
55 Self {
56 valid: true,
57 errors: Vec::new(),
58 }
59 }
60
61 /// Create a failed validation result
62 pub fn invalid(errors: Vec<String>) -> Self {
63 Self {
64 valid: false,
65 errors,
66 }
67 }
68}
69
70/// Request validator
71pub struct RequestValidator;
72
73impl RequestValidator {
74 /// Validate a request against an OpenAPI operation
75 pub fn validate_request(
76 spec: &crate::openapi::OpenApiSpec,
77 operation: &Operation,
78 path_params: &std::collections::HashMap<String, String>,
79 query_params: &std::collections::HashMap<String, String>,
80 headers: &std::collections::HashMap<String, String>,
81 body: Option<&Value>,
82 ) -> Result<RequestValidationResult> {
83 let mut errors = Vec::new();
84
85 // Validate parameters
86 for param_ref in &operation.parameters {
87 if let Some(param) = param_ref.as_item() {
88 match param {
89 Parameter::Path { parameter_data, .. } => {
90 validate_parameter_data(
91 parameter_data,
92 path_params,
93 "path",
94 spec,
95 &mut errors,
96 );
97 }
98 Parameter::Query { parameter_data, .. } => {
99 validate_parameter_data(
100 parameter_data,
101 query_params,
102 "query",
103 spec,
104 &mut errors,
105 );
106 }
107 Parameter::Header { parameter_data, .. } => {
108 validate_parameter_data(
109 parameter_data,
110 headers,
111 "header",
112 spec,
113 &mut errors,
114 );
115 }
116 Parameter::Cookie { .. } => {
117 // Cookie parameter validation not implemented
118 }
119 }
120 }
121 }
122
123 // Validate request body
124 if let Some(request_body_ref) = &operation.request_body {
125 match request_body_ref {
126 openapiv3::ReferenceOr::Reference { reference } => {
127 if let Some(request_body) = spec.get_request_body(reference) {
128 if let Some(body_errors) =
129 validate_request_body(body, &request_body.content, spec)
130 {
131 errors.extend(body_errors);
132 }
133 }
134 }
135 openapiv3::ReferenceOr::Item(request_body) => {
136 if let Some(body_errors) =
137 validate_request_body(body, &request_body.content, spec)
138 {
139 errors.extend(body_errors);
140 }
141 }
142 }
143 }
144
145 if errors.is_empty() {
146 Ok(RequestValidationResult::valid())
147 } else {
148 Ok(RequestValidationResult::invalid(errors))
149 }
150 }
151}
152
153/// Response validator
154pub struct ResponseValidator;
155
156impl ResponseValidator {
157 /// Validate a response against an OpenAPI operation
158 pub fn validate_response(
159 spec: &crate::openapi::OpenApiSpec,
160 operation: &Operation,
161 status_code: u16,
162 headers: &std::collections::HashMap<String, String>,
163 body: Option<&Value>,
164 ) -> Result<ResponseValidationResult> {
165 let mut errors = Vec::new();
166
167 // Find the response definition for the status code
168 let response = find_response_for_status(&operation.responses, status_code);
169
170 if let Some(response_ref) = response {
171 if let Some(response_item) = response_ref.as_item() {
172 // Validate headers
173 if let Some(header_errors) =
174 validate_response_headers(headers, &response_item.headers, spec)
175 {
176 errors.extend(header_errors);
177 }
178
179 // Validate body
180 if let Some(body_errors) =
181 validate_response_body(body, &response_item.content, spec)
182 {
183 errors.extend(body_errors);
184 }
185 }
186 } else {
187 // No response definition found for this status code
188 errors.push(format!("No response definition found for status code {}", status_code));
189 }
190
191 if errors.is_empty() {
192 Ok(ResponseValidationResult::valid())
193 } else {
194 Ok(ResponseValidationResult::invalid(errors))
195 }
196 }
197}
198
199/// Find the response definition for a given status code
200fn find_response_for_status(
201 responses: &Responses,
202 status_code: u16,
203) -> Option<&ReferenceOr<Response>> {
204 // First try exact match
205 if let Some(response) = responses.responses.get(&openapiv3::StatusCode::Code(status_code)) {
206 return Some(response);
207 }
208
209 // Try default response
210 if let Some(default_response) = &responses.default {
211 return Some(default_response);
212 }
213
214 None
215}
216
217/// Validate response headers against the response definition
218fn validate_response_headers(
219 actual_headers: &HashMap<String, String>,
220 expected_headers: &IndexMap<String, ReferenceOr<Header>>,
221 spec: &crate::openapi::OpenApiSpec,
222) -> Option<Vec<String>> {
223 let mut errors = Vec::new();
224
225 for (header_name, header_ref) in expected_headers {
226 if let Some(header) = header_ref.as_item() {
227 if header.required && !actual_headers.contains_key(header_name) {
228 errors.push(format!("Missing required header: {}", header_name));
229 }
230 // Validate header schema if present
231 if let ParameterSchemaOrContent::Schema(schema_ref) = &header.format {
232 if let Some(actual_value) = actual_headers.get(header_name) {
233 let header_value = Value::String(actual_value.clone());
234 match schema_ref {
235 ReferenceOr::Item(schema) => {
236 match serde_json::to_value(schema) {
237 Ok(schema_json) => {
238 match jsonschema::options()
239 .with_draft(Draft::Draft7)
240 .build(&schema_json)
241 {
242 Ok(validator) => {
243 let mut schema_errors = Vec::new();
244 for error in validator.iter_errors(&header_value) {
245 schema_errors.push(error.to_string());
246 }
247 if !schema_errors.is_empty() {
248 errors.push(format!(
249 "Header '{}' validation failed: {}",
250 header_name,
251 schema_errors.join(", ")
252 ));
253 }
254 }
255 Err(e) => {
256 errors.push(format!("Failed to create schema validator for header '{}': {}", header_name, e));
257 }
258 }
259 }
260 Err(e) => {
261 errors.push(format!(
262 "Failed to convert schema for header '{}' to JSON: {}",
263 header_name, e
264 ));
265 }
266 }
267 }
268 ReferenceOr::Reference { reference } => {
269 if let Some(resolved_schema) = spec.get_schema(reference) {
270 match serde_json::to_value(&resolved_schema.schema) {
271 Ok(schema_json) => {
272 match jsonschema::options()
273 .with_draft(Draft::Draft7)
274 .build(&schema_json)
275 {
276 Ok(validator) => {
277 let mut schema_errors = Vec::new();
278 for error in validator.iter_errors(&header_value) {
279 schema_errors.push(error.to_string());
280 }
281 if !schema_errors.is_empty() {
282 errors.push(format!(
283 "Header '{}' validation failed: {}",
284 header_name,
285 schema_errors.join(", ")
286 ));
287 }
288 }
289 Err(e) => {
290 errors.push(format!("Failed to create schema validator for header '{}': {}", header_name, e));
291 }
292 }
293 }
294 Err(e) => {
295 errors.push(format!(
296 "Failed to convert schema for header '{}' to JSON: {}",
297 header_name, e
298 ));
299 }
300 }
301 } else {
302 errors.push(format!(
303 "Failed to resolve schema reference for header '{}': {}",
304 header_name, reference
305 ));
306 }
307 }
308 }
309 }
310 }
311 }
312 }
313
314 if errors.is_empty() {
315 None
316 } else {
317 Some(errors)
318 }
319}
320
321/// Validate response body against the response content definition
322fn validate_response_body(
323 body: Option<&Value>,
324 content: &IndexMap<String, MediaType>,
325 spec: &crate::openapi::OpenApiSpec,
326) -> Option<Vec<String>> {
327 // For now, only validate JSON content
328 if let Some(media_type) = content.get("application/json") {
329 if let Some(schema_ref) = &media_type.schema {
330 match body {
331 Some(body_value) => {
332 // Implement proper schema validation
333 match schema_ref {
334 ReferenceOr::Item(schema) => {
335 // Convert OpenAPI schema to JSON Schema
336 match serde_json::to_value(schema) {
337 Ok(schema_json) => {
338 // Create JSON Schema validator
339 match jsonschema::options()
340 .with_draft(Draft::Draft7)
341 .build(&schema_json)
342 {
343 Ok(validator) => {
344 // Validate the body against the schema
345 let mut errors = Vec::new();
346 for error in validator.iter_errors(body_value) {
347 errors.push(error.to_string());
348 }
349 if errors.is_empty() {
350 None
351 } else {
352 Some(errors)
353 }
354 }
355 Err(e) => Some(vec![format!(
356 "Failed to create schema validator: {}",
357 e
358 )]),
359 }
360 }
361 Err(e) => Some(vec![format!(
362 "Failed to convert OpenAPI schema to JSON: {}",
363 e
364 )]),
365 }
366 }
367 ReferenceOr::Reference { reference } => {
368 // Resolve schema reference
369 if let Some(resolved_schema) = spec.get_schema(reference) {
370 // Convert OpenAPI schema to JSON Schema
371 match serde_json::to_value(&resolved_schema.schema) {
372 Ok(schema_json) => {
373 // Create JSON Schema validator
374 match jsonschema::options()
375 .with_draft(Draft::Draft7)
376 .build(&schema_json)
377 {
378 Ok(validator) => {
379 // Validate the body against the schema
380 let mut errors = Vec::new();
381 for error in validator.iter_errors(body_value) {
382 errors.push(error.to_string());
383 }
384 if errors.is_empty() {
385 None
386 } else {
387 Some(errors)
388 }
389 }
390 Err(e) => Some(vec![format!(
391 "Failed to create schema validator: {}",
392 e
393 )]),
394 }
395 }
396 Err(e) => Some(vec![format!(
397 "Failed to convert OpenAPI schema to JSON: {}",
398 e
399 )]),
400 }
401 } else {
402 Some(vec![format!(
403 "Failed to resolve schema reference: {}",
404 reference
405 )])
406 }
407 }
408 }
409 }
410 None => Some(vec!["Response body is required but not provided".to_string()]),
411 }
412 } else {
413 // No schema defined, body is optional
414 None
415 }
416 } else {
417 // No JSON content type defined, skip validation
418 None
419 }
420}
421
422/// Validate request body against the request body content definition
423fn validate_request_body(
424 body: Option<&Value>,
425 content: &IndexMap<String, MediaType>,
426 spec: &crate::openapi::OpenApiSpec,
427) -> Option<Vec<String>> {
428 // For now, only validate JSON content
429 if let Some(media_type) = content.get("application/json") {
430 if let Some(schema_ref) = &media_type.schema {
431 match body {
432 Some(body_value) => {
433 // Implement proper schema validation
434 match schema_ref {
435 ReferenceOr::Item(schema) => {
436 // Convert OpenAPI schema to JSON Schema
437 match serde_json::to_value(schema) {
438 Ok(schema_json) => {
439 // Create JSON Schema validator
440 match jsonschema::options()
441 .with_draft(Draft::Draft7)
442 .build(&schema_json)
443 {
444 Ok(validator) => {
445 // Validate the body against the schema
446 let mut errors = Vec::new();
447 for error in validator.iter_errors(body_value) {
448 errors.push(error.to_string());
449 }
450 if errors.is_empty() {
451 None
452 } else {
453 Some(errors)
454 }
455 }
456 Err(e) => Some(vec![format!(
457 "Failed to create schema validator: {}",
458 e
459 )]),
460 }
461 }
462 Err(e) => Some(vec![format!(
463 "Failed to convert OpenAPI schema to JSON: {}",
464 e
465 )]),
466 }
467 }
468 ReferenceOr::Reference { reference } => {
469 // Resolve schema reference
470 if let Some(resolved_schema) = spec.get_schema(reference) {
471 // Convert OpenAPI schema to JSON Schema
472 match serde_json::to_value(&resolved_schema.schema) {
473 Ok(schema_json) => {
474 // Create JSON Schema validator
475 match jsonschema::options()
476 .with_draft(Draft::Draft7)
477 .build(&schema_json)
478 {
479 Ok(validator) => {
480 // Validate the body against the schema
481 let mut errors = Vec::new();
482 for error in validator.iter_errors(body_value) {
483 errors.push(error.to_string());
484 }
485 if errors.is_empty() {
486 None
487 } else {
488 Some(errors)
489 }
490 }
491 Err(e) => Some(vec![format!(
492 "Failed to create schema validator: {}",
493 e
494 )]),
495 }
496 }
497 Err(e) => Some(vec![format!(
498 "Failed to convert OpenAPI schema to JSON: {}",
499 e
500 )]),
501 }
502 } else {
503 Some(vec![format!(
504 "Failed to resolve schema reference: {}",
505 reference
506 )])
507 }
508 }
509 }
510 }
511 None => Some(vec!["Request body is required but not provided".to_string()]),
512 }
513 } else {
514 // No schema defined, body is optional
515 None
516 }
517 } else {
518 // No JSON content type defined, skip validation
519 None
520 }
521}
522
523/// Validate a parameter against its definition
524fn validate_parameter_data(
525 parameter_data: &ParameterData,
526 params_map: &HashMap<String, String>,
527 location: &str,
528 spec: &crate::openapi::OpenApiSpec,
529 errors: &mut Vec<String>,
530) {
531 // Check if required parameter is present
532 if parameter_data.required && !params_map.contains_key(¶meter_data.name) {
533 errors.push(format!("Missing required {} parameter: {}", location, parameter_data.name));
534 }
535
536 // Validate parameter value against schema if present
537 if let ParameterSchemaOrContent::Schema(schema_ref) = ¶meter_data.format {
538 if let Some(actual_value) = params_map.get(¶meter_data.name) {
539 let param_value = Value::String(actual_value.clone());
540 match schema_ref {
541 ReferenceOr::Item(schema) => match serde_json::to_value(schema) {
542 Ok(schema_json) => {
543 match jsonschema::options().with_draft(Draft::Draft7).build(&schema_json) {
544 Ok(validator) => {
545 let mut schema_errors = Vec::new();
546 for error in validator.iter_errors(¶m_value) {
547 schema_errors.push(error.to_string());
548 }
549 if !schema_errors.is_empty() {
550 errors.push(format!(
551 "Parameter '{}' {} validation failed: {}",
552 parameter_data.name,
553 location,
554 schema_errors.join(", ")
555 ));
556 }
557 }
558 Err(e) => {
559 errors.push(format!(
560 "Failed to create schema validator for parameter '{}': {}",
561 parameter_data.name, e
562 ));
563 }
564 }
565 }
566 Err(e) => {
567 errors.push(format!(
568 "Failed to convert schema for parameter '{}' to JSON: {}",
569 parameter_data.name, e
570 ));
571 }
572 },
573 ReferenceOr::Reference { reference } => {
574 if let Some(resolved_schema) = spec.get_schema(reference) {
575 match serde_json::to_value(&resolved_schema.schema) {
576 Ok(schema_json) => {
577 match jsonschema::options()
578 .with_draft(Draft::Draft7)
579 .build(&schema_json)
580 {
581 Ok(validator) => {
582 let mut schema_errors = Vec::new();
583 for error in validator.iter_errors(¶m_value) {
584 schema_errors.push(error.to_string());
585 }
586 if !schema_errors.is_empty() {
587 errors.push(format!(
588 "Parameter '{}' {} validation failed: {}",
589 parameter_data.name,
590 location,
591 schema_errors.join(", ")
592 ));
593 }
594 }
595 Err(e) => {
596 errors.push(format!("Failed to create schema validator for parameter '{}': {}", parameter_data.name, e));
597 }
598 }
599 }
600 Err(e) => {
601 errors.push(format!(
602 "Failed to convert schema for parameter '{}' to JSON: {}",
603 parameter_data.name, e
604 ));
605 }
606 }
607 } else {
608 errors.push(format!(
609 "Failed to resolve schema reference for parameter '{}': {}",
610 parameter_data.name, reference
611 ));
612 }
613 }
614 }
615 }
616 }
617}