1use 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
14static URI_REGEX: Lazy<Regex> =
16 Lazy::new(|| Regex::new(r"^[a-zA-Z][a-zA-Z0-9+.-]*:").expect("Invalid URI regex pattern"));
17
18static 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#[derive(Debug, Clone)]
25pub struct ProtocolValidator {
26 rules: ValidationRules,
28 strict_mode: bool,
30}
31
32#[derive(Debug, Clone)]
34pub struct ValidationRules {
35 pub max_message_size: usize,
37 pub max_batch_size: usize,
39 pub max_string_length: usize,
41 pub max_array_length: usize,
43 pub max_object_depth: usize,
45 pub required_fields: HashMap<String, HashSet<String>>,
47}
48
49impl ValidationRules {
50 #[inline]
52 pub fn uri_regex(&self) -> &Regex {
53 &URI_REGEX
54 }
55
56 #[inline]
58 pub fn method_name_regex(&self) -> &Regex {
59 &METHOD_NAME_REGEX
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum ValidationResult {
66 Valid,
68 ValidWithWarnings(Vec<ValidationWarning>),
70 Invalid(Vec<ValidationError>),
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct ValidationWarning {
77 pub code: String,
79 pub message: String,
81 pub field_path: Option<String>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct ValidationError {
88 pub code: String,
90 pub message: String,
92 pub field_path: Option<String>,
94}
95
96#[derive(Debug, Clone)]
98struct ValidationContext {
99 path: Vec<String>,
101 depth: usize,
103 warnings: Vec<ValidationWarning>,
105 errors: Vec<ValidationError>,
107}
108
109impl Default for ValidationRules {
110 fn default() -> Self {
111 let mut required_fields = HashMap::new();
112
113 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 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, max_batch_size: 100,
160 max_string_length: 1024 * 1024, max_array_length: 10000,
162 max_object_depth: 32,
163 required_fields,
164 }
165 }
166}
167
168impl ProtocolValidator {
169 pub fn new() -> Self {
171 Self {
172 rules: ValidationRules::default(),
173 strict_mode: false,
174 }
175 }
176
177 pub fn with_strict_mode(mut self) -> Self {
179 self.strict_mode = true;
180 self
181 }
182
183 pub fn with_rules(mut self, rules: ValidationRules) -> Self {
185 self.rules = rules;
186 self
187 }
188
189 pub fn validate_request(&self, request: &JsonRpcRequest) -> ValidationResult {
191 let mut ctx = ValidationContext::new();
192
193 self.validate_jsonrpc_request(request, &mut ctx);
195
196 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 pub fn validate_response(&self, response: &JsonRpcResponse) -> ValidationResult {
206 let mut ctx = ValidationContext::new();
207
208 self.validate_jsonrpc_response(response, &mut ctx);
210
211 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 _ => {} }
231
232 ctx.into_result()
233 }
234
235 pub fn validate_notification(&self, notification: &JsonRpcNotification) -> ValidationResult {
237 let mut ctx = ValidationContext::new();
238
239 self.validate_jsonrpc_notification(notification, &mut ctx);
241
242 self.validate_method_name(¬ification.method, &mut ctx);
244
245 if let Some(params) = ¬ification.params {
247 self.validate_method_params(¬ification.method, params, &mut ctx);
248 }
249
250 ctx.into_result()
251 }
252
253 pub fn validate_tool(&self, tool: &Tool) -> ValidationResult {
255 let mut ctx = ValidationContext::new();
256
257 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 self.validate_tool_input(&tool.input_schema, &mut ctx);
279
280 ctx.into_result()
281 }
282
283 pub fn validate_prompt(&self, prompt: &Prompt) -> ValidationResult {
285 let mut ctx = ValidationContext::new();
286
287 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 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 pub fn validate_resource(&self, resource: &Resource) -> ValidationResult {
315 let mut ctx = ValidationContext::new();
316
317 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 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 pub fn validate_initialize_request(&self, request: &InitializeRequest) -> ValidationResult {
340 let mut ctx = ValidationContext::new();
341
342 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 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 pub fn validate_model_preferences(
378 &self,
379 prefs: &crate::types::ModelPreferences,
380 ) -> ValidationResult {
381 let mut ctx = ValidationContext::new();
382
383 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 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 pub fn validate_elicitation_schema(
447 &self,
448 schema: &crate::types::ElicitationSchema,
449 ) -> ValidationResult {
450 let mut ctx = ValidationContext::new();
451
452 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 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 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 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 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 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 }
534 PrimitiveSchemaDefinition::Boolean { .. } => {
535 }
537 }
538 }
539
540 pub fn validate_string_format(value: &str, format: &str) -> std::result::Result<(), String> {
544 match format {
545 "email" => {
546 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 if !value.contains("://") && !value.starts_with('/') {
558 return Err(format!("Invalid URI format: {}", value));
559 }
560 }
561 "date" => {
562 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 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 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 Self::validate_string_format(parts[0], "date")?;
588 if !parts[1].contains(':') {
590 return Err("Time component must contain ':'".to_string());
591 }
592 }
593 _ => {
594 }
596 }
597 Ok(())
598 }
599
600 fn validate_jsonrpc_request(&self, request: &JsonRpcRequest, ctx: &mut ValidationContext) {
603 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 if let Some(ref params) = request.params {
632 self.validate_parameters(params, ctx);
633 }
634
635 self.validate_request_id(&request.id, ctx);
638 }
639
640 fn validate_jsonrpc_response(&self, response: &JsonRpcResponse, ctx: &mut ValidationContext) {
641 self.validate_response_id(&response.id, ctx);
649
650 if let Some(error) = response.error() {
652 self.validate_jsonrpc_error(error, ctx);
653 }
654
655 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 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(¬ification.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 if let Some(ref params) = notification.params {
695 self.validate_parameters(params, ctx);
696 }
697
698 }
700
701 fn validate_jsonrpc_error(
702 &self,
703 error: &crate::jsonrpc::JsonRpcError,
704 ctx: &mut ValidationContext,
705 ) {
706 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 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 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 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 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 _ => {} }
844 }
845
846 fn validate_parameters(&self, params: &Value, ctx: &mut ValidationContext) {
847 self.validate_value_structure(params, "params", ctx);
849
850 match params {
852 Value::Array(arr) => {
853 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 }
868 }
869 }
870
871 fn validate_request_id(&self, _id: &crate::types::RequestId, _ctx: &mut ValidationContext) {
872 }
876
877 fn validate_response_id(&self, id: &crate::jsonrpc::ResponseId, _ctx: &mut ValidationContext) {
878 if id.is_null() {
880 }
883 }
885
886 fn validate_result_value(&self, result: &Value, ctx: &mut ValidationContext) {
887 self.validate_value_structure(result, "result", ctx);
889
890 }
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 pub fn is_valid(&self) -> bool {
959 !matches!(self, ValidationResult::Invalid(_))
960 }
961
962 pub fn is_invalid(&self) -> bool {
964 matches!(self, ValidationResult::Invalid(_))
965 }
966
967 pub fn has_warnings(&self) -> bool {
969 matches!(self, ValidationResult::ValidWithWarnings(_))
970 }
971
972 pub fn warnings(&self) -> &[ValidationWarning] {
974 match self {
975 ValidationResult::ValidWithWarnings(warnings) => warnings,
976 _ => &[],
977 }
978 }
979
980 pub fn errors(&self) -> &[ValidationError] {
982 match self {
983 ValidationResult::Invalid(errors) => errors,
984 _ => &[],
985 }
986 }
987}
988
989pub mod utils {
991 use super::*;
992
993 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 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 pub fn is_valid_uri(uri: &str) -> bool {
1013 ValidationRules::default().uri_regex().is_match(uri)
1014 }
1015
1016 pub fn is_valid_method_name(method: &str) -> bool {
1018 ValidationRules::default()
1019 .method_name_regex()
1020 .is_match(method)
1021 }
1022}
1023
1024#[cfg(test)]
1030mod tests;