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 '{field_name}': '{original_url}' → '{fixed_url}'"
251 ));
252 }
253 }
254 }
255 }
256 }
257
258 if let Some("number") = field_schema.get("type").and_then(|t| t.as_str()) {
260 if let Value::String(str_val) = param_value {
261 let original_str = str_val.clone();
262 if let Ok(num_val) = original_str.parse::<f64>() {
263 *param_value = Value::Number(serde_json::Number::from_f64(num_val).unwrap());
264 transformations.push(format!(
265 "Converted string to number in '{field_name}': '{original_str}' → {num_val}"
266 ));
267 }
268 }
269 }
270
271 if let Some("integer") = field_schema.get("type").and_then(|t| t.as_str()) {
273 if let Value::String(str_val) = param_value {
274 let original_str = str_val.clone();
275 if let Ok(int_val) = original_str.parse::<i64>() {
276 *param_value = Value::Number(serde_json::Number::from(int_val));
277 transformations.push(format!(
278 "Converted string to integer in '{field_name}': '{original_str}' → {int_val}"
279 ));
280 }
281 }
282 }
283
284 if let Some("boolean") = field_schema.get("type").and_then(|t| t.as_str()) {
286 if let Value::String(str_val) = param_value {
287 let original_str = str_val.clone();
288 let bool_val = match original_str.to_lowercase().as_str() {
289 "true" | "yes" | "1" | "on" => Some(true),
290 "false" | "no" | "0" | "off" => Some(false),
291 _ => None,
292 };
293 if let Some(bool_val) = bool_val {
294 *param_value = Value::Bool(bool_val);
295 transformations.push(format!(
296 "Converted string to boolean in '{field_name}': '{original_str}' → {bool_val}"
297 ));
298 }
299 }
300 }
301
302 Ok(transformations)
303 }
304
305 fn auto_fix_url(&self, url: &str) -> Option<String> {
307 if url.is_empty() || url.starts_with("http://") || url.starts_with("https://") {
308 return None; }
310
311 if url.starts_with("localhost")
313 || url.starts_with("127.0.0.1")
314 || url.starts_with("0.0.0.0")
315 {
316 Some(format!("http://{url}"))
317 } else if url.contains('.') && !url.contains(' ') {
318 Some(format!("https://{url}"))
320 } else {
321 None
322 }
323 }
324
325 fn check_required_fields(&self, schema: &Value, params: &Value) -> Result<(), ValidationError> {
327 if let Some(required) = schema.get("required").and_then(|r| r.as_array()) {
328 if let Some(params_obj) = params.as_object() {
329 for required_field in required {
330 if let Some(field_name) = required_field.as_str() {
331 if !params_obj.contains_key(field_name) {
332 return Err(ValidationError::MissingRequired {
333 field: field_name.to_string(),
334 });
335 }
336 }
337 }
338 }
339 }
340 Ok(())
341 }
342
343 pub fn is_valid(&self, schema: &Value, params: &Value) -> bool {
345 self.validate(schema, params).is_valid
346 }
347
348 pub fn extract_parameter_hints(&self, schema: &Value) -> HashMap<String, ParameterHint> {
350 let mut hints = HashMap::new();
351
352 if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
353 let required_fields: Vec<String> = schema
354 .get("required")
355 .and_then(|r| r.as_array())
356 .map(|arr| {
357 arr.iter()
358 .filter_map(|v| v.as_str().map(|s| s.to_string()))
359 .collect()
360 })
361 .unwrap_or_default();
362
363 for (field_name, field_schema) in properties {
364 let hint = ParameterHint {
365 name: field_name.clone(),
366 param_type: field_schema
367 .get("type")
368 .and_then(|t| t.as_str())
369 .unwrap_or("string")
370 .to_string(),
371 description: field_schema
372 .get("description")
373 .and_then(|d| d.as_str())
374 .map(|s| s.to_string()),
375 required: required_fields.contains(field_name),
376 default_value: field_schema.get("default").cloned(),
377 enum_values: field_schema.get("enum").and_then(|e| e.as_array()).cloned(),
378 format: field_schema
379 .get("format")
380 .and_then(|f| f.as_str())
381 .map(|s| s.to_string()),
382 pattern: field_schema
383 .get("pattern")
384 .and_then(|p| p.as_str())
385 .map(|s| s.to_string()),
386 min_length: field_schema.get("minLength").and_then(|m| m.as_u64()),
387 max_length: field_schema.get("maxLength").and_then(|m| m.as_u64()),
388 };
389 hints.insert(field_name.clone(), hint);
390 }
391 }
392
393 hints
394 }
395}
396
397#[derive(Debug, Clone)]
399pub struct ParameterHint {
400 pub name: String,
402 pub param_type: String,
404 pub description: Option<String>,
406 pub required: bool,
408 pub default_value: Option<Value>,
410 pub enum_values: Option<Vec<Value>>,
412 pub format: Option<String>,
414 pub pattern: Option<String>,
416 pub min_length: Option<u64>,
418 pub max_length: Option<u64>,
420}
421
422pub fn validate_parameters(schema: &Value, params: &Value) -> ValidationResult {
424 ParameterValidator::new().validate(schema, params)
425}
426
427pub fn validate_parameters_strict(schema: &Value, params: &Value) -> ValidationResult {
429 ParameterValidator::strict().validate(schema, params)
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435 use serde_json::json;
436
437 #[test]
438 fn test_url_auto_prefixing() {
439 let schema = json!({
440 "type": "object",
441 "properties": {
442 "url": {
443 "type": "string",
444 "description": "The URL to navigate to"
445 }
446 },
447 "required": ["url"]
448 });
449
450 let params = json!({"url": "www.google.com"});
451 let validator = ParameterValidator::new();
452 let result = validator.validate(&schema, ¶ms);
453
454 assert!(result.is_valid);
455 assert_eq!(result.validated_params["url"], "https://www.google.com");
456 assert!(!result.transformations.is_empty());
457 }
458
459 #[test]
460 fn test_localhost_url_prefixing() {
461 let schema = json!({
462 "type": "object",
463 "properties": {
464 "url": {
465 "type": "string",
466 "description": "The URL to navigate to"
467 }
468 }
469 });
470
471 let params = json!({"url": "localhost:3000"});
472 let validator = ParameterValidator::new();
473 let result = validator.validate(&schema, ¶ms);
474
475 assert!(result.is_valid);
476 assert_eq!(result.validated_params["url"], "http://localhost:3000");
477 }
478
479 #[test]
480 fn test_type_coercion() {
481 let schema = json!({
482 "type": "object",
483 "properties": {
484 "width": {"type": "number"},
485 "height": {"type": "integer"},
486 "visible": {"type": "boolean"}
487 }
488 });
489
490 let params = json!({
491 "width": "800.5",
492 "height": "600",
493 "visible": "true"
494 });
495
496 let validator = ParameterValidator::new();
497 let result = validator.validate(&schema, ¶ms);
498
499 assert!(result.is_valid);
500 assert_eq!(result.validated_params["width"], 800.5);
501 assert_eq!(result.validated_params["height"], 600);
502 assert_eq!(result.validated_params["visible"], true);
503 assert_eq!(result.transformations.len(), 3);
504 }
505
506 #[test]
507 fn test_required_field_validation() {
508 let schema = json!({
509 "type": "object",
510 "properties": {
511 "url": {"type": "string"}
512 },
513 "required": ["url"]
514 });
515
516 let params = json!({});
517 let validator = ParameterValidator::new();
518 let result = validator.validate(&schema, ¶ms);
519
520 assert!(!result.is_valid);
521 assert!(result
522 .errors
523 .iter()
524 .any(|e| matches!(e, ValidationError::MissingRequired { field } if field == "url")));
525 }
526
527 #[test]
528 fn test_strict_mode_no_transforms() {
529 let schema = json!({
530 "type": "object",
531 "properties": {
532 "url": {"type": "string"}
533 }
534 });
535
536 let params = json!({"url": "www.google.com"});
537 let validator = ParameterValidator::strict();
538 let result = validator.validate(&schema, ¶ms);
539
540 assert_eq!(result.validated_params["url"], "www.google.com");
542 assert!(result.transformations.is_empty());
543 }
544}