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 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 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 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 pub fn validate_initialize_request(&self, request: &InitializeRequest) -> ValidationResult {
352 let mut ctx = ValidationContext::new();
353
354 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 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 pub fn validate_model_preferences(
390 &self,
391 prefs: &crate::types::ModelPreferences,
392 ) -> ValidationResult {
393 let mut ctx = ValidationContext::new();
394
395 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 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 pub fn validate_elicitation_schema(
459 &self,
460 schema: &crate::types::ElicitationSchema,
461 ) -> ValidationResult {
462 let mut ctx = ValidationContext::new();
463
464 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 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 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 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 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 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 }
546 PrimitiveSchemaDefinition::Boolean { .. } => {
547 }
549 }
550 }
551
552 pub fn validate_string_format(value: &str, format: &str) -> std::result::Result<(), String> {
556 match format {
557 "email" => {
558 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 if !value.contains("://") && !value.starts_with('/') {
570 return Err(format!("Invalid URI format: {}", value));
571 }
572 }
573 "date" => {
574 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 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 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 Self::validate_string_format(parts[0], "date")?;
600 if !parts[1].contains(':') {
602 return Err("Time component must contain ':'".to_string());
603 }
604 }
605 _ => {
606 }
608 }
609 Ok(())
610 }
611
612 fn validate_jsonrpc_request(&self, request: &JsonRpcRequest, ctx: &mut ValidationContext) {
615 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 if let Some(ref params) = request.params {
644 self.validate_parameters(params, ctx);
645 }
646
647 self.validate_request_id(&request.id, ctx);
650 }
651
652 fn validate_jsonrpc_response(&self, response: &JsonRpcResponse, ctx: &mut ValidationContext) {
653 self.validate_response_id(&response.id, ctx);
661
662 if let Some(error) = response.error() {
664 self.validate_jsonrpc_error(error, ctx);
665 }
666
667 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 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(¬ification.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 if let Some(ref params) = notification.params {
707 self.validate_parameters(params, ctx);
708 }
709
710 }
712
713 fn validate_jsonrpc_error(
714 &self,
715 error: &crate::jsonrpc::JsonRpcError,
716 ctx: &mut ValidationContext,
717 ) {
718 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 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 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 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 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 _ => {} }
856 }
857
858 fn validate_parameters(&self, params: &Value, ctx: &mut ValidationContext) {
859 self.validate_value_structure(params, "params", ctx);
861
862 match params {
864 Value::Array(arr) => {
865 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 }
880 }
881 }
882
883 fn validate_request_id(&self, _id: &crate::types::RequestId, _ctx: &mut ValidationContext) {
884 }
888
889 fn validate_response_id(&self, id: &crate::jsonrpc::ResponseId, _ctx: &mut ValidationContext) {
890 if id.is_null() {
892 }
895 }
897
898 fn validate_result_value(&self, result: &Value, ctx: &mut ValidationContext) {
899 self.validate_value_structure(result, "result", ctx);
901
902 }
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 pub fn is_valid(&self) -> bool {
971 !matches!(self, ValidationResult::Invalid(_))
972 }
973
974 pub fn is_invalid(&self) -> bool {
976 matches!(self, ValidationResult::Invalid(_))
977 }
978
979 pub fn has_warnings(&self) -> bool {
981 matches!(self, ValidationResult::ValidWithWarnings(_))
982 }
983
984 pub fn warnings(&self) -> &[ValidationWarning] {
986 match self {
987 ValidationResult::ValidWithWarnings(warnings) => warnings,
988 _ => &[],
989 }
990 }
991
992 pub fn errors(&self) -> &[ValidationError] {
994 match self {
995 ValidationResult::Invalid(errors) => errors,
996 _ => &[],
997 }
998 }
999}
1000
1001pub mod utils {
1003 use super::*;
1004
1005 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 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 pub fn is_valid_uri(uri: &str) -> bool {
1025 ValidationRules::default().uri_regex().is_match(uri)
1026 }
1027
1028 pub fn is_valid_method_name(method: &str) -> bool {
1030 ValidationRules::default()
1031 .method_name_regex()
1032 .is_match(method)
1033 }
1034}
1035
1036#[cfg(test)]
1042mod tests;