pulseengine_mcp_protocol/
validation.rs

1//! Validation utilities for MCP protocol types
2
3use crate::{Error, Result};
4use serde_json::Value;
5use std::collections::HashMap;
6use uuid::Uuid;
7use validator::Validate;
8
9/// Protocol validation utilities
10pub struct Validator;
11
12impl Validator {
13    /// Validate a UUID string
14    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    /// Validate that a string is not empty
21    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    /// Validate a tool name (must be alphanumeric with underscores)
32    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    /// Validate a resource URI
48    pub fn validate_resource_uri(uri: &str) -> Result<()> {
49        Self::validate_non_empty(uri, "Resource URI")?;
50
51        // Basic URI validation - must not contain control characters
52        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    /// Validate JSON schema
62    pub fn validate_json_schema(schema: &Value) -> Result<()> {
63        // Basic validation - ensure it's an object with a "type" field
64        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    /// Validate tool arguments against a schema
78    pub fn validate_tool_arguments(args: &HashMap<String, Value>, schema: &Value) -> Result<()> {
79        // Basic validation - check required properties if defined
80        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    /// Validate pagination parameters
100    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    /// Validate prompt name
118    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    /// Validate a struct using the validator crate
134    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}