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> {
19 uuid_str
20 .parse::<Uuid>()
21 .map_err(|e| Error::validation_error(format!("Invalid UUID: {e}")))
22 }
23
24 pub fn validate_non_empty(value: &str, field_name: &str) -> Result<()> {
30 if value.trim().is_empty() {
31 Err(Error::validation_error(format!(
32 "{field_name} cannot be empty"
33 )))
34 } else {
35 Ok(())
36 }
37 }
38
39 pub fn validate_tool_name(name: &str) -> Result<()> {
45 Self::validate_non_empty(name, "Tool name")?;
46
47 if !name
48 .chars()
49 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
50 {
51 return Err(Error::validation_error(
52 "Tool name must contain only alphanumeric characters, underscores, and hyphens",
53 ));
54 }
55
56 Ok(())
57 }
58
59 pub fn validate_resource_uri(uri: &str) -> Result<()> {
65 Self::validate_non_empty(uri, "Resource URI")?;
66
67 if uri.chars().any(char::is_control) {
69 return Err(Error::validation_error(
70 "Resource URI cannot contain control characters",
71 ));
72 }
73
74 Ok(())
75 }
76
77 pub fn validate_json_schema(schema: &Value) -> Result<()> {
83 if let Some(obj) = schema.as_object() {
85 if !obj.contains_key("type") {
86 return Err(Error::validation_error(
87 "JSON schema must have a 'type' field",
88 ));
89 }
90 } else {
91 return Err(Error::validation_error("JSON schema must be an object"));
92 }
93
94 Ok(())
95 }
96
97 pub fn validate_tool_arguments(args: &HashMap<String, Value>, schema: &Value) -> Result<()> {
103 if let Some(schema_obj) = schema.as_object() {
105 if let Some(_properties) = schema_obj.get("properties").and_then(|p| p.as_object()) {
106 if let Some(required) = schema_obj.get("required").and_then(|r| r.as_array()) {
107 for req_field in required {
108 if let Some(field_name) = req_field.as_str() {
109 if !args.contains_key(field_name) {
110 return Err(Error::validation_error(format!(
111 "Required argument '{field_name}' is missing"
112 )));
113 }
114 }
115 }
116 }
117 }
118 }
119
120 Ok(())
121 }
122
123 pub fn validate_pagination(cursor: Option<&str>, limit: Option<u32>) -> Result<()> {
129 if let Some(cursor_val) = cursor {
130 Self::validate_non_empty(cursor_val, "Cursor")?;
131 }
132
133 if let Some(limit_val) = limit {
134 if limit_val == 0 {
135 return Err(Error::validation_error("Limit must be greater than 0"));
136 }
137 if limit_val > 1000 {
138 return Err(Error::validation_error("Limit cannot exceed 1000"));
139 }
140 }
141
142 Ok(())
143 }
144
145 pub fn validate_prompt_name(name: &str) -> Result<()> {
151 Self::validate_non_empty(name, "Prompt name")?;
152
153 if !name
154 .chars()
155 .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
156 {
157 return Err(Error::validation_error(
158 "Prompt name must contain only alphanumeric characters, underscores, hyphens, and dots"
159 ));
160 }
161
162 Ok(())
163 }
164
165 pub fn validate_struct<T: Validate>(item: &T) -> Result<()> {
171 item.validate()
172 .map_err(|e| Error::validation_error(e.to_string()))
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use serde_json::json;
180
181 #[test]
182 fn test_validate_uuid() {
183 let valid_uuid = "550e8400-e29b-41d4-a716-446655440000";
184 assert!(Validator::validate_uuid(valid_uuid).is_ok());
185
186 let invalid_uuid = "not-a-uuid";
187 assert!(Validator::validate_uuid(invalid_uuid).is_err());
188 }
189
190 #[test]
191 fn test_validate_non_empty() {
192 assert!(Validator::validate_non_empty("valid", "field").is_ok());
193 assert!(Validator::validate_non_empty("", "field").is_err());
194 assert!(Validator::validate_non_empty(" ", "field").is_err());
195 }
196
197 #[test]
198 fn test_validate_tool_name() {
199 assert!(Validator::validate_tool_name("valid_tool").is_ok());
200 assert!(Validator::validate_tool_name("tool-name").is_ok());
201 assert!(Validator::validate_tool_name("tool123").is_ok());
202 assert!(Validator::validate_tool_name("").is_err());
203 assert!(Validator::validate_tool_name("invalid tool").is_err());
204 assert!(Validator::validate_tool_name("tool@name").is_err());
205 }
206
207 #[test]
208 fn test_validate_json_schema() {
209 let valid_schema = json!({"type": "object"});
210 assert!(Validator::validate_json_schema(&valid_schema).is_ok());
211
212 let invalid_schema = json!("not an object");
213 assert!(Validator::validate_json_schema(&invalid_schema).is_err());
214
215 let no_type_schema = json!({"properties": {}});
216 assert!(Validator::validate_json_schema(&no_type_schema).is_err());
217 }
218
219 #[test]
220 fn test_validate_pagination() {
221 assert!(Validator::validate_pagination(None, None).is_ok());
222 assert!(Validator::validate_pagination(Some("cursor"), Some(10)).is_ok());
223 assert!(Validator::validate_pagination(Some(""), None).is_err());
224 assert!(Validator::validate_pagination(None, Some(0)).is_err());
225 assert!(Validator::validate_pagination(None, Some(1001)).is_err());
226 }
227}