mockforge_core/openapi_routes/
validation.rs

1//! Request/response validation logic
2//!
3//! This module provides validation functionality for OpenAPI-based routes,
4//! including request validation, response validation, and error handling.
5
6use jsonschema::validate;
7use serde::Deserialize;
8use serde_json::Value;
9use std::collections::HashMap;
10
11/// Validation mode for requests and responses
12#[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize, Default)]
13pub enum ValidationMode {
14    /// Validation is disabled (no checks performed)
15    Disabled,
16    /// Validation warnings are logged but do not fail requests
17    #[default]
18    Warn,
19    /// Validation failures return error responses
20    Enforce,
21}
22
23/// Validation options for configuring OpenAPI route validation
24#[derive(Debug, Clone)]
25pub struct ValidationOptions {
26    /// Validation mode for incoming requests
27    pub request_mode: ValidationMode,
28    /// Whether to aggregate multiple validation errors into a single response
29    pub aggregate_errors: bool,
30    /// Whether to validate outgoing responses against schemas
31    pub validate_responses: bool,
32    /// Per-operation validation mode overrides (operation ID -> mode)
33    pub overrides: HashMap<String, ValidationMode>,
34    /// Skip validation for request paths starting with any of these prefixes
35    pub admin_skip_prefixes: Vec<String>,
36    /// Expand templating tokens in responses/examples after generation
37    pub response_template_expand: bool,
38    /// HTTP status code to return when validation fails (e.g., 400 or 422)
39    pub validation_status: Option<u16>,
40}
41
42impl Default for ValidationOptions {
43    fn default() -> Self {
44        Self {
45            request_mode: ValidationMode::Enforce,
46            aggregate_errors: true,
47            validate_responses: false,
48            overrides: HashMap::new(),
49            admin_skip_prefixes: Vec::new(),
50            response_template_expand: false,
51            validation_status: None,
52        }
53    }
54}
55
56/// Validation error information for a specific field
57#[derive(Debug, Clone, Deserialize)]
58pub struct ValidationError {
59    /// JSON path to the field with the validation issue
60    pub field: String,
61    /// Human-readable error message
62    pub message: String,
63    /// Expected value or type (if applicable)
64    pub expected: Option<Value>,
65    /// Actual value found (if applicable)
66    pub actual: Option<Value>,
67}
68
69/// Result of a validation operation
70#[derive(Debug, Clone)]
71pub struct ValidationResult {
72    /// Whether the validation passed (no errors)
73    pub is_valid: bool,
74    /// List of validation errors found
75    pub errors: Vec<ValidationError>,
76    /// List of validation warnings (non-blocking)
77    pub warnings: Vec<ValidationError>,
78}
79
80/// Validation context for tracking errors during validation
81#[derive(Debug, Default)]
82pub struct ValidationContext {
83    errors: Vec<ValidationError>,
84    warnings: Vec<ValidationError>,
85}
86
87impl ValidationContext {
88    /// Create a new validation context
89    pub fn new() -> Self {
90        Self::default()
91    }
92
93    /// Add an error to the validation context
94    pub fn add_error(&mut self, field: String, message: String) {
95        self.errors.push(ValidationError {
96            field,
97            message,
98            expected: None,
99            actual: None,
100        });
101    }
102
103    /// Add an error with expected and actual values
104    pub fn add_error_with_values(
105        &mut self,
106        field: String,
107        message: String,
108        expected: Value,
109        actual: Value,
110    ) {
111        self.errors.push(ValidationError {
112            field,
113            message,
114            expected: Some(expected),
115            actual: Some(actual),
116        });
117    }
118
119    /// Add a warning to the validation context
120    pub fn add_warning(&mut self, field: String, message: String) {
121        self.warnings.push(ValidationError {
122            field,
123            message,
124            expected: None,
125            actual: None,
126        });
127    }
128
129    /// Get the validation result
130    pub fn result(&self) -> ValidationResult {
131        ValidationResult {
132            is_valid: self.errors.is_empty(),
133            errors: self.errors.clone(),
134            warnings: self.warnings.clone(),
135        }
136    }
137
138    /// Check if validation has errors
139    pub fn has_errors(&self) -> bool {
140        !self.errors.is_empty()
141    }
142
143    /// Check if validation has warnings
144    pub fn has_warnings(&self) -> bool {
145        !self.warnings.is_empty()
146    }
147}
148
149/// Validate a JSON value against a schema
150pub fn validate_json_value(value: &Value, schema: &Value) -> ValidationResult {
151    let mut ctx = ValidationContext::new();
152
153    // Basic validation - check required fields and types
154    validate_against_schema(value, schema, &mut ctx);
155
156    ctx.result()
157}
158
159/// Validate a value against a JSON schema
160fn validate_against_schema(value: &Value, schema: &Value, ctx: &mut ValidationContext) {
161    // Use proper JSON Schema validation
162    if let Err(error) = validate(schema, value) {
163        let field = error.instance_path.to_string();
164        let message = error.to_string();
165        ctx.add_error(field, message);
166    }
167}