1use anyhow::Result;
7use serde_json::Value;
8use std::collections::HashMap;
9use thiserror::Error;
10
11#[derive(Error, Debug, Clone)]
13pub enum ValidationError {
14 #[error("Schema compilation failed: {0}")]
16 SchemaError(String),
17
18 #[error("Parameter '{field}' is required but missing")]
20 MissingRequired {
21 field: String,
23 },
24
25 #[error("Parameter '{field}' validation failed: {reason}")]
27 ValidationFailed {
28 field: String,
30 reason: String,
32 },
33
34 #[error("Value transformation failed for '{field}': {reason}")]
36 TransformationFailed {
37 field: String,
39 reason: String,
41 },
42
43 #[error("JSON Schema is invalid: {0}")]
45 InvalidSchema(String),
46}
47
48#[derive(Debug, Clone)]
50pub struct ValidationResult {
51 pub is_valid: bool,
53 pub errors: Vec<ValidationError>,
55 pub warnings: Vec<String>,
57 pub validated_params: Value,
59 pub transformations: Vec<String>,
61}
62
63pub struct ParameterValidator {
65 pub auto_transform: bool,
67 pub strict_mode: bool,
69}
70
71impl Default for ParameterValidator {
72 fn default() -> Self {
73 Self {
74 auto_transform: true,
75 strict_mode: false,
76 }
77 }
78}
79
80impl ParameterValidator {
81 pub fn new() -> Self {
83 Self::default()
84 }
85
86 pub fn strict() -> Self {
88 Self {
89 auto_transform: false,
90 strict_mode: true,
91 }
92 }
93
94 pub fn validate(&self, schema: &Value, params: &Value) -> ValidationResult {
96 let mut result = ValidationResult {
97 is_valid: true,
98 errors: Vec::new(),
99 warnings: Vec::new(),
100 validated_params: params.clone(),
101 transformations: Vec::new(),
102 };
103
104 if let Err(e) = self.validate_schema_syntax(schema) {
106 result.is_valid = false;
107 result.errors.push(e);
108 return result;
109 }
110
111 if self.auto_transform {
113 if let Err(e) = self.apply_transformations(schema, &mut result) {
114 result.is_valid = false;
115 result.errors.push(e);
116 return result;
117 }
118 }
119
120 if let Err(e) = self.validate_against_schema(schema, &result.validated_params) {
122 result.is_valid = false;
123 result.errors.push(e);
124 }
125
126 if let Err(e) = self.check_required_fields(schema, &result.validated_params) {
128 result.is_valid = false;
129 result.errors.push(e);
130 }
131
132 result
133 }
134
135 fn validate_schema_syntax(&self, schema: &Value) -> Result<(), ValidationError> {
137 if !schema.is_object() {
139 return Err(ValidationError::InvalidSchema(
140 "Schema must be a JSON object".to_string(),
141 ));
142 }
143 Ok(())
144 }
145
146 fn validate_against_schema(
148 &self,
149 schema: &Value,
150 params: &Value,
151 ) -> Result<(), ValidationError> {
152 if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
154 if let Some(params_obj) = params.as_object() {
155 for (field_name, field_schema) in properties {
156 if let Some(param_value) = params_obj.get(field_name) {
157 if let Some(expected_type) =
159 field_schema.get("type").and_then(|t| t.as_str())
160 {
161 let valid_type = match expected_type {
162 "string" => param_value.is_string(),
163 "number" => param_value.is_number(),
164 "integer" => {
165 param_value.is_number()
166 && param_value.as_f64().is_some_and(|n| n.fract() == 0.0)
167 }
168 "boolean" => param_value.is_boolean(),
169 "array" => param_value.is_array(),
170 "object" => param_value.is_object(),
171 _ => true, };
173
174 if !valid_type {
175 return Err(ValidationError::ValidationFailed {
176 field: field_name.clone(),
177 reason: format!(
178 "Expected type '{}' but got '{}'",
179 expected_type,
180 if param_value.is_string() {
181 "string"
182 } else if param_value.is_number() {
183 "number"
184 } else if param_value.is_boolean() {
185 "boolean"
186 } else if param_value.is_array() {
187 "array"
188 } else if param_value.is_object() {
189 "object"
190 } else {
191 "null"
192 }
193 ),
194 });
195 }
196 }
197 }
198 }
199 }
200 }
201 Ok(())
202 }
203
204 fn apply_transformations(
206 &self,
207 schema: &Value,
208 result: &mut ValidationResult,
209 ) -> Result<(), ValidationError> {
210 if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
211 if let Value::Object(ref mut params_map) = result.validated_params {
212 let mut transformations = Vec::new();
213
214 for (field_name, field_schema) in properties {
215 if let Some(param_value) = params_map.get_mut(field_name) {
216 let field_transformations =
217 self.transform_field_value(field_name, field_schema, param_value)?;
218 transformations.extend(field_transformations);
219 }
220 }
221
222 result.transformations.extend(transformations);
223 }
224 }
225 Ok(())
226 }
227
228 fn transform_field_value(
230 &self,
231 field_name: &str,
232 field_schema: &Value,
233 param_value: &mut Value,
234 ) -> Result<Vec<String>, ValidationError> {
235 let mut transformations = Vec::new();
236
237 if let Some("string") = field_schema.get("type").and_then(|t| t.as_str()) {
239 if let Some(description) = field_schema.get("description").and_then(|d| d.as_str()) {
240 let desc_lower = description.to_lowercase();
241 if desc_lower.contains("url")
242 || desc_lower.contains("uri")
243 || field_name.to_lowercase().contains("url")
244 {
245 if let Value::String(url_str) = param_value {
246 let original_url = url_str.clone();
247 if let Some(fixed_url) = self.auto_fix_url(&original_url) {
248 *param_value = Value::String(fixed_url.clone());
249 transformations.push(format!(
250 "Auto-prefixed URL in '{}': '{}' → '{}'",
251 field_name, original_url, fixed_url
252 ));
253 }
254 }
255 }
256 }
257 }
258
259 if let Some("number") = field_schema.get("type").and_then(|t| t.as_str()) {
261 if let Value::String(str_val) = param_value {
262 let original_str = str_val.clone();
263 if let Ok(num_val) = original_str.parse::<f64>() {
264 *param_value = Value::Number(serde_json::Number::from_f64(num_val).unwrap());
265 transformations.push(format!(
266 "Converted string to number in '{}': '{}' → {}",
267 field_name, original_str, num_val
268 ));
269 }
270 }
271 }
272
273 if let Some("integer") = field_schema.get("type").and_then(|t| t.as_str()) {
275 if let Value::String(str_val) = param_value {
276 let original_str = str_val.clone();
277 if let Ok(int_val) = original_str.parse::<i64>() {
278 *param_value = Value::Number(serde_json::Number::from(int_val));
279 transformations.push(format!(
280 "Converted string to integer in '{}': '{}' → {}",
281 field_name, original_str, int_val
282 ));
283 }
284 }
285 }
286
287 if let Some("boolean") = field_schema.get("type").and_then(|t| t.as_str()) {
289 if let Value::String(str_val) = param_value {
290 let original_str = str_val.clone();
291 let bool_val = match original_str.to_lowercase().as_str() {
292 "true" | "yes" | "1" | "on" => Some(true),
293 "false" | "no" | "0" | "off" => Some(false),
294 _ => None,
295 };
296 if let Some(bool_val) = bool_val {
297 *param_value = Value::Bool(bool_val);
298 transformations.push(format!(
299 "Converted string to boolean in '{}': '{}' → {}",
300 field_name, original_str, bool_val
301 ));
302 }
303 }
304 }
305
306 Ok(transformations)
307 }
308
309 fn auto_fix_url(&self, url: &str) -> Option<String> {
311 if url.is_empty() || url.starts_with("http://") || url.starts_with("https://") {
312 return None; }
314
315 if url.starts_with("localhost")
317 || url.starts_with("127.0.0.1")
318 || url.starts_with("0.0.0.0")
319 {
320 Some(format!("http://{}", url))
321 } else if url.contains('.') && !url.contains(' ') {
322 Some(format!("https://{}", url))
324 } else {
325 None
326 }
327 }
328
329 fn check_required_fields(&self, schema: &Value, params: &Value) -> Result<(), ValidationError> {
331 if let Some(required) = schema.get("required").and_then(|r| r.as_array()) {
332 if let Some(params_obj) = params.as_object() {
333 for required_field in required {
334 if let Some(field_name) = required_field.as_str() {
335 if !params_obj.contains_key(field_name) {
336 return Err(ValidationError::MissingRequired {
337 field: field_name.to_string(),
338 });
339 }
340 }
341 }
342 }
343 }
344 Ok(())
345 }
346
347 pub fn is_valid(&self, schema: &Value, params: &Value) -> bool {
349 self.validate(schema, params).is_valid
350 }
351
352 pub fn extract_parameter_hints(&self, schema: &Value) -> HashMap<String, ParameterHint> {
354 let mut hints = HashMap::new();
355
356 if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
357 let required_fields: Vec<String> = schema
358 .get("required")
359 .and_then(|r| r.as_array())
360 .map(|arr| {
361 arr.iter()
362 .filter_map(|v| v.as_str().map(|s| s.to_string()))
363 .collect()
364 })
365 .unwrap_or_default();
366
367 for (field_name, field_schema) in properties {
368 let hint = ParameterHint {
369 name: field_name.clone(),
370 param_type: field_schema
371 .get("type")
372 .and_then(|t| t.as_str())
373 .unwrap_or("string")
374 .to_string(),
375 description: field_schema
376 .get("description")
377 .and_then(|d| d.as_str())
378 .map(|s| s.to_string()),
379 required: required_fields.contains(field_name),
380 default_value: field_schema.get("default").cloned(),
381 enum_values: field_schema.get("enum").and_then(|e| e.as_array()).cloned(),
382 format: field_schema
383 .get("format")
384 .and_then(|f| f.as_str())
385 .map(|s| s.to_string()),
386 pattern: field_schema
387 .get("pattern")
388 .and_then(|p| p.as_str())
389 .map(|s| s.to_string()),
390 min_length: field_schema.get("minLength").and_then(|m| m.as_u64()),
391 max_length: field_schema.get("maxLength").and_then(|m| m.as_u64()),
392 };
393 hints.insert(field_name.clone(), hint);
394 }
395 }
396
397 hints
398 }
399}
400
401#[derive(Debug, Clone)]
403pub struct ParameterHint {
404 pub name: String,
406 pub param_type: String,
408 pub description: Option<String>,
410 pub required: bool,
412 pub default_value: Option<Value>,
414 pub enum_values: Option<Vec<Value>>,
416 pub format: Option<String>,
418 pub pattern: Option<String>,
420 pub min_length: Option<u64>,
422 pub max_length: Option<u64>,
424}
425
426pub fn validate_parameters(schema: &Value, params: &Value) -> ValidationResult {
428 ParameterValidator::new().validate(schema, params)
429}
430
431pub fn validate_parameters_strict(schema: &Value, params: &Value) -> ValidationResult {
433 ParameterValidator::strict().validate(schema, params)
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use serde_json::json;
440
441 #[test]
442 fn test_url_auto_prefixing() {
443 let schema = json!({
444 "type": "object",
445 "properties": {
446 "url": {
447 "type": "string",
448 "description": "The URL to navigate to"
449 }
450 },
451 "required": ["url"]
452 });
453
454 let params = json!({"url": "www.google.com"});
455 let validator = ParameterValidator::new();
456 let result = validator.validate(&schema, ¶ms);
457
458 assert!(result.is_valid);
459 assert_eq!(result.validated_params["url"], "https://www.google.com");
460 assert!(!result.transformations.is_empty());
461 }
462
463 #[test]
464 fn test_localhost_url_prefixing() {
465 let schema = json!({
466 "type": "object",
467 "properties": {
468 "url": {
469 "type": "string",
470 "description": "The URL to navigate to"
471 }
472 }
473 });
474
475 let params = json!({"url": "localhost:3000"});
476 let validator = ParameterValidator::new();
477 let result = validator.validate(&schema, ¶ms);
478
479 assert!(result.is_valid);
480 assert_eq!(result.validated_params["url"], "http://localhost:3000");
481 }
482
483 #[test]
484 fn test_type_coercion() {
485 let schema = json!({
486 "type": "object",
487 "properties": {
488 "width": {"type": "number"},
489 "height": {"type": "integer"},
490 "visible": {"type": "boolean"}
491 }
492 });
493
494 let params = json!({
495 "width": "800.5",
496 "height": "600",
497 "visible": "true"
498 });
499
500 let validator = ParameterValidator::new();
501 let result = validator.validate(&schema, ¶ms);
502
503 assert!(result.is_valid);
504 assert_eq!(result.validated_params["width"], 800.5);
505 assert_eq!(result.validated_params["height"], 600);
506 assert_eq!(result.validated_params["visible"], true);
507 assert_eq!(result.transformations.len(), 3);
508 }
509
510 #[test]
511 fn test_required_field_validation() {
512 let schema = json!({
513 "type": "object",
514 "properties": {
515 "url": {"type": "string"}
516 },
517 "required": ["url"]
518 });
519
520 let params = json!({});
521 let validator = ParameterValidator::new();
522 let result = validator.validate(&schema, ¶ms);
523
524 assert!(!result.is_valid);
525 assert!(result
526 .errors
527 .iter()
528 .any(|e| matches!(e, ValidationError::MissingRequired { field } if field == "url")));
529 }
530
531 #[test]
532 fn test_strict_mode_no_transforms() {
533 let schema = json!({
534 "type": "object",
535 "properties": {
536 "url": {"type": "string"}
537 }
538 });
539
540 let params = json!({"url": "www.google.com"});
541 let validator = ParameterValidator::strict();
542 let result = validator.validate(&schema, ¶ms);
543
544 assert_eq!(result.validated_params["url"], "www.google.com");
546 assert!(result.transformations.is_empty());
547 }
548}