pulseengine_mcp_protocol/
validation.rs1use crate::{Error, Result};
4use serde_json::Value;
5use std::collections::HashMap;
6use uuid::Uuid;
7use validator::Validate;
8
9pub struct Validator;
11
12impl Validator {
13 pub fn validate_uuid(uuid_str: &str) -> Result<Uuid> {
15 uuid_str
16 .parse::<Uuid>()
17 .map_err(|e| Error::validation_error(format!("Invalid UUID: {e}")))
18 }
19
20 pub fn validate_non_empty(value: &str, field_name: &str) -> Result<()> {
22 if value.trim().is_empty() {
23 Err(Error::validation_error(format!(
24 "{field_name} cannot be empty"
25 )))
26 } else {
27 Ok(())
28 }
29 }
30
31 pub fn validate_tool_name(name: &str) -> Result<()> {
33 Self::validate_non_empty(name, "Tool name")?;
34
35 if !name
36 .chars()
37 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
38 {
39 return Err(Error::validation_error(
40 "Tool name must contain only alphanumeric characters, underscores, and hyphens",
41 ));
42 }
43
44 Ok(())
45 }
46
47 pub fn validate_resource_uri(uri: &str) -> Result<()> {
49 Self::validate_non_empty(uri, "Resource URI")?;
50
51 if uri.chars().any(|c| c.is_control()) {
53 return Err(Error::validation_error(
54 "Resource URI cannot contain control characters",
55 ));
56 }
57
58 Ok(())
59 }
60
61 pub fn validate_json_schema(schema: &Value) -> Result<()> {
63 if let Some(obj) = schema.as_object() {
65 if !obj.contains_key("type") {
66 return Err(Error::validation_error(
67 "JSON schema must have a 'type' field",
68 ));
69 }
70 } else {
71 return Err(Error::validation_error("JSON schema must be an object"));
72 }
73
74 Ok(())
75 }
76
77 pub fn validate_tool_arguments(args: &HashMap<String, Value>, schema: &Value) -> Result<()> {
79 if let Some(schema_obj) = schema.as_object() {
81 if let Some(_properties) = schema_obj.get("properties").and_then(|p| p.as_object()) {
82 if let Some(required) = schema_obj.get("required").and_then(|r| r.as_array()) {
83 for req_field in required {
84 if let Some(field_name) = req_field.as_str() {
85 if !args.contains_key(field_name) {
86 return Err(Error::validation_error(format!(
87 "Required argument '{field_name}' is missing"
88 )));
89 }
90 }
91 }
92 }
93 }
94 }
95
96 Ok(())
97 }
98
99 pub fn validate_pagination(cursor: Option<&str>, limit: Option<u32>) -> Result<()> {
101 if let Some(cursor_val) = cursor {
102 Self::validate_non_empty(cursor_val, "Cursor")?;
103 }
104
105 if let Some(limit_val) = limit {
106 if limit_val == 0 {
107 return Err(Error::validation_error("Limit must be greater than 0"));
108 }
109 if limit_val > 1000 {
110 return Err(Error::validation_error("Limit cannot exceed 1000"));
111 }
112 }
113
114 Ok(())
115 }
116
117 pub fn validate_prompt_name(name: &str) -> Result<()> {
119 Self::validate_non_empty(name, "Prompt name")?;
120
121 if !name
122 .chars()
123 .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
124 {
125 return Err(Error::validation_error(
126 "Prompt name must contain only alphanumeric characters, underscores, hyphens, and dots"
127 ));
128 }
129
130 Ok(())
131 }
132
133 pub fn validate_struct<T: Validate>(item: &T) -> Result<()> {
135 item.validate()
136 .map_err(|e| Error::validation_error(e.to_string()))
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use serde_json::json;
144
145 #[test]
146 fn test_validate_uuid() {
147 let valid_uuid = "550e8400-e29b-41d4-a716-446655440000";
148 assert!(Validator::validate_uuid(valid_uuid).is_ok());
149
150 let invalid_uuid = "not-a-uuid";
151 assert!(Validator::validate_uuid(invalid_uuid).is_err());
152 }
153
154 #[test]
155 fn test_validate_non_empty() {
156 assert!(Validator::validate_non_empty("valid", "field").is_ok());
157 assert!(Validator::validate_non_empty("", "field").is_err());
158 assert!(Validator::validate_non_empty(" ", "field").is_err());
159 }
160
161 #[test]
162 fn test_validate_tool_name() {
163 assert!(Validator::validate_tool_name("valid_tool").is_ok());
164 assert!(Validator::validate_tool_name("tool-name").is_ok());
165 assert!(Validator::validate_tool_name("tool123").is_ok());
166 assert!(Validator::validate_tool_name("").is_err());
167 assert!(Validator::validate_tool_name("invalid tool").is_err());
168 assert!(Validator::validate_tool_name("tool@name").is_err());
169 }
170
171 #[test]
172 fn test_validate_json_schema() {
173 let valid_schema = json!({"type": "object"});
174 assert!(Validator::validate_json_schema(&valid_schema).is_ok());
175
176 let invalid_schema = json!("not an object");
177 assert!(Validator::validate_json_schema(&invalid_schema).is_err());
178
179 let no_type_schema = json!({"properties": {}});
180 assert!(Validator::validate_json_schema(&no_type_schema).is_err());
181 }
182
183 #[test]
184 fn test_validate_pagination() {
185 assert!(Validator::validate_pagination(None, None).is_ok());
186 assert!(Validator::validate_pagination(Some("cursor"), Some(10)).is_ok());
187 assert!(Validator::validate_pagination(Some(""), None).is_err());
188 assert!(Validator::validate_pagination(None, Some(0)).is_err());
189 assert!(Validator::validate_pagination(None, Some(1001)).is_err());
190 }
191}