1use regex::Regex;
7use serde_json::Value;
8use std::collections::{HashMap, HashSet};
9use std::sync::LazyLock;
10
11use crate::jsonrpc::{JsonRpcNotification, JsonRpcRequest, JsonRpcResponse};
12use crate::types::*;
13
14static URI_REGEX: LazyLock<Regex> =
16 LazyLock::new(|| Regex::new(r"^[a-zA-Z][a-zA-Z0-9+.-]*:").expect("Invalid URI regex pattern"));
17
18static METHOD_NAME_REGEX: LazyLock<Regex> =
24 LazyLock::new(|| Regex::new(r"^[^\s\x00-\x1F]+$").expect("Invalid method name regex pattern"));
25
26#[derive(Debug, Clone)]
28pub struct ProtocolValidator {
29 rules: ValidationRules,
31 strict_mode: bool,
33}
34
35#[derive(Debug, Clone)]
37pub struct ValidationRules {
38 pub max_message_size: usize,
40 pub max_batch_size: usize,
42 pub max_string_length: usize,
44 pub max_array_length: usize,
46 pub max_object_depth: usize,
48 pub required_fields: HashMap<String, HashSet<String>>,
50}
51
52impl ValidationRules {
53 #[inline]
55 pub fn uri_regex(&self) -> &Regex {
56 &URI_REGEX
57 }
58
59 #[inline]
61 pub fn method_name_regex(&self) -> &Regex {
62 &METHOD_NAME_REGEX
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum ValidationResult {
69 Valid,
71 ValidWithWarnings(Vec<ValidationWarning>),
73 Invalid(Vec<ValidationError>),
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct ValidationWarning {
80 pub code: String,
82 pub message: String,
84 pub field_path: Option<String>,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct ValidationError {
91 pub code: String,
93 pub message: String,
95 pub field_path: Option<String>,
97}
98
99#[derive(Debug, Clone)]
101struct ValidationContext {
102 path: Vec<String>,
104 depth: usize,
106 warnings: Vec<ValidationWarning>,
108 errors: Vec<ValidationError>,
110}
111
112impl Default for ValidationRules {
113 fn default() -> Self {
114 let mut required_fields = HashMap::new();
115
116 required_fields.insert(
118 "request".to_string(),
119 ["jsonrpc", "method", "id"]
120 .iter()
121 .map(|s| s.to_string())
122 .collect(),
123 );
124 required_fields.insert(
125 "response".to_string(),
126 ["jsonrpc", "id"].iter().map(|s| s.to_string()).collect(),
127 );
128 required_fields.insert(
129 "notification".to_string(),
130 ["jsonrpc", "method"]
131 .iter()
132 .map(|s| s.to_string())
133 .collect(),
134 );
135
136 required_fields.insert(
138 "initialize".to_string(),
139 ["protocolVersion", "capabilities", "clientInfo"]
140 .iter()
141 .map(|s| s.to_string())
142 .collect(),
143 );
144 required_fields.insert(
145 "tool".to_string(),
146 ["name", "inputSchema"]
147 .iter()
148 .map(|s| s.to_string())
149 .collect(),
150 );
151 required_fields.insert(
152 "prompt".to_string(),
153 ["name"].iter().map(|s| s.to_string()).collect(),
154 );
155 required_fields.insert(
156 "resource".to_string(),
157 ["uri", "name"].iter().map(|s| s.to_string()).collect(),
158 );
159
160 Self {
161 max_message_size: 10 * 1024 * 1024, max_batch_size: 100,
163 max_string_length: 1024 * 1024, max_array_length: 10000,
165 max_object_depth: 32,
166 required_fields,
167 }
168 }
169}
170
171impl ProtocolValidator {
172 pub fn new() -> Self {
174 Self {
175 rules: ValidationRules::default(),
176 strict_mode: false,
177 }
178 }
179
180 pub fn with_strict_mode(mut self) -> Self {
182 self.strict_mode = true;
183 self
184 }
185
186 pub fn with_rules(mut self, rules: ValidationRules) -> Self {
188 self.rules = rules;
189 self
190 }
191
192 pub fn validate_request(&self, request: &JsonRpcRequest) -> ValidationResult {
194 let mut ctx = ValidationContext::new();
195
196 self.validate_jsonrpc_request(request, &mut ctx);
198
199 if let Some(params) = &request.params {
201 self.validate_method_params(&request.method, params, &mut ctx);
202 }
203
204 ctx.into_result()
205 }
206
207 pub fn validate_response(&self, response: &JsonRpcResponse) -> ValidationResult {
209 let mut ctx = ValidationContext::new();
210
211 self.validate_jsonrpc_response(response, &mut ctx);
213
214 match (response.result().is_some(), response.error().is_some()) {
218 (true, true) => {
219 ctx.add_error(
220 "RESPONSE_BOTH_RESULT_AND_ERROR",
221 "Response cannot have both result and error".to_string(),
222 None,
223 );
224 }
225 (false, false) => {
226 ctx.add_error(
227 "RESPONSE_MISSING_RESULT_OR_ERROR",
228 "Response must have either result or error".to_string(),
229 None,
230 );
231 }
232 _ => {} }
234
235 ctx.into_result()
236 }
237
238 pub fn validate_notification(&self, notification: &JsonRpcNotification) -> ValidationResult {
240 let mut ctx = ValidationContext::new();
241
242 self.validate_jsonrpc_notification(notification, &mut ctx);
244
245 self.validate_method_name(¬ification.method, &mut ctx);
247
248 if let Some(params) = ¬ification.params {
250 self.validate_method_params(¬ification.method, params, &mut ctx);
251 }
252
253 ctx.into_result()
254 }
255
256 pub fn validate_tool(&self, tool: &Tool) -> ValidationResult {
258 let mut ctx = ValidationContext::new();
259
260 if tool.name.is_empty() {
262 ctx.add_error(
263 "TOOL_EMPTY_NAME",
264 "Tool name cannot be empty".to_string(),
265 Some("name".to_string()),
266 );
267 }
268
269 if tool.name.len() > self.rules.max_string_length {
270 ctx.add_error(
271 "TOOL_NAME_TOO_LONG",
272 format!(
273 "Tool name exceeds maximum length of {}",
274 self.rules.max_string_length
275 ),
276 Some("name".to_string()),
277 );
278 }
279
280 self.validate_tool_input(&tool.input_schema, &mut ctx);
282
283 ctx.into_result()
284 }
285
286 pub fn validate_prompt(&self, prompt: &Prompt) -> ValidationResult {
288 let mut ctx = ValidationContext::new();
289
290 if prompt.name.is_empty() {
292 ctx.add_error(
293 "PROMPT_EMPTY_NAME",
294 "Prompt name cannot be empty".to_string(),
295 Some("name".to_string()),
296 );
297 }
298
299 if let Some(arguments) = &prompt.arguments
301 && arguments.len() > self.rules.max_array_length
302 {
303 ctx.add_error(
304 "PROMPT_TOO_MANY_ARGS",
305 format!(
306 "Prompt has too many arguments (max: {})",
307 self.rules.max_array_length
308 ),
309 Some("arguments".to_string()),
310 );
311 }
312
313 ctx.into_result()
314 }
315
316 pub fn validate_resource(&self, resource: &Resource) -> ValidationResult {
318 let mut ctx = ValidationContext::new();
319
320 if resource.uri.len() > self.rules.max_string_length {
322 ctx.add_error(
323 "RESOURCE_URI_TOO_LONG",
324 format!(
325 "Resource URI exceeds maximum length of {}",
326 self.rules.max_string_length
327 ),
328 Some("uri".to_string()),
329 );
330 }
331
332 if !self.rules.uri_regex().is_match(&resource.uri) {
334 ctx.add_error(
335 "RESOURCE_INVALID_URI",
336 format!("Invalid URI format: {}", resource.uri),
337 Some("uri".to_string()),
338 );
339 }
340
341 if resource.name.is_empty() {
343 ctx.add_error(
344 "RESOURCE_EMPTY_NAME",
345 "Resource name cannot be empty".to_string(),
346 Some("name".to_string()),
347 );
348 }
349
350 ctx.into_result()
351 }
352
353 pub fn validate_initialize_request(&self, request: &InitializeRequest) -> ValidationResult {
355 let mut ctx = ValidationContext::new();
356
357 if !crate::SUPPORTED_VERSIONS.contains(&request.protocol_version.as_str()) {
359 ctx.add_warning(
360 "UNSUPPORTED_PROTOCOL_VERSION",
361 format!(
362 "Protocol version {} is not officially supported",
363 request.protocol_version
364 ),
365 Some("protocolVersion".to_string()),
366 );
367 }
368
369 if request.client_info.name.is_empty() {
371 ctx.add_error(
372 "EMPTY_CLIENT_NAME",
373 "Client name cannot be empty".to_string(),
374 Some("clientInfo.name".to_string()),
375 );
376 }
377
378 if request.client_info.version.is_empty() {
379 ctx.add_error(
380 "EMPTY_CLIENT_VERSION",
381 "Client version cannot be empty".to_string(),
382 Some("clientInfo.version".to_string()),
383 );
384 }
385
386 ctx.into_result()
387 }
388
389 pub fn validate_model_preferences(
393 &self,
394 prefs: &crate::types::ModelPreferences,
395 ) -> ValidationResult {
396 let mut ctx = ValidationContext::new();
397
398 let priorities = [
400 ("costPriority", prefs.cost_priority),
401 ("speedPriority", prefs.speed_priority),
402 ("intelligencePriority", prefs.intelligence_priority),
403 ];
404
405 for (name, value) in priorities {
406 if let Some(v) = value
407 && !(0.0..=1.0).contains(&v)
408 {
409 ctx.add_error(
410 "PRIORITY_OUT_OF_RANGE",
411 format!(
412 "{} must be between 0.0 and 1.0 (inclusive), got {}",
413 name, v
414 ),
415 Some(name.to_string()),
416 );
417 }
418 }
419
420 ctx.into_result()
421 }
422
423 pub fn validate_elicit_result(&self, result: &crate::types::ElicitResult) -> ValidationResult {
427 let mut ctx = ValidationContext::new();
428
429 use crate::types::ElicitationAction;
430
431 match result.action {
432 ElicitationAction::Accept => {
433 if result.content.is_none() {
434 ctx.add_error(
435 "MISSING_CONTENT_ON_ACCEPT",
436 "ElicitResult must have content when action is 'accept'".to_string(),
437 Some("content".to_string()),
438 );
439 }
440 }
441 ElicitationAction::Decline | ElicitationAction::Cancel => {
442 if result.content.is_some() {
443 ctx.add_warning(
444 "UNEXPECTED_CONTENT",
445 format!(
446 "Content should not be present when action is '{:?}'",
447 result.action
448 ),
449 Some("content".to_string()),
450 );
451 }
452 }
453 }
454
455 ctx.into_result()
456 }
457
458 pub fn validate_elicitation_schema(
462 &self,
463 schema: &crate::types::ElicitationSchema,
464 ) -> ValidationResult {
465 let mut ctx = ValidationContext::new();
466
467 if schema.schema_type != "object" {
469 ctx.add_error(
470 "SCHEMA_NOT_OBJECT",
471 format!(
472 "Elicitation schema type must be 'object', got '{}'",
473 schema.schema_type
474 ),
475 Some("type".to_string()),
476 );
477 }
478
479 if let Some(additional) = schema.additional_properties
481 && additional
482 {
483 ctx.add_warning(
484 "ADDITIONAL_PROPERTIES_NOT_RECOMMENDED",
485 "Elicitation schemas should have additionalProperties=false for flat structure"
486 .to_string(),
487 Some("additionalProperties".to_string()),
488 );
489 }
490
491 for (key, prop) in &schema.properties {
493 self.validate_primitive_schema(prop, &format!("properties.{}", key), &mut ctx);
494 }
495
496 ctx.into_result()
497 }
498
499 fn validate_primitive_schema(
501 &self,
502 schema: &crate::types::PrimitiveSchemaDefinition,
503 field_path: &str,
504 ctx: &mut ValidationContext,
505 ) {
506 use crate::types::PrimitiveSchemaDefinition;
507
508 match schema {
509 PrimitiveSchemaDefinition::String {
510 enum_values,
511 enum_names,
512 format,
513 ..
514 } => {
515 if let (Some(values), Some(names)) = (enum_values, enum_names)
517 && values.len() != names.len()
518 {
519 ctx.add_error(
520 "ENUM_NAMES_LENGTH_MISMATCH",
521 format!(
522 "enum and enumNames arrays must have equal length: {} vs {}",
523 values.len(),
524 names.len()
525 ),
526 Some(format!("{}.enumNames", field_path)),
527 );
528 }
529
530 if let Some(fmt) = format {
532 let valid_formats = ["email", "uri", "date", "date-time"];
533 if !valid_formats.contains(&fmt.as_str()) {
534 ctx.add_warning(
535 "UNKNOWN_STRING_FORMAT",
536 format!(
537 "Unknown format '{}', expected one of: {:?}",
538 fmt, valid_formats
539 ),
540 Some(format!("{}.format", field_path)),
541 );
542 }
543 }
544 }
545 PrimitiveSchemaDefinition::Number { .. }
546 | PrimitiveSchemaDefinition::Integer { .. } => {
547 }
549 PrimitiveSchemaDefinition::Boolean { .. } => {
550 }
552 }
553 }
554
555 pub fn validate_string_format(value: &str, format: &str) -> std::result::Result<(), String> {
559 match format {
560 "email" => {
561 let trimmed = value.trim();
565 if trimmed.is_empty() {
566 return Err(format!("Invalid email format: {value}"));
567 }
568 let Some((local, domain)) = trimmed.rsplit_once('@') else {
569 return Err(format!("Invalid email format: {value}"));
570 };
571 if local.is_empty() || domain.is_empty() {
572 return Err(format!("Invalid email format: {value}"));
573 }
574 let labels: Vec<&str> = domain.split('.').collect();
577 if labels.len() < 2 || labels.iter().any(|l| l.is_empty()) {
578 return Err(format!("Invalid email format: {value}"));
579 }
580 }
581 "uri" if url::Url::parse(value).is_err() => {
585 return Err(format!("Invalid URI format: {value}"));
586 }
587 "date" => {
588 chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d")
591 .map_err(|e| format!("Date must be in ISO 8601 format (YYYY-MM-DD): {e}"))?;
592 }
593 "date-time" => {
594 if !value.contains('T') {
596 return Err("DateTime must contain 'T' separator (ISO 8601 format)".to_string());
597 }
598 let parts: Vec<&str> = value.split('T').collect();
599 if parts.len() != 2 {
600 return Err("DateTime must be in ISO 8601 format".to_string());
601 }
602 Self::validate_string_format(parts[0], "date")?;
604 if !parts[1].contains(':') {
606 return Err("Time component must contain ':'".to_string());
607 }
608 }
609 _ => {
610 }
612 }
613 Ok(())
614 }
615
616 fn validate_jsonrpc_request(&self, request: &JsonRpcRequest, ctx: &mut ValidationContext) {
619 if request.method.is_empty() {
624 ctx.add_error(
625 "EMPTY_METHOD_NAME",
626 "Method name cannot be empty".to_string(),
627 Some("method".to_string()),
628 );
629 } else if request.method.len() > self.rules.max_string_length {
630 ctx.add_error(
631 "METHOD_NAME_TOO_LONG",
632 format!(
633 "Method name exceeds maximum length of {}",
634 self.rules.max_string_length
635 ),
636 Some("method".to_string()),
637 );
638 } else if request.method.starts_with("rpc.") {
639 ctx.add_error(
640 "RESERVED_METHOD_NAME",
641 format!(
642 "Method name '{}' uses reserved 'rpc.' prefix",
643 request.method
644 ),
645 Some("method".to_string()),
646 );
647 } else if !utils::is_valid_method_name(&request.method) {
648 ctx.add_error(
649 "INVALID_METHOD_NAME",
650 format!("Invalid method name format: '{}'", request.method),
651 Some("method".to_string()),
652 );
653 }
654
655 self.validate_request_id(&request.id, ctx);
658 }
659
660 fn validate_jsonrpc_response(&self, response: &JsonRpcResponse, ctx: &mut ValidationContext) {
661 self.validate_response_id(&response.id, ctx);
669
670 if let Some(error) = response.error() {
672 self.validate_jsonrpc_error(error, ctx);
673 }
674
675 if let Some(result) = response.result() {
677 self.validate_result_value(result, ctx);
678 }
679 }
680
681 fn validate_jsonrpc_notification(
682 &self,
683 notification: &JsonRpcNotification,
684 ctx: &mut ValidationContext,
685 ) {
686 if notification.method.is_empty() {
691 ctx.add_error(
692 "EMPTY_METHOD_NAME",
693 "Method name cannot be empty".to_string(),
694 Some("method".to_string()),
695 );
696 } else if notification.method.len() > self.rules.max_string_length {
697 ctx.add_error(
698 "METHOD_NAME_TOO_LONG",
699 format!(
700 "Method name exceeds maximum length of {}",
701 self.rules.max_string_length
702 ),
703 Some("method".to_string()),
704 );
705 } else if notification.method.starts_with("rpc.") {
706 ctx.add_error(
707 "RESERVED_METHOD_NAME",
708 format!(
709 "Method name '{}' uses reserved 'rpc.' prefix",
710 notification.method
711 ),
712 Some("method".to_string()),
713 );
714 } else if !utils::is_valid_method_name(¬ification.method) {
715 ctx.add_error(
716 "INVALID_METHOD_NAME",
717 format!("Invalid method name format: '{}'", notification.method),
718 Some("method".to_string()),
719 );
720 }
721
722 }
724
725 fn validate_jsonrpc_error(
726 &self,
727 error: &crate::jsonrpc::JsonRpcError,
728 ctx: &mut ValidationContext,
729 ) {
730 if error.code >= 0 {
732 ctx.add_warning(
733 "POSITIVE_ERROR_CODE",
734 "Error codes should be negative according to JSON-RPC spec".to_string(),
735 Some("error.code".to_string()),
736 );
737 }
738
739 if error.message.is_empty() {
740 ctx.add_error(
741 "EMPTY_ERROR_MESSAGE",
742 "Error message cannot be empty".to_string(),
743 Some("error.message".to_string()),
744 );
745 }
746 }
747
748 fn validate_method_name(&self, method: &str, ctx: &mut ValidationContext) {
749 if method.is_empty() {
750 ctx.add_error(
751 "EMPTY_METHOD_NAME",
752 "Method name cannot be empty".to_string(),
753 Some("method".to_string()),
754 );
755 return;
756 }
757
758 if method.starts_with("rpc.") {
761 ctx.add_error(
762 "RESERVED_METHOD_NAME",
763 format!("Method name '{method}' uses reserved 'rpc.' prefix"),
764 Some("method".to_string()),
765 );
766 return;
767 }
768
769 if !self.rules.method_name_regex().is_match(method) {
770 ctx.add_error(
771 "INVALID_METHOD_NAME",
772 format!("Invalid method name format: {method}"),
773 Some("method".to_string()),
774 );
775 }
776 }
777
778 fn validate_method_params(&self, method: &str, params: &Value, ctx: &mut ValidationContext) {
779 ctx.push_path("params".to_string());
780 self.validate_parameters(params, ctx);
781
782 if method == "tools/list"
784 && !params.is_null()
785 && !params.as_object().is_some_and(|obj| obj.is_empty())
786 {
787 ctx.add_warning(
788 "UNEXPECTED_PARAMS",
789 "tools/list should not have parameters".to_string(),
790 None,
791 );
792 }
793
794 ctx.pop_path();
795 }
796
797 fn validate_tool_input(&self, input: &ToolInputSchema, ctx: &mut ValidationContext) {
798 ctx.push_path("inputSchema".to_string());
799
800 if let Some(schema_type) = input.schema_type.as_ref()
802 && !schema_declares_type(schema_type, "object")
803 {
804 ctx.add_warning(
805 "NON_OBJECT_SCHEMA",
806 format!(
807 "Tool input schema should typically be 'object', got {}",
808 describe_schema_type(schema_type)
809 ),
810 Some("type".to_string()),
811 );
812 }
813
814 ctx.pop_path();
815 }
816
817 fn validate_value_structure(
818 &self,
819 value: &Value,
820 _expected_type: &str,
821 ctx: &mut ValidationContext,
822 ) {
823 if ctx.depth > self.rules.max_object_depth {
825 ctx.add_error(
826 "MAX_DEPTH_EXCEEDED",
827 format!(
828 "Maximum object depth ({}) exceeded",
829 self.rules.max_object_depth
830 ),
831 None,
832 );
833 return;
834 }
835
836 match value {
837 Value::Object(obj) => {
838 ctx.depth += 1;
839 for (key, val) in obj {
840 ctx.push_path(key.clone());
841 self.validate_value_structure(val, "unknown", ctx);
842 ctx.pop_path();
843 }
844 ctx.depth -= 1;
845 }
846 Value::Array(arr) => {
847 if arr.len() > self.rules.max_array_length {
848 ctx.add_error(
849 "ARRAY_TOO_LONG",
850 format!(
851 "Array exceeds maximum length of {}",
852 self.rules.max_array_length
853 ),
854 None,
855 );
856 }
857
858 for (index, val) in arr.iter().enumerate() {
859 ctx.push_path(index.to_string());
860 self.validate_value_structure(val, "unknown", ctx);
861 ctx.pop_path();
862 }
863 }
864 Value::String(s) if s.len() > self.rules.max_string_length => {
865 ctx.add_error(
866 "STRING_TOO_LONG",
867 format!(
868 "String exceeds maximum length of {}",
869 self.rules.max_string_length
870 ),
871 None,
872 );
873 }
874 _ => {} }
876 }
877
878 fn validate_parameters(&self, params: &Value, ctx: &mut ValidationContext) {
879 self.validate_value_structure(params, "params", ctx);
881
882 match params {
884 Value::Array(arr) if arr.len() > self.rules.max_array_length => {
886 ctx.add_error(
887 "PARAMS_ARRAY_TOO_LONG",
888 format!(
889 "Parameter array exceeds maximum length of {}",
890 self.rules.max_array_length
891 ),
892 Some("params".to_string()),
893 );
894 }
895 _ => {
896 }
898 }
899 }
900
901 fn validate_request_id(&self, _id: &crate::types::RequestId, _ctx: &mut ValidationContext) {
902 }
906
907 fn validate_response_id(&self, id: &crate::jsonrpc::ResponseId, _ctx: &mut ValidationContext) {
908 if id.is_null() {
910 }
913 }
915
916 fn validate_result_value(&self, result: &Value, ctx: &mut ValidationContext) {
917 self.validate_value_structure(result, "result", ctx);
919
920 }
923}
924
925impl Default for ProtocolValidator {
926 fn default() -> Self {
927 Self::new()
928 }
929}
930
931fn schema_declares_type(schema_type: &Value, expected: &str) -> bool {
932 match schema_type {
933 Value::String(value) => value == expected,
934 Value::Array(values) => values.iter().any(|value| value.as_str() == Some(expected)),
935 _ => false,
936 }
937}
938
939fn describe_schema_type(schema_type: &Value) -> String {
940 match schema_type {
941 Value::String(value) => format!("'{value}'"),
942 other => other.to_string(),
943 }
944}
945
946impl ValidationContext {
947 fn new() -> Self {
948 Self {
949 path: Vec::new(),
950 depth: 0,
951 warnings: Vec::new(),
952 errors: Vec::new(),
953 }
954 }
955
956 fn push_path(&mut self, segment: String) {
957 self.path.push(segment);
958 }
959
960 fn pop_path(&mut self) {
961 self.path.pop();
962 }
963
964 fn current_path(&self) -> Option<String> {
965 if self.path.is_empty() {
966 None
967 } else {
968 Some(self.path.join("."))
969 }
970 }
971
972 fn add_error(&mut self, code: &str, message: String, field_path: Option<String>) {
973 let path = field_path.or_else(|| self.current_path());
974 self.errors.push(ValidationError {
975 code: code.to_string(),
976 message,
977 field_path: path,
978 });
979 }
980
981 fn add_warning(&mut self, code: &str, message: String, field_path: Option<String>) {
982 let path = field_path.or_else(|| self.current_path());
983 self.warnings.push(ValidationWarning {
984 code: code.to_string(),
985 message,
986 field_path: path,
987 });
988 }
989
990 fn into_result(self) -> ValidationResult {
991 if !self.errors.is_empty() {
992 ValidationResult::Invalid(self.errors)
993 } else if !self.warnings.is_empty() {
994 ValidationResult::ValidWithWarnings(self.warnings)
995 } else {
996 ValidationResult::Valid
997 }
998 }
999}
1000
1001impl ValidationResult {
1002 pub fn is_valid(&self) -> bool {
1004 !matches!(self, ValidationResult::Invalid(_))
1005 }
1006
1007 pub fn is_invalid(&self) -> bool {
1009 matches!(self, ValidationResult::Invalid(_))
1010 }
1011
1012 pub fn has_warnings(&self) -> bool {
1014 matches!(self, ValidationResult::ValidWithWarnings(_))
1015 }
1016
1017 pub fn warnings(&self) -> &[ValidationWarning] {
1019 match self {
1020 ValidationResult::ValidWithWarnings(warnings) => warnings,
1021 _ => &[],
1022 }
1023 }
1024
1025 pub fn errors(&self) -> &[ValidationError] {
1027 match self {
1028 ValidationResult::Invalid(errors) => errors,
1029 _ => &[],
1030 }
1031 }
1032}
1033
1034pub mod utils {
1036 use super::*;
1037
1038 pub fn error(code: &str, message: &str) -> ValidationError {
1040 ValidationError {
1041 code: code.to_string(),
1042 message: message.to_string(),
1043 field_path: None,
1044 }
1045 }
1046
1047 pub fn warning(code: &str, message: &str) -> ValidationWarning {
1049 ValidationWarning {
1050 code: code.to_string(),
1051 message: message.to_string(),
1052 field_path: None,
1053 }
1054 }
1055
1056 pub fn is_valid_uri(uri: &str) -> bool {
1058 ValidationRules::default().uri_regex().is_match(uri)
1059 }
1060
1061 pub fn is_valid_method_name(method: &str) -> bool {
1063 !method.is_empty()
1064 && !method.starts_with("rpc.")
1065 && ValidationRules::default()
1066 .method_name_regex()
1067 .is_match(method)
1068 }
1069}
1070
1071#[cfg(test)]
1077mod tests;