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
318        if !self.rules.uri_regex().is_match(&resource.uri) {
319            ctx.add_error(
320                "RESOURCE_INVALID_URI",
321                format!("Invalid URI format: {}", resource.uri),
322                Some("uri".to_string()),
323            );
324        }
325
326        // Validate name
327        if resource.name.is_empty() {
328            ctx.add_error(
329                "RESOURCE_EMPTY_NAME",
330                "Resource name cannot be empty".to_string(),
331                Some("name".to_string()),
332            );
333        }
334
335        ctx.into_result()
336    }
337
338    /// Validate initialization request
339    pub fn validate_initialize_request(&self, request: &InitializeRequest) -> ValidationResult {
340        let mut ctx = ValidationContext::new();
341
342        // Validate protocol version
343        if !crate::SUPPORTED_VERSIONS.contains(&request.protocol_version.as_str()) {
344            ctx.add_warning(
345                "UNSUPPORTED_PROTOCOL_VERSION",
346                format!(
347                    "Protocol version {} is not officially supported",
348                    request.protocol_version
349                ),
350                Some("protocolVersion".to_string()),
351            );
352        }
353
354        // Validate client info
355        if request.client_info.name.is_empty() {
356            ctx.add_error(
357                "EMPTY_CLIENT_NAME",
358                "Client name cannot be empty".to_string(),
359                Some("clientInfo.name".to_string()),
360            );
361        }
362
363        if request.client_info.version.is_empty() {
364            ctx.add_error(
365                "EMPTY_CLIENT_VERSION",
366                "Client version cannot be empty".to_string(),
367                Some("clientInfo.version".to_string()),
368            );
369        }
370
371        ctx.into_result()
372    }
373
374    /// Validate model preferences (priority ranges must be 0.0-1.0)
375    ///
376    /// Per MCP 2025-06-18 schema (lines 1346-1370), priority values must be in range [0.0, 1.0].
377    pub fn validate_model_preferences(
378        &self,
379        prefs: &crate::types::ModelPreferences,
380    ) -> ValidationResult {
381        let mut ctx = ValidationContext::new();
382
383        // Validate each priority field
384        let priorities = [
385            ("costPriority", prefs.cost_priority),
386            ("speedPriority", prefs.speed_priority),
387            ("intelligencePriority", prefs.intelligence_priority),
388        ];
389
390        for (name, value) in priorities {
391            if let Some(v) = value
392                && !(0.0..=1.0).contains(&v)
393            {
394                ctx.add_error(
395                    "PRIORITY_OUT_OF_RANGE",
396                    format!(
397                        "{} must be between 0.0 and 1.0 (inclusive), got {}",
398                        name, v
399                    ),
400                    Some(name.to_string()),
401                );
402            }
403        }
404
405        ctx.into_result()
406    }
407
408    /// Validate elicitation result (content required for 'accept' action)
409    ///
410    /// Per MCP 2025-06-18 schema (line 634), content is "only present when action is 'accept'".
411    pub fn validate_elicit_result(&self, result: &crate::types::ElicitResult) -> ValidationResult {
412        let mut ctx = ValidationContext::new();
413
414        use crate::types::ElicitationAction;
415
416        match result.action {
417            ElicitationAction::Accept => {
418                if result.content.is_none() {
419                    ctx.add_error(
420                        "MISSING_CONTENT_ON_ACCEPT",
421                        "ElicitResult must have content when action is 'accept'".to_string(),
422                        Some("content".to_string()),
423                    );
424                }
425            }
426            ElicitationAction::Decline | ElicitationAction::Cancel => {
427                if result.content.is_some() {
428                    ctx.add_warning(
429                        "UNEXPECTED_CONTENT",
430                        format!(
431                            "Content should not be present when action is '{:?}'",
432                            result.action
433                        ),
434                        Some("content".to_string()),
435                    );
436                }
437            }
438        }
439
440        ctx.into_result()
441    }
442
443    /// Validate elicitation schema structure
444    ///
445    /// Per MCP 2025-06-18 spec, schemas must be flat objects with primitive properties only.
446    pub fn validate_elicitation_schema(
447        &self,
448        schema: &crate::types::ElicitationSchema,
449    ) -> ValidationResult {
450        let mut ctx = ValidationContext::new();
451
452        // Schema type must be "object" (schema.json:585)
453        if schema.schema_type != "object" {
454            ctx.add_error(
455                "SCHEMA_NOT_OBJECT",
456                format!(
457                    "Elicitation schema type must be 'object', got '{}'",
458                    schema.schema_type
459                ),
460                Some("type".to_string()),
461            );
462        }
463
464        // Validate additionalProperties = false (flat constraint)
465        if let Some(additional) = schema.additional_properties
466            && additional
467        {
468            ctx.add_warning(
469                "ADDITIONAL_PROPERTIES_NOT_RECOMMENDED",
470                "Elicitation schemas should have additionalProperties=false for flat structure"
471                    .to_string(),
472                Some("additionalProperties".to_string()),
473            );
474        }
475
476        // Validate properties
477        for (key, prop) in &schema.properties {
478            self.validate_primitive_schema(prop, &format!("properties.{}", key), &mut ctx);
479        }
480
481        ctx.into_result()
482    }
483
484    /// Validate primitive schema definition
485    fn validate_primitive_schema(
486        &self,
487        schema: &crate::types::PrimitiveSchemaDefinition,
488        field_path: &str,
489        ctx: &mut ValidationContext,
490    ) {
491        use crate::types::PrimitiveSchemaDefinition;
492
493        match schema {
494            PrimitiveSchemaDefinition::String {
495                enum_values,
496                enum_names,
497                format,
498                ..
499            } => {
500                // Validate enum/enumNames length match (schema.json:679-708)
501                if let (Some(values), Some(names)) = (enum_values, enum_names)
502                    && values.len() != names.len()
503                {
504                    ctx.add_error(
505                        "ENUM_NAMES_LENGTH_MISMATCH",
506                        format!(
507                            "enum and enumNames arrays must have equal length: {} vs {}",
508                            values.len(),
509                            names.len()
510                        ),
511                        Some(format!("{}.enumNames", field_path)),
512                    );
513                }
514
515                // Validate format if present (schema.json:2244-2251)
516                if let Some(fmt) = format {
517                    let valid_formats = ["email", "uri", "date", "date-time"];
518                    if !valid_formats.contains(&fmt.as_str()) {
519                        ctx.add_warning(
520                            "UNKNOWN_STRING_FORMAT",
521                            format!(
522                                "Unknown format '{}', expected one of: {:?}",
523                                fmt, valid_formats
524                            ),
525                            Some(format!("{}.format", field_path)),
526                        );
527                    }
528                }
529            }
530            PrimitiveSchemaDefinition::Number { .. }
531            | PrimitiveSchemaDefinition::Integer { .. } => {
532                // Number/Integer validation could go here
533            }
534            PrimitiveSchemaDefinition::Boolean { .. } => {
535                // Boolean validation could go here
536            }
537        }
538    }
539
540    /// Validate string value against format constraints
541    ///
542    /// Validates email, uri, date, and date-time formats per MCP 2025-06-18 spec.
543    pub fn validate_string_format(value: &str, format: &str) -> std::result::Result<(), String> {
544        match format {
545            "email" => {
546                // RFC 5322 basic validation
547                if !value.contains('@') || !value.contains('.') {
548                    return Err(format!("Invalid email format: {}", value));
549                }
550                let parts: Vec<&str> = value.split('@').collect();
551                if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
552                    return Err(format!("Invalid email format: {}", value));
553                }
554            }
555            "uri" => {
556                // Basic URI validation - must have a scheme
557                if !value.contains("://") && !value.starts_with('/') {
558                    return Err(format!("Invalid URI format: {}", value));
559                }
560            }
561            "date" => {
562                // ISO 8601 date format: YYYY-MM-DD
563                let parts: Vec<&str> = value.split('-').collect();
564                if parts.len() != 3 {
565                    return Err("Date must be in ISO 8601 format (YYYY-MM-DD)".to_string());
566                }
567                if parts[0].len() != 4 || parts[1].len() != 2 || parts[2].len() != 2 {
568                    return Err("Date must be in ISO 8601 format (YYYY-MM-DD)".to_string());
569                }
570                // Basic numeric check
571                for part in parts {
572                    if !part.chars().all(|c| c.is_ascii_digit()) {
573                        return Err("Date components must be numeric".to_string());
574                    }
575                }
576            }
577            "date-time" => {
578                // ISO 8601 datetime format: YYYY-MM-DDTHH:MM:SS[.sss][Z|±HH:MM]
579                if !value.contains('T') {
580                    return Err("DateTime must contain 'T' separator (ISO 8601 format)".to_string());
581                }
582                let parts: Vec<&str> = value.split('T').collect();
583                if parts.len() != 2 {
584                    return Err("DateTime must be in ISO 8601 format".to_string());
585                }
586                // Validate date part
587                Self::validate_string_format(parts[0], "date")?;
588                // Time part should have colons
589                if !parts[1].contains(':') {
590                    return Err("Time component must contain ':'".to_string());
591                }
592            }
593            _ => {
594                // Unknown formats don't fail validation (forward compatibility)
595            }
596        }
597        Ok(())
598    }
599
600    // Private validation methods
601
602    fn validate_jsonrpc_request(&self, request: &JsonRpcRequest, ctx: &mut ValidationContext) {
603        // Validate JSON-RPC version (implicitly "2.0" via JsonRpcVersion type)
604        // This is handled by type system during deserialization
605
606        // Validate method name - check length first, then format
607        if request.method.is_empty() {
608            ctx.add_error(
609                "EMPTY_METHOD_NAME",
610                "Method name cannot be empty".to_string(),
611                Some("method".to_string()),
612            );
613        } else if request.method.len() > self.rules.max_string_length {
614            ctx.add_error(
615                "METHOD_NAME_TOO_LONG",
616                format!(
617                    "Method name exceeds maximum length of {}",
618                    self.rules.max_string_length
619                ),
620                Some("method".to_string()),
621            );
622        } else if !utils::is_valid_method_name(&request.method) {
623            ctx.add_error(
624                "INVALID_METHOD_NAME",
625                format!("Invalid method name format: '{}'", request.method),
626                Some("method".to_string()),
627            );
628        }
629
630        // Validate parameters if present
631        if let Some(ref params) = request.params {
632            self.validate_parameters(params, ctx);
633        }
634
635        // Request ID is always present for requests (enforced by type system)
636        // Validate ID format if needed
637        self.validate_request_id(&request.id, ctx);
638    }
639
640    fn validate_jsonrpc_response(&self, response: &JsonRpcResponse, ctx: &mut ValidationContext) {
641        // Validate JSON-RPC version (implicitly "2.0" via JsonRpcVersion type)
642        // This is handled by type system during deserialization
643
644        // Validate response has either result or error (enforced by type system)
645        // Our JsonRpcResponsePayload enum ensures mutual exclusion
646
647        // Validate response ID
648        self.validate_response_id(&response.id, ctx);
649
650        // Validate error if present
651        if let Some(error) = response.error() {
652            self.validate_jsonrpc_error(error, ctx);
653        }
654
655        // Validate result structure if present
656        if let Some(result) = response.result() {
657            self.validate_result_value(result, ctx);
658        }
659    }
660
661    fn validate_jsonrpc_notification(
662        &self,
663        notification: &JsonRpcNotification,
664        ctx: &mut ValidationContext,
665    ) {
666        // Validate JSON-RPC version (implicitly "2.0" via JsonRpcVersion type)
667        // This is handled by type system during deserialization
668
669        // Validate method name - check length first, then format
670        if notification.method.is_empty() {
671            ctx.add_error(
672                "EMPTY_METHOD_NAME",
673                "Method name cannot be empty".to_string(),
674                Some("method".to_string()),
675            );
676        } else if notification.method.len() > self.rules.max_string_length {
677            ctx.add_error(
678                "METHOD_NAME_TOO_LONG",
679                format!(
680                    "Method name exceeds maximum length of {}",
681                    self.rules.max_string_length
682                ),
683                Some("method".to_string()),
684            );
685        } else if !utils::is_valid_method_name(&notification.method) {
686            ctx.add_error(
687                "INVALID_METHOD_NAME",
688                format!("Invalid method name format: '{}'", notification.method),
689                Some("method".to_string()),
690            );
691        }
692
693        // Validate parameters if present
694        if let Some(ref params) = notification.params {
695            self.validate_parameters(params, ctx);
696        }
697
698        // Notifications do NOT have an ID field (enforced by type system)
699    }
700
701    fn validate_jsonrpc_error(
702        &self,
703        error: &crate::jsonrpc::JsonRpcError,
704        ctx: &mut ValidationContext,
705    ) {
706        // Error codes should be in the valid range
707        if error.code >= 0 {
708            ctx.add_warning(
709                "POSITIVE_ERROR_CODE",
710                "Error codes should be negative according to JSON-RPC spec".to_string(),
711                Some("error.code".to_string()),
712            );
713        }
714
715        if error.message.is_empty() {
716            ctx.add_error(
717                "EMPTY_ERROR_MESSAGE",
718                "Error message cannot be empty".to_string(),
719                Some("error.message".to_string()),
720            );
721        }
722    }
723
724    fn validate_method_name(&self, method: &str, ctx: &mut ValidationContext) {
725        if method.is_empty() {
726            ctx.add_error(
727                "EMPTY_METHOD_NAME",
728                "Method name cannot be empty".to_string(),
729                Some("method".to_string()),
730            );
731            return;
732        }
733
734        if !self.rules.method_name_regex().is_match(method) {
735            ctx.add_error(
736                "INVALID_METHOD_NAME",
737                format!("Invalid method name format: {method}"),
738                Some("method".to_string()),
739            );
740        }
741    }
742
743    fn validate_method_params(&self, method: &str, params: &Value, ctx: &mut ValidationContext) {
744        ctx.push_path("params".to_string());
745
746        match method {
747            "initialize" => self.validate_value_structure(params, "initialize", ctx),
748            "tools/list" => {
749                // Should be empty object or null
750                if !params.is_null() && !params.as_object().is_some_and(|obj| obj.is_empty()) {
751                    ctx.add_warning(
752                        "UNEXPECTED_PARAMS",
753                        "tools/list should not have parameters".to_string(),
754                        None,
755                    );
756                }
757            }
758            "tools/call" => self.validate_value_structure(params, "call_tool", ctx),
759            _ => {
760                // Unknown method - validate basic structure
761                self.validate_value_structure(params, "generic", ctx);
762            }
763        }
764
765        ctx.pop_path();
766    }
767
768    fn validate_tool_input(&self, input: &ToolInputSchema, ctx: &mut ValidationContext) {
769        ctx.push_path("inputSchema".to_string());
770
771        // Validate schema type
772        if input.schema_type != "object" {
773            ctx.add_warning(
774                "NON_OBJECT_SCHEMA",
775                "Tool input schema should typically be 'object'".to_string(),
776                Some("type".to_string()),
777            );
778        }
779
780        ctx.pop_path();
781    }
782
783    fn validate_value_structure(
784        &self,
785        value: &Value,
786        _expected_type: &str,
787        ctx: &mut ValidationContext,
788    ) {
789        // Prevent infinite recursion
790        if ctx.depth > self.rules.max_object_depth {
791            ctx.add_error(
792                "MAX_DEPTH_EXCEEDED",
793                format!(
794                    "Maximum object depth ({}) exceeded",
795                    self.rules.max_object_depth
796                ),
797                None,
798            );
799            return;
800        }
801
802        match value {
803            Value::Object(obj) => {
804                ctx.depth += 1;
805                for (key, val) in obj {
806                    ctx.push_path(key.clone());
807                    self.validate_value_structure(val, "unknown", ctx);
808                    ctx.pop_path();
809                }
810                ctx.depth -= 1;
811            }
812            Value::Array(arr) => {
813                if arr.len() > self.rules.max_array_length {
814                    ctx.add_error(
815                        "ARRAY_TOO_LONG",
816                        format!(
817                            "Array exceeds maximum length of {}",
818                            self.rules.max_array_length
819                        ),
820                        None,
821                    );
822                }
823
824                for (index, val) in arr.iter().enumerate() {
825                    ctx.push_path(index.to_string());
826                    self.validate_value_structure(val, "unknown", ctx);
827                    ctx.pop_path();
828                }
829            }
830            Value::String(s) => {
831                if s.len() > self.rules.max_string_length {
832                    ctx.add_error(
833                        "STRING_TOO_LONG",
834                        format!(
835                            "String exceeds maximum length of {}",
836                            self.rules.max_string_length
837                        ),
838                        None,
839                    );
840                }
841            }
842            _ => {} // Other types are fine
843        }
844    }
845
846    fn validate_parameters(&self, params: &Value, ctx: &mut ValidationContext) {
847        // Validate parameter structure depth and content
848        self.validate_value_structure(params, "params", ctx);
849
850        // Additional parameter-specific validation
851        match params {
852            Value::Array(arr) => {
853                // Validate array parameters length
854                if arr.len() > self.rules.max_array_length {
855                    ctx.add_error(
856                        "PARAMS_ARRAY_TOO_LONG",
857                        format!(
858                            "Parameter array exceeds maximum length of {}",
859                            self.rules.max_array_length
860                        ),
861                        Some("params".to_string()),
862                    );
863                }
864            }
865            _ => {
866                // Other parameter types are acceptable
867            }
868        }
869    }
870
871    fn validate_request_id(&self, _id: &crate::types::RequestId, _ctx: &mut ValidationContext) {
872        // Request ID validation
873        // ID is always present for requests (enforced by type system)
874        // Additional ID format validation could be added here if needed
875    }
876
877    fn validate_response_id(&self, id: &crate::jsonrpc::ResponseId, _ctx: &mut ValidationContext) {
878        // Validate response ID semantics
879        if id.is_null() {
880            // Null ID is only valid for parse errors
881            // This should be checked at a higher level when the error type is known
882        }
883        // Additional response ID validation could be added here
884    }
885
886    fn validate_result_value(&self, result: &Value, ctx: &mut ValidationContext) {
887        // Validate result structure depth and content
888        self.validate_value_structure(result, "result", ctx);
889
890        // Additional result validation based on method type could be added here
891        // For now, we just validate general structure
892    }
893}
894
895impl Default for ProtocolValidator {
896    fn default() -> Self {
897        Self::new()
898    }
899}
900
901impl ValidationContext {
902    fn new() -> Self {
903        Self {
904            path: Vec::new(),
905            depth: 0,
906            warnings: Vec::new(),
907            errors: Vec::new(),
908        }
909    }
910
911    fn push_path(&mut self, segment: String) {
912        self.path.push(segment);
913    }
914
915    fn pop_path(&mut self) {
916        self.path.pop();
917    }
918
919    fn current_path(&self) -> Option<String> {
920        if self.path.is_empty() {
921            None
922        } else {
923            Some(self.path.join("."))
924        }
925    }
926
927    fn add_error(&mut self, code: &str, message: String, field_path: Option<String>) {
928        let path = field_path.or_else(|| self.current_path());
929        self.errors.push(ValidationError {
930            code: code.to_string(),
931            message,
932            field_path: path,
933        });
934    }
935
936    fn add_warning(&mut self, code: &str, message: String, field_path: Option<String>) {
937        let path = field_path.or_else(|| self.current_path());
938        self.warnings.push(ValidationWarning {
939            code: code.to_string(),
940            message,
941            field_path: path,
942        });
943    }
944
945    fn into_result(self) -> ValidationResult {
946        if !self.errors.is_empty() {
947            ValidationResult::Invalid(self.errors)
948        } else if !self.warnings.is_empty() {
949            ValidationResult::ValidWithWarnings(self.warnings)
950        } else {
951            ValidationResult::Valid
952        }
953    }
954}
955
956impl ValidationResult {
957    /// Check if validation passed (with or without warnings)
958    pub fn is_valid(&self) -> bool {
959        !matches!(self, ValidationResult::Invalid(_))
960    }
961
962    /// Check if validation failed
963    pub fn is_invalid(&self) -> bool {
964        matches!(self, ValidationResult::Invalid(_))
965    }
966
967    /// Check if validation has warnings
968    pub fn has_warnings(&self) -> bool {
969        matches!(self, ValidationResult::ValidWithWarnings(_))
970    }
971
972    /// Get warnings (if any)
973    pub fn warnings(&self) -> &[ValidationWarning] {
974        match self {
975            ValidationResult::ValidWithWarnings(warnings) => warnings,
976            _ => &[],
977        }
978    }
979
980    /// Get errors (if any)
981    pub fn errors(&self) -> &[ValidationError] {
982        match self {
983            ValidationResult::Invalid(errors) => errors,
984            _ => &[],
985        }
986    }
987}
988
989/// Utility functions for validation
990pub mod utils {
991    use super::*;
992
993    /// Create a validation error
994    pub fn error(code: &str, message: &str) -> ValidationError {
995        ValidationError {
996            code: code.to_string(),
997            message: message.to_string(),
998            field_path: None,
999        }
1000    }
1001
1002    /// Create a validation warning
1003    pub fn warning(code: &str, message: &str) -> ValidationWarning {
1004        ValidationWarning {
1005            code: code.to_string(),
1006            message: message.to_string(),
1007            field_path: None,
1008        }
1009    }
1010
1011    /// Check if a string is a valid URI
1012    pub fn is_valid_uri(uri: &str) -> bool {
1013        ValidationRules::default().uri_regex().is_match(uri)
1014    }
1015
1016    /// Check if a string is a valid method name
1017    pub fn is_valid_method_name(method: &str) -> bool {
1018        ValidationRules::default()
1019            .method_name_regex()
1020            .is_match(method)
1021    }
1022}
1023
1024// Comprehensive tests in separate file (tokio/axum pattern)
1025// This gives us:
1026// - Better organization (tests don't clutter the implementation)
1027// - Access to private items (tests are still part of the module)
1028// - Easy to find (tests.rs is in the same directory as validation.rs)
1029#[cfg(test)]
1030mod tests;