mockforge_core/
runtime_validation.rs

1//! Runtime validation for SDKs
2//!
3//! This module provides runtime validation utilities that can be used in generated SDKs
4//! to validate requests and responses against OpenAPI schemas at runtime.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Runtime validation error with contract diff reference
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct RuntimeValidationError {
12    /// JSON path to the invalid field (e.g., "body.user.email")
13    pub schema_path: String,
14    /// Expected schema type or format
15    pub expected_type: String,
16    /// Actual value received (serialized to string)
17    pub actual_value: Option<String>,
18    /// Link to contract diff entry (if available)
19    pub contract_diff_id: Option<String>,
20    /// Whether this is a breaking change
21    pub is_breaking_change: bool,
22    /// Human-readable error message
23    pub message: String,
24    /// Additional validation details
25    pub details: Option<HashMap<String, serde_json::Value>>,
26}
27
28impl RuntimeValidationError {
29    /// Create a new runtime validation error
30    pub fn new(
31        schema_path: impl Into<String>,
32        expected_type: impl Into<String>,
33        message: impl Into<String>,
34    ) -> Self {
35        Self {
36            schema_path: schema_path.into(),
37            expected_type: expected_type.into(),
38            actual_value: None,
39            contract_diff_id: None,
40            is_breaking_change: false,
41            message: message.into(),
42            details: None,
43        }
44    }
45
46    /// Set the actual value
47    pub fn with_actual_value(mut self, value: impl Into<String>) -> Self {
48        self.actual_value = Some(value.into());
49        self
50    }
51
52    /// Set the contract diff ID
53    pub fn with_contract_diff_id(mut self, diff_id: impl Into<String>) -> Self {
54        self.contract_diff_id = Some(diff_id.into());
55        self
56    }
57
58    /// Mark as breaking change
59    pub fn as_breaking_change(mut self) -> Self {
60        self.is_breaking_change = true;
61        self
62    }
63
64    /// Add additional details
65    pub fn with_details(mut self, details: HashMap<String, serde_json::Value>) -> Self {
66        self.details = Some(details);
67        self
68    }
69}
70
71/// Runtime validation result
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct RuntimeValidationResult {
74    /// Whether validation passed
75    pub valid: bool,
76    /// Validation errors (if any)
77    pub errors: Vec<RuntimeValidationError>,
78    /// Validation warnings (non-blocking)
79    pub warnings: Vec<RuntimeValidationError>,
80}
81
82impl RuntimeValidationResult {
83    /// Create a successful validation result
84    pub fn success() -> Self {
85        Self {
86            valid: true,
87            errors: Vec::new(),
88            warnings: Vec::new(),
89        }
90    }
91
92    /// Create a failed validation result
93    pub fn failure(errors: Vec<RuntimeValidationError>) -> Self {
94        Self {
95            valid: false,
96            errors,
97            warnings: Vec::new(),
98        }
99    }
100
101    /// Add an error
102    pub fn add_error(&mut self, error: RuntimeValidationError) {
103        self.valid = false;
104        self.errors.push(error);
105    }
106
107    /// Add a warning
108    pub fn add_warning(&mut self, warning: RuntimeValidationError) {
109        self.warnings.push(warning);
110    }
111}
112
113/// Schema metadata for runtime validation
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct SchemaMetadata {
116    /// Schema identifier (e.g., "User", "CreateUserRequest")
117    pub id: String,
118    /// JSON Schema representation
119    pub schema: serde_json::Value,
120    /// Whether this schema is required
121    pub required: bool,
122    /// Contract diff ID (if this schema has been changed)
123    pub contract_diff_id: Option<String>,
124}
125
126/// Runtime validator configuration
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct RuntimeValidatorConfig {
129    /// Whether to validate requests
130    pub validate_requests: bool,
131    /// Whether to validate responses
132    pub validate_responses: bool,
133    /// Whether to throw errors on validation failure (vs. just logging)
134    pub throw_on_error: bool,
135    /// Whether to include contract diff references in errors
136    pub include_contract_diffs: bool,
137    /// Schema registry (schema_id -> SchemaMetadata)
138    pub schemas: HashMap<String, SchemaMetadata>,
139}
140
141impl Default for RuntimeValidatorConfig {
142    fn default() -> Self {
143        Self {
144            validate_requests: false,
145            validate_responses: false,
146            throw_on_error: false,
147            include_contract_diffs: true,
148            schemas: HashMap::new(),
149        }
150    }
151}
152
153impl RuntimeValidatorConfig {
154    /// Create a new runtime validator config
155    pub fn new() -> Self {
156        Self::default()
157    }
158
159    /// Enable request validation
160    pub fn with_request_validation(mut self, enabled: bool) -> Self {
161        self.validate_requests = enabled;
162        self
163    }
164
165    /// Enable response validation
166    pub fn with_response_validation(mut self, enabled: bool) -> Self {
167        self.validate_responses = enabled;
168        self
169    }
170
171    /// Configure error throwing behavior
172    pub fn with_throw_on_error(mut self, throw: bool) -> Self {
173        self.throw_on_error = throw;
174        self
175    }
176
177    /// Add a schema to the registry
178    pub fn add_schema(&mut self, metadata: SchemaMetadata) {
179        self.schemas.insert(metadata.id.clone(), metadata);
180    }
181}