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(&parameter_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) = &parameter_data.format {
538        if let Some(actual_value) = params_map.get(&parameter_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(&param_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(&param_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}