turbomcp_protocol/
validation.rs

1//! # Protocol Validation
2//!
3//! This module provides comprehensive validation for MCP protocol messages,
4//! ensuring data integrity and specification compliance.
5
6use once_cell::sync::Lazy;
7use regex::Regex;
8use serde_json::Value;
9use std::collections::{HashMap, HashSet};
10
11use crate::jsonrpc::{JsonRpcNotification, JsonRpcRequest, JsonRpcResponse};
12use crate::types::*;
13
14/// Cached regex for URI validation (compiled once)
15static URI_REGEX: Lazy<Regex> =
16    Lazy::new(|| Regex::new(r"^[a-zA-Z][a-zA-Z0-9+.-]*:").expect("Invalid URI regex pattern"));
17
18/// Cached regex for method name validation (compiled once)
19static METHOD_NAME_REGEX: Lazy<Regex> = Lazy::new(|| {
20    Regex::new(r"^[a-zA-Z][a-zA-Z0-9_/]*$").expect("Invalid method name regex pattern")
21});
22
23/// Protocol message validator
24#[derive(Debug, Clone)]
25pub struct ProtocolValidator {
26    /// Validation rules
27    rules: ValidationRules,
28    /// Strict validation mode
29    strict_mode: bool,
30}
31
32/// Validation rules configuration
33#[derive(Debug, Clone)]
34pub struct ValidationRules {
35    /// Maximum message size in bytes
36    pub max_message_size: usize,
37    /// Maximum batch size
38    pub max_batch_size: usize,
39    /// Maximum string length
40    pub max_string_length: usize,
41    /// Maximum array length
42    pub max_array_length: usize,
43    /// Maximum object depth
44    pub max_object_depth: usize,
45    /// Required fields per message type
46    pub required_fields: HashMap<String, HashSet<String>>,
47}
48
49impl ValidationRules {
50    /// Get the URI validation regex (cached globally)
51    #[inline]
52    pub fn uri_regex(&self) -> &Regex {
53        &URI_REGEX
54    }
55
56    /// Get the method name validation regex (cached globally)
57    #[inline]
58    pub fn method_name_regex(&self) -> &Regex {
59        &METHOD_NAME_REGEX
60    }
61}
62
63/// Validation result
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum ValidationResult {
66    /// Validation passed
67    Valid,
68    /// Validation passed with warnings
69    ValidWithWarnings(Vec<ValidationWarning>),
70    /// Validation failed
71    Invalid(Vec<ValidationError>),
72}
73
74/// Validation warning
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct ValidationWarning {
77    /// Warning code
78    pub code: String,
79    /// Warning message
80    pub message: String,
81    /// Field path (if applicable)
82    pub field_path: Option<String>,
83}
84
85/// Validation error
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct ValidationError {
88    /// Error code
89    pub code: String,
90    /// Error message
91    pub message: String,
92    /// Field path (if applicable)
93    pub field_path: Option<String>,
94}
95
96/// Validation context for tracking state during validation
97#[derive(Debug, Clone)]
98struct ValidationContext {
99    /// Current field path
100    path: Vec<String>,
101    /// Current object depth
102    depth: usize,
103    /// Accumulated warnings
104    warnings: Vec<ValidationWarning>,
105    /// Accumulated errors
106    errors: Vec<ValidationError>,
107}
108
109impl Default for ValidationRules {
110    fn default() -> Self {
111        let mut required_fields = HashMap::new();
112
113        // JSON-RPC required fields
114        required_fields.insert(
115            "request".to_string(),
116            ["jsonrpc", "method", "id"]
117                .iter()
118                .map(|s| s.to_string())
119                .collect(),
120        );
121        required_fields.insert(
122            "response".to_string(),
123            ["jsonrpc", "id"].iter().map(|s| s.to_string()).collect(),
124        );
125        required_fields.insert(
126            "notification".to_string(),
127            ["jsonrpc", "method"]
128                .iter()
129                .map(|s| s.to_string())
130                .collect(),
131        );
132
133        // MCP message required fields
134        required_fields.insert(
135            "initialize".to_string(),
136            ["protocolVersion", "capabilities", "clientInfo"]
137                .iter()
138                .map(|s| s.to_string())
139                .collect(),
140        );
141        required_fields.insert(
142            "tool".to_string(),
143            ["name", "inputSchema"]
144                .iter()
145                .map(|s| s.to_string())
146                .collect(),
147        );
148        required_fields.insert(
149            "prompt".to_string(),
150            ["name"].iter().map(|s| s.to_string()).collect(),
151        );
152        required_fields.insert(
153            "resource".to_string(),
154            ["uri", "name"].iter().map(|s| s.to_string()).collect(),
155        );
156
157        Self {
158            max_message_size: 10 * 1024 * 1024, // 10MB
159            max_batch_size: 100,
160            max_string_length: 1024 * 1024, // 1MB
161            max_array_length: 10000,
162            max_object_depth: 32,
163            required_fields,
164        }
165    }
166}
167
168impl ProtocolValidator {
169    /// Create a new validator with default rules
170    pub fn new() -> Self {
171        Self {
172            rules: ValidationRules::default(),
173            strict_mode: false,
174        }
175    }
176
177    /// Enable strict validation mode
178    pub fn with_strict_mode(mut self) -> Self {
179        self.strict_mode = true;
180        self
181    }
182
183    /// Set custom validation rules
184    pub fn with_rules(mut self, rules: ValidationRules) -> Self {
185        self.rules = rules;
186        self
187    }
188
189    /// Validate a JSON-RPC request
190    pub fn validate_request(&self, request: &JsonRpcRequest) -> ValidationResult {
191        let mut ctx = ValidationContext::new();
192
193        // Validate JSON-RPC structure (includes method name validation)
194        self.validate_jsonrpc_request(request, &mut ctx);
195
196        // Validate parameters based on method
197        if let Some(params) = &request.params {
198            self.validate_method_params(&request.method, params, &mut ctx);
199        }
200
201        ctx.into_result()
202    }
203
204    /// Validate a JSON-RPC response
205    pub fn validate_response(&self, response: &JsonRpcResponse) -> ValidationResult {
206        let mut ctx = ValidationContext::new();
207
208        // Validate JSON-RPC structure
209        self.validate_jsonrpc_response(response, &mut ctx);
210
211        // Ensure either result or error is present (but not both)
212        // Note: This validation is now enforced at the type level with JsonRpcResponsePayload enum
213        // But we still validate for completeness
214        match (response.result().is_some(), response.error().is_some()) {
215            (true, true) => {
216                ctx.add_error(
217                    "RESPONSE_BOTH_RESULT_AND_ERROR",
218                    "Response cannot have both result and error".to_string(),
219                    None,
220                );
221            }
222            (false, false) => {
223                ctx.add_error(
224                    "RESPONSE_MISSING_RESULT_OR_ERROR",
225                    "Response must have either result or error".to_string(),
226                    None,
227                );
228            }
229            _ => {} // Valid
230        }
231
232        ctx.into_result()
233    }
234
235    /// Validate a JSON-RPC notification
236    pub fn validate_notification(&self, notification: &JsonRpcNotification) -> ValidationResult {
237        let mut ctx = ValidationContext::new();
238
239        // Validate JSON-RPC structure
240        self.validate_jsonrpc_notification(notification, &mut ctx);
241
242        // Validate method name
243        self.validate_method_name(&notification.method, &mut ctx);
244
245        // Validate parameters based on method
246        if let Some(params) = &notification.params {
247            self.validate_method_params(&notification.method, params, &mut ctx);
248        }
249
250        ctx.into_result()
251    }
252
253    /// Validate MCP protocol types
254    pub fn validate_tool(&self, tool: &Tool) -> ValidationResult {
255        let mut ctx = ValidationContext::new();
256
257        // Validate tool name
258        if tool.name.is_empty() {
259            ctx.add_error(
260                "TOOL_EMPTY_NAME",
261                "Tool name cannot be empty".to_string(),
262                Some("name".to_string()),
263            );
264        }
265
266        if tool.name.len() > self.rules.max_string_length {
267            ctx.add_error(
268                "TOOL_NAME_TOO_LONG",
269                format!(
270                    "Tool name exceeds maximum length of {}",
271                    self.rules.max_string_length
272                ),
273                Some("name".to_string()),
274            );
275        }
276
277        // Validate input schema
278        self.validate_tool_input(&tool.input_schema, &mut ctx);
279
280        ctx.into_result()
281    }
282
283    /// Validate a prompt
284    pub fn validate_prompt(&self, prompt: &Prompt) -> ValidationResult {
285        let mut ctx = ValidationContext::new();
286
287        // Validate prompt name
288        if prompt.name.is_empty() {
289            ctx.add_error(
290                "PROMPT_EMPTY_NAME",
291                "Prompt name cannot be empty".to_string(),
292                Some("name".to_string()),
293            );
294        }
295
296        // Validate arguments if present
297        if let Some(arguments) = &prompt.arguments
298            && arguments.len() > self.rules.max_array_length
299        {
300            ctx.add_error(
301                "PROMPT_TOO_MANY_ARGS",
302                format!(
303                    "Prompt has too many arguments (max: {})",
304                    self.rules.max_array_length
305                ),
306                Some("arguments".to_string()),
307            );
308        }
309
310        ctx.into_result()
311    }
312
313    /// Validate a resource
314    pub fn validate_resource(&self, resource: &Resource) -> ValidationResult {
315        let mut ctx = ValidationContext::new();
316
317        // Validate URI length (defense-in-depth before regex)
318        if resource.uri.len() > self.rules.max_string_length {
319            ctx.add_error(
320                "RESOURCE_URI_TOO_LONG",
321                format!(
322                    "Resource URI exceeds maximum length of {}",
323                    self.rules.max_string_length
324                ),
325                Some("uri".to_string()),
326            );
327        }
328
329        // Validate URI format
330        if !self.rules.uri_regex().is_match(&resource.uri) {
331            ctx.add_error(
332                "RESOURCE_INVALID_URI",
333                format!("Invalid URI format: {}", resource.uri),
334                Some("uri".to_string()),
335            );
336        }
337
338        // Validate name
339        if resource.name.is_empty() {
340            ctx.add_error(
341                "RESOURCE_EMPTY_NAME",
342                "Resource name cannot be empty".to_string(),
343                Some("name".to_string()),
344            );
345        }
346
347        ctx.into_result()
348    }
349
350    /// Validate initialization request
351    pub fn validate_initialize_request(&self, request: &InitializeRequest) -> ValidationResult {
352        let mut ctx = ValidationContext::new();
353
354        // Validate protocol version
355        if !crate::SUPPORTED_VERSIONS.contains(&request.protocol_version.as_str()) {
356            ctx.add_warning(
357                "UNSUPPORTED_PROTOCOL_VERSION",
358                format!(
359                    "Protocol version {} is not officially supported",
360                    request.protocol_version
361                ),
362                Some("protocolVersion".to_string()),
363            );
364        }
365
366        // Validate client info
367        if request.client_info.name.is_empty() {
368            ctx.add_error(
369                "EMPTY_CLIENT_NAME",
370                "Client name cannot be empty".to_string(),
371                Some("clientInfo.name".to_string()),
372            );
373        }
374
375        if request.client_info.version.is_empty() {
376            ctx.add_error(
377                "EMPTY_CLIENT_VERSION",
378                "Client version cannot be empty".to_string(),
379                Some("clientInfo.version".to_string()),
380            );
381        }
382
383        ctx.into_result()
384    }
385
386    /// Validate model preferences (priority ranges must be 0.0-1.0)
387    ///
388    /// Per MCP 2025-06-18 schema (lines 1346-1370), priority values must be in range [0.0, 1.0].
389    pub fn validate_model_preferences(
390        &self,
391        prefs: &crate::types::ModelPreferences,
392    ) -> ValidationResult {
393        let mut ctx = ValidationContext::new();
394
395        // Validate each priority field
396        let priorities = [
397            ("costPriority", prefs.cost_priority),
398            ("speedPriority", prefs.speed_priority),
399            ("intelligencePriority", prefs.intelligence_priority),
400        ];
401
402        for (name, value) in priorities {
403            if let Some(v) = value
404                && !(0.0..=1.0).contains(&v)
405            {
406                ctx.add_error(
407                    "PRIORITY_OUT_OF_RANGE",
408                    format!(
409                        "{} must be between 0.0 and 1.0 (inclusive), got {}",
410                        name, v
411                    ),
412                    Some(name.to_string()),
413                );
414            }
415        }
416
417        ctx.into_result()
418    }
419
420    /// Validate elicitation result (content required for 'accept' action)
421    ///
422    /// Per MCP 2025-06-18 schema (line 634), content is "only present when action is 'accept'".
423    pub fn validate_elicit_result(&self, result: &crate::types::ElicitResult) -> ValidationResult {
424        let mut ctx = ValidationContext::new();
425
426        use crate::types::ElicitationAction;
427
428        match result.action {
429            ElicitationAction::Accept => {
430                if result.content.is_none() {
431                    ctx.add_error(
432                        "MISSING_CONTENT_ON_ACCEPT",
433                        "ElicitResult must have content when action is 'accept'".to_string(),
434                        Some("content".to_string()),
435                    );
436                }
437            }
438            ElicitationAction::Decline | ElicitationAction::Cancel => {
439                if result.content.is_some() {
440                    ctx.add_warning(
441                        "UNEXPECTED_CONTENT",
442                        format!(
443                            "Content should not be present when action is '{:?}'",
444                            result.action
445                        ),
446                        Some("content".to_string()),
447                    );
448                }
449            }
450        }
451
452        ctx.into_result()
453    }
454
455    /// Validate elicitation schema structure
456    ///
457    /// Per MCP 2025-06-18 spec, schemas must be flat objects with primitive properties only.
458    pub fn validate_elicitation_schema(
459        &self,
460        schema: &crate::types::ElicitationSchema,
461    ) -> ValidationResult {
462        let mut ctx = ValidationContext::new();
463
464        // Schema type must be "object" (schema.json:585)
465        if schema.schema_type != "object" {
466            ctx.add_error(
467                "SCHEMA_NOT_OBJECT",
468                format!(
469                    "Elicitation schema type must be 'object', got '{}'",
470                    schema.schema_type
471                ),
472                Some("type".to_string()),
473            );
474        }
475
476        // Validate additionalProperties = false (flat constraint)
477        if let Some(additional) = schema.additional_properties
478            && additional
479        {
480            ctx.add_warning(
481                "ADDITIONAL_PROPERTIES_NOT_RECOMMENDED",
482                "Elicitation schemas should have additionalProperties=false for flat structure"
483                    .to_string(),
484                Some("additionalProperties".to_string()),
485            );
486        }
487
488        // Validate properties
489        for (key, prop) in &schema.properties {
490            self.validate_primitive_schema(prop, &format!("properties.{}", key), &mut ctx);
491        }
492
493        ctx.into_result()
494    }
495
496    /// Validate primitive schema definition
497    fn validate_primitive_schema(
498        &self,
499        schema: &crate::types::PrimitiveSchemaDefinition,
500        field_path: &str,
501        ctx: &mut ValidationContext,
502    ) {
503        use crate::types::PrimitiveSchemaDefinition;
504
505        match schema {
506            PrimitiveSchemaDefinition::String {
507                enum_values,
508                enum_names,
509                format,
510                ..
511            } => {
512                // Validate enum/enumNames length match (schema.json:679-708)
513                if let (Some(values), Some(names)) = (enum_values, enum_names)
514                    && values.len() != names.len()
515                {
516                    ctx.add_error(
517                        "ENUM_NAMES_LENGTH_MISMATCH",
518                        format!(
519                            "enum and enumNames arrays must have equal length: {} vs {}",
520                            values.len(),
521                            names.len()
522                        ),
523                        Some(format!("{}.enumNames", field_path)),
524                    );
525                }
526
527                // Validate format if present (schema.json:2244-2251)
528                if let Some(fmt) = format {
529                    let valid_formats = ["email", "uri", "date", "date-time"];
530                    if !valid_formats.contains(&fmt.as_str()) {
531                        ctx.add_warning(
532                            "UNKNOWN_STRING_FORMAT",
533                            format!(
534                                "Unknown format '{}', expected one of: {:?}",
535                                fmt, valid_formats
536                            ),
537                            Some(format!("{}.format", field_path)),
538                        );
539                    }
540                }
541            }
542            PrimitiveSchemaDefinition::Number { .. }
543            | PrimitiveSchemaDefinition::Integer { .. } => {
544                // Number/Integer validation could go here
545            }
546            PrimitiveSchemaDefinition::Boolean { .. } => {
547                // Boolean validation could go here
548            }
549        }
550    }
551
552    /// Validate string value against format constraints
553    ///
554    /// Validates email, uri, date, and date-time formats per MCP 2025-06-18 spec.
555    pub fn validate_string_format(value: &str, format: &str) -> std::result::Result<(), String> {
556        match format {
557            "email" => {
558                // RFC 5322 basic validation
559                if !value.contains('@') || !value.contains('.') {
560                    return Err(format!("Invalid email format: {}", value));
561                }
562                let parts: Vec<&str> = value.split('@').collect();
563                if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
564                    return Err(format!("Invalid email format: {}", value));
565                }
566            }
567            "uri" => {
568                // Basic URI validation - must have a scheme
569                if !value.contains("://") && !value.starts_with('/') {
570                    return Err(format!("Invalid URI format: {}", value));
571                }
572            }
573            "date" => {
574                // ISO 8601 date format: YYYY-MM-DD
575                let parts: Vec<&str> = value.split('-').collect();
576                if parts.len() != 3 {
577                    return Err("Date must be in ISO 8601 format (YYYY-MM-DD)".to_string());
578                }
579                if parts[0].len() != 4 || parts[1].len() != 2 || parts[2].len() != 2 {
580                    return Err("Date must be in ISO 8601 format (YYYY-MM-DD)".to_string());
581                }
582                // Basic numeric check
583                for part in parts {
584                    if !part.chars().all(|c| c.is_ascii_digit()) {
585                        return Err("Date components must be numeric".to_string());
586                    }
587                }
588            }
589            "date-time" => {
590                // ISO 8601 datetime format: YYYY-MM-DDTHH:MM:SS[.sss][Z|±HH:MM]
591                if !value.contains('T') {
592                    return Err("DateTime must contain 'T' separator (ISO 8601 format)".to_string());
593                }
594                let parts: Vec<&str> = value.split('T').collect();
595                if parts.len() != 2 {
596                    return Err("DateTime must be in ISO 8601 format".to_string());
597                }
598                // Validate date part
599                Self::validate_string_format(parts[0], "date")?;
600                // Time part should have colons
601                if !parts[1].contains(':') {
602                    return Err("Time component must contain ':'".to_string());
603                }
604            }
605            _ => {
606                // Unknown formats don't fail validation (forward compatibility)
607            }
608        }
609        Ok(())
610    }
611
612    // Private validation methods
613
614    fn validate_jsonrpc_request(&self, request: &JsonRpcRequest, ctx: &mut ValidationContext) {
615        // Validate JSON-RPC version (implicitly "2.0" via JsonRpcVersion type)
616        // This is handled by type system during deserialization
617
618        // Validate method name - check length first, then format
619        if request.method.is_empty() {
620            ctx.add_error(
621                "EMPTY_METHOD_NAME",
622                "Method name cannot be empty".to_string(),
623                Some("method".to_string()),
624            );
625        } else if request.method.len() > self.rules.max_string_length {
626            ctx.add_error(
627                "METHOD_NAME_TOO_LONG",
628                format!(
629                    "Method name exceeds maximum length of {}",
630                    self.rules.max_string_length
631                ),
632                Some("method".to_string()),
633            );
634        } else if !utils::is_valid_method_name(&request.method) {
635            ctx.add_error(
636                "INVALID_METHOD_NAME",
637                format!("Invalid method name format: '{}'", request.method),
638                Some("method".to_string()),
639            );
640        }
641
642        // Validate parameters if present
643        if let Some(ref params) = request.params {
644            self.validate_parameters(params, ctx);
645        }
646
647        // Request ID is always present for requests (enforced by type system)
648        // Validate ID format if needed
649        self.validate_request_id(&request.id, ctx);
650    }
651
652    fn validate_jsonrpc_response(&self, response: &JsonRpcResponse, ctx: &mut ValidationContext) {
653        // Validate JSON-RPC version (implicitly "2.0" via JsonRpcVersion type)
654        // This is handled by type system during deserialization
655
656        // Validate response has either result or error (enforced by type system)
657        // Our JsonRpcResponsePayload enum ensures mutual exclusion
658
659        // Validate response ID
660        self.validate_response_id(&response.id, ctx);
661
662        // Validate error if present
663        if let Some(error) = response.error() {
664            self.validate_jsonrpc_error(error, ctx);
665        }
666
667        // Validate result structure if present
668        if let Some(result) = response.result() {
669            self.validate_result_value(result, ctx);
670        }
671    }
672
673    fn validate_jsonrpc_notification(
674        &self,
675        notification: &JsonRpcNotification,
676        ctx: &mut ValidationContext,
677    ) {
678        // Validate JSON-RPC version (implicitly "2.0" via JsonRpcVersion type)
679        // This is handled by type system during deserialization
680
681        // Validate method name - check length first, then format
682        if notification.method.is_empty() {
683            ctx.add_error(
684                "EMPTY_METHOD_NAME",
685                "Method name cannot be empty".to_string(),
686                Some("method".to_string()),
687            );
688        } else if notification.method.len() > self.rules.max_string_length {
689            ctx.add_error(
690                "METHOD_NAME_TOO_LONG",
691                format!(
692                    "Method name exceeds maximum length of {}",
693                    self.rules.max_string_length
694                ),
695                Some("method".to_string()),
696            );
697        } else if !utils::is_valid_method_name(&notification.method) {
698            ctx.add_error(
699                "INVALID_METHOD_NAME",
700                format!("Invalid method name format: '{}'", notification.method),
701                Some("method".to_string()),
702            );
703        }
704
705        // Validate parameters if present
706        if let Some(ref params) = notification.params {
707            self.validate_parameters(params, ctx);
708        }
709
710        // Notifications do NOT have an ID field (enforced by type system)
711    }
712
713    fn validate_jsonrpc_error(
714        &self,
715        error: &crate::jsonrpc::JsonRpcError,
716        ctx: &mut ValidationContext,
717    ) {
718        // Error codes should be in the valid range
719        if error.code >= 0 {
720            ctx.add_warning(
721                "POSITIVE_ERROR_CODE",
722                "Error codes should be negative according to JSON-RPC spec".to_string(),
723                Some("error.code".to_string()),
724            );
725        }
726
727        if error.message.is_empty() {
728            ctx.add_error(
729                "EMPTY_ERROR_MESSAGE",
730                "Error message cannot be empty".to_string(),
731                Some("error.message".to_string()),
732            );
733        }
734    }
735
736    fn validate_method_name(&self, method: &str, ctx: &mut ValidationContext) {
737        if method.is_empty() {
738            ctx.add_error(
739                "EMPTY_METHOD_NAME",
740                "Method name cannot be empty".to_string(),
741                Some("method".to_string()),
742            );
743            return;
744        }
745
746        if !self.rules.method_name_regex().is_match(method) {
747            ctx.add_error(
748                "INVALID_METHOD_NAME",
749                format!("Invalid method name format: {method}"),
750                Some("method".to_string()),
751            );
752        }
753    }
754
755    fn validate_method_params(&self, method: &str, params: &Value, ctx: &mut ValidationContext) {
756        ctx.push_path("params".to_string());
757
758        match method {
759            "initialize" => self.validate_value_structure(params, "initialize", ctx),
760            "tools/list" => {
761                // Should be empty object or null
762                if !params.is_null() && !params.as_object().is_some_and(|obj| obj.is_empty()) {
763                    ctx.add_warning(
764                        "UNEXPECTED_PARAMS",
765                        "tools/list should not have parameters".to_string(),
766                        None,
767                    );
768                }
769            }
770            "tools/call" => self.validate_value_structure(params, "call_tool", ctx),
771            _ => {
772                // Unknown method - validate basic structure
773                self.validate_value_structure(params, "generic", ctx);
774            }
775        }
776
777        ctx.pop_path();
778    }
779
780    fn validate_tool_input(&self, input: &ToolInputSchema, ctx: &mut ValidationContext) {
781        ctx.push_path("inputSchema".to_string());
782
783        // Validate schema type
784        if input.schema_type != "object" {
785            ctx.add_warning(
786                "NON_OBJECT_SCHEMA",
787                "Tool input schema should typically be 'object'".to_string(),
788                Some("type".to_string()),
789            );
790        }
791
792        ctx.pop_path();
793    }
794
795    fn validate_value_structure(
796        &self,
797        value: &Value,
798        _expected_type: &str,
799        ctx: &mut ValidationContext,
800    ) {
801        // Prevent infinite recursion
802        if ctx.depth > self.rules.max_object_depth {
803            ctx.add_error(
804                "MAX_DEPTH_EXCEEDED",
805                format!(
806                    "Maximum object depth ({}) exceeded",
807                    self.rules.max_object_depth
808                ),
809                None,
810            );
811            return;
812        }
813
814        match value {
815            Value::Object(obj) => {
816                ctx.depth += 1;
817                for (key, val) in obj {
818                    ctx.push_path(key.clone());
819                    self.validate_value_structure(val, "unknown", ctx);
820                    ctx.pop_path();
821                }
822                ctx.depth -= 1;
823            }
824            Value::Array(arr) => {
825                if arr.len() > self.rules.max_array_length {
826                    ctx.add_error(
827                        "ARRAY_TOO_LONG",
828                        format!(
829                            "Array exceeds maximum length of {}",
830                            self.rules.max_array_length
831                        ),
832                        None,
833                    );
834                }
835
836                for (index, val) in arr.iter().enumerate() {
837                    ctx.push_path(index.to_string());
838                    self.validate_value_structure(val, "unknown", ctx);
839                    ctx.pop_path();
840                }
841            }
842            Value::String(s) => {
843                if s.len() > self.rules.max_string_length {
844                    ctx.add_error(
845                        "STRING_TOO_LONG",
846                        format!(
847                            "String exceeds maximum length of {}",
848                            self.rules.max_string_length
849                        ),
850                        None,
851                    );
852                }
853            }
854            _ => {} // Other types are fine
855        }
856    }
857
858    fn validate_parameters(&self, params: &Value, ctx: &mut ValidationContext) {
859        // Validate parameter structure depth and content
860        self.validate_value_structure(params, "params", ctx);
861
862        // Additional parameter-specific validation
863        match params {
864            Value::Array(arr) => {
865                // Validate array parameters length
866                if arr.len() > self.rules.max_array_length {
867                    ctx.add_error(
868                        "PARAMS_ARRAY_TOO_LONG",
869                        format!(
870                            "Parameter array exceeds maximum length of {}",
871                            self.rules.max_array_length
872                        ),
873                        Some("params".to_string()),
874                    );
875                }
876            }
877            _ => {
878                // Other parameter types are acceptable
879            }
880        }
881    }
882
883    fn validate_request_id(&self, _id: &crate::types::RequestId, _ctx: &mut ValidationContext) {
884        // Request ID validation
885        // ID is always present for requests (enforced by type system)
886        // Additional ID format validation could be added here if needed
887    }
888
889    fn validate_response_id(&self, id: &crate::jsonrpc::ResponseId, _ctx: &mut ValidationContext) {
890        // Validate response ID semantics
891        if id.is_null() {
892            // Null ID is only valid for parse errors
893            // This should be checked at a higher level when the error type is known
894        }
895        // Additional response ID validation could be added here
896    }
897
898    fn validate_result_value(&self, result: &Value, ctx: &mut ValidationContext) {
899        // Validate result structure depth and content
900        self.validate_value_structure(result, "result", ctx);
901
902        // Additional result validation based on method type could be added here
903        // For now, we just validate general structure
904    }
905}
906
907impl Default for ProtocolValidator {
908    fn default() -> Self {
909        Self::new()
910    }
911}
912
913impl ValidationContext {
914    fn new() -> Self {
915        Self {
916            path: Vec::new(),
917            depth: 0,
918            warnings: Vec::new(),
919            errors: Vec::new(),
920        }
921    }
922
923    fn push_path(&mut self, segment: String) {
924        self.path.push(segment);
925    }
926
927    fn pop_path(&mut self) {
928        self.path.pop();
929    }
930
931    fn current_path(&self) -> Option<String> {
932        if self.path.is_empty() {
933            None
934        } else {
935            Some(self.path.join("."))
936        }
937    }
938
939    fn add_error(&mut self, code: &str, message: String, field_path: Option<String>) {
940        let path = field_path.or_else(|| self.current_path());
941        self.errors.push(ValidationError {
942            code: code.to_string(),
943            message,
944            field_path: path,
945        });
946    }
947
948    fn add_warning(&mut self, code: &str, message: String, field_path: Option<String>) {
949        let path = field_path.or_else(|| self.current_path());
950        self.warnings.push(ValidationWarning {
951            code: code.to_string(),
952            message,
953            field_path: path,
954        });
955    }
956
957    fn into_result(self) -> ValidationResult {
958        if !self.errors.is_empty() {
959            ValidationResult::Invalid(self.errors)
960        } else if !self.warnings.is_empty() {
961            ValidationResult::ValidWithWarnings(self.warnings)
962        } else {
963            ValidationResult::Valid
964        }
965    }
966}
967
968impl ValidationResult {
969    /// Check if validation passed (with or without warnings)
970    pub fn is_valid(&self) -> bool {
971        !matches!(self, ValidationResult::Invalid(_))
972    }
973
974    /// Check if validation failed
975    pub fn is_invalid(&self) -> bool {
976        matches!(self, ValidationResult::Invalid(_))
977    }
978
979    /// Check if validation has warnings
980    pub fn has_warnings(&self) -> bool {
981        matches!(self, ValidationResult::ValidWithWarnings(_))
982    }
983
984    /// Get warnings (if any)
985    pub fn warnings(&self) -> &[ValidationWarning] {
986        match self {
987            ValidationResult::ValidWithWarnings(warnings) => warnings,
988            _ => &[],
989        }
990    }
991
992    /// Get errors (if any)
993    pub fn errors(&self) -> &[ValidationError] {
994        match self {
995            ValidationResult::Invalid(errors) => errors,
996            _ => &[],
997        }
998    }
999}
1000
1001/// Utility functions for validation
1002pub mod utils {
1003    use super::*;
1004
1005    /// Create a validation error
1006    pub fn error(code: &str, message: &str) -> ValidationError {
1007        ValidationError {
1008            code: code.to_string(),
1009            message: message.to_string(),
1010            field_path: None,
1011        }
1012    }
1013
1014    /// Create a validation warning
1015    pub fn warning(code: &str, message: &str) -> ValidationWarning {
1016        ValidationWarning {
1017            code: code.to_string(),
1018            message: message.to_string(),
1019            field_path: None,
1020        }
1021    }
1022
1023    /// Check if a string is a valid URI
1024    pub fn is_valid_uri(uri: &str) -> bool {
1025        ValidationRules::default().uri_regex().is_match(uri)
1026    }
1027
1028    /// Check if a string is a valid method name
1029    pub fn is_valid_method_name(method: &str) -> bool {
1030        ValidationRules::default()
1031            .method_name_regex()
1032            .is_match(method)
1033    }
1034}
1035
1036// Comprehensive tests in separate file (tokio/axum pattern)
1037// This gives us:
1038// - Better organization (tests don't clutter the implementation)
1039// - Access to private items (tests are still part of the module)
1040// - Easy to find (tests.rs is in the same directory as validation.rs)
1041#[cfg(test)]
1042mod tests;