use std::collections::HashMap;
use crate::traits::ValidationError;
pub fn validate_args(
args: &HashMap<String, serde_json::Value>,
schema: &serde_json::Value,
) -> Result<(), String> {
let errors = validate_args_detailed(args, schema);
if errors.is_empty() {
Ok(())
} else {
Err(errors[0].to_string())
}
}
pub fn validate_args_detailed(
args: &HashMap<String, serde_json::Value>,
schema: &serde_json::Value,
) -> Vec<ValidationError> {
let mut errors = Vec::new();
if let Some(required) = schema.get("required").and_then(|r| r.as_array()) {
for req in required {
if let Some(field_name) = req.as_str()
&& !args.contains_key(field_name)
{
errors.push(ValidationError {
path: field_name.to_string(),
message: format!("Missing required parameter: '{field_name}'"),
});
}
}
}
if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
for (key, value) in args {
if let Some(prop_schema) = properties.get(key)
&& let Err(msg) = validate_value_type(key, value, prop_schema)
{
errors.push(ValidationError {
path: key.clone(),
message: msg,
});
}
}
}
errors
}
fn validate_value_type(
key: &str,
value: &serde_json::Value,
prop_schema: &serde_json::Value,
) -> Result<(), String> {
if let Some(enum_values) = prop_schema.get("enum").and_then(|e| e.as_array())
&& !enum_values.contains(value)
{
return Err(format!(
"Parameter '{key}' value {value} is not one of the allowed values: {enum_values:?}"
));
}
if let Some(expected_type) = prop_schema.get("type").and_then(|t| t.as_str()) {
let type_ok = match expected_type {
"string" => value.is_string(),
"number" => value.is_number(),
"integer" => value.is_i64() || value.is_u64(),
"boolean" => value.is_boolean(),
"array" => value.is_array(),
"object" => value.is_object(),
"null" => value.is_null(),
_ => true, };
if !type_ok {
if expected_type == "number" && (value.is_i64() || value.is_u64()) {
return Ok(());
}
return Err(format!(
"Parameter '{key}' expected type '{expected_type}', got {}",
json_type_name(value)
));
}
}
if let Some(s) = value.as_str()
&& let Some(min_len) = prop_schema.get("minLength").and_then(|v| v.as_u64())
&& (s.len() as u64) < min_len
{
return Err(format!(
"Parameter '{key}' string length {} is below minimum {min_len}",
s.len()
));
}
if let Some(s) = value.as_str()
&& let Some(max_len) = prop_schema.get("maxLength").and_then(|v| v.as_u64())
&& (s.len() as u64) > max_len
{
return Err(format!(
"Parameter '{key}' string length {} exceeds maximum {max_len}",
s.len()
));
}
if let Some(arr) = value.as_array() {
if let Some(min_items) = prop_schema.get("minItems").and_then(|v| v.as_u64())
&& (arr.len() as u64) < min_items
{
return Err(format!(
"Parameter '{key}' array has {} items, minimum is {min_items}",
arr.len()
));
}
if let Some(max_items) = prop_schema.get("maxItems").and_then(|v| v.as_u64())
&& (arr.len() as u64) > max_items
{
return Err(format!(
"Parameter '{key}' array has {} items, maximum is {max_items}",
arr.len()
));
}
if let Some(items_schema) = prop_schema.get("items") {
for (i, elem) in arr.iter().enumerate() {
let elem_key = format!("{key}[{i}]");
validate_array_item(&elem_key, elem, items_schema)?;
}
}
}
if let Some(obj) = value.as_object()
&& let Some(properties) = prop_schema.get("properties").and_then(|p| p.as_object())
{
if let Some(required) = prop_schema.get("required").and_then(|r| r.as_array()) {
for req in required {
if let Some(field_name) = req.as_str()
&& !obj.contains_key(field_name)
{
return Err(format!(
"Parameter '{key}' is missing required field '{field_name}'"
));
}
}
}
for (prop_key, prop_val) in obj {
if let Some(prop_def) = properties.get(prop_key) {
let nested_key = format!("{key}.{prop_key}");
validate_value_type(&nested_key, prop_val, prop_def)?;
}
}
}
Ok(())
}
fn validate_array_item(
key: &str,
value: &serde_json::Value,
items_schema: &serde_json::Value,
) -> Result<(), String> {
if let Some(variants) = items_schema.get("oneOf").and_then(|v| v.as_array()) {
let mut last_err = None;
for variant in variants {
match validate_value_type(key, value, variant) {
Ok(()) => return Ok(()),
Err(e) => last_err = Some(e),
}
}
return Err(last_err.unwrap_or_else(|| {
format!("Parameter '{key}' does not match any allowed schema variant")
}));
}
validate_value_type(key, value, items_schema)
}
fn json_type_name(value: &serde_json::Value) -> &'static str {
match value {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "boolean",
serde_json::Value::Number(_) => "number",
serde_json::Value::String(_) => "string",
serde_json::Value::Array(_) => "array",
serde_json::Value::Object(_) => "object",
}
}
#[cfg(test)]
#[path = "validation_tests.rs"]
mod tests;