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    ///
15    /// # Errors
16    ///
17    /// Returns an error if the string is not a valid UUID format
18    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    /// Validate that a string is not empty
25    ///
26    /// # Errors
27    ///
28    /// Returns an error if the string is empty or contains only whitespace
29    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    /// Validate a tool name (must be alphanumeric with underscores)
40    ///
41    /// # Errors
42    ///
43    /// Returns an error if the name is empty or contains invalid characters
44    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    /// Validate a resource URI
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if the URI is empty or contains control characters
64    pub fn validate_resource_uri(uri: &str) -> Result<()> {
65        Self::validate_non_empty(uri, "Resource URI")?;
66
67        // Basic URI validation - must not contain control characters
68        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    /// Validate JSON schema
78    ///
79    /// # Errors
80    ///
81    /// Returns an error if the schema is not a valid JSON object with a type field
82    pub fn validate_json_schema(schema: &Value) -> Result<()> {
83        // Basic validation - ensure it's an object with a "type" field
84        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    /// Validate tool arguments against a schema
98    ///
99    /// # Errors
100    ///
101    /// Returns an error if required arguments are missing from the provided arguments
102    pub fn validate_tool_arguments(args: &HashMap<String, Value>, schema: &Value) -> Result<()> {
103        // Basic validation - check required properties if defined
104        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    /// Validate pagination parameters
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if cursor is empty, limit is 0, or limit exceeds 1000
128    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    /// Validate prompt name
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if the name is empty or contains invalid characters
150    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    /// Validate a struct using the validator crate
166    ///
167    /// # Errors
168    ///
169    /// Returns an error if the struct fails validation according to its validation rules
170    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}