pub mod format;
use serde_json::Value;
const MAX_REPORTED_ERRORS: usize = 3;
const MAX_ERROR_LEN: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ArgValidation {
Valid,
Invalid(String),
SchemaUnusable(String),
}
pub fn validate_tool_args(tool_name: &str, schema: &Value, args: &Value) -> ArgValidation {
let empty_object;
let instance = match args.is_null() {
true => {
empty_object = Value::Object(serde_json::Map::new());
&empty_object
}
false => args,
};
check(
schema,
instance,
&format!("invalid arguments for '{tool_name}'"),
)
}
pub fn validate_output(schema: &Value, content: &str) -> ArgValidation {
if let Err(e) = jsonschema::validator_for(schema) {
return ArgValidation::SchemaUnusable(e.to_string());
}
let instance = match serde_json::from_str::<Value>(content) {
Ok(v) => v,
Err(e) => {
let rendered = e.to_string();
let message = leviath_core::truncate_at_boundary(&rendered, MAX_ERROR_LEN);
return ArgValidation::Invalid(format!(
"[error] final output does not match the declared schema: it is not valid JSON \
({message})"
));
}
};
check(
schema,
&instance,
"final output does not match the declared schema",
)
}
fn check(schema: &Value, instance: &Value, subject: &str) -> ArgValidation {
let validator = match jsonschema::validator_for(schema) {
Ok(v) => v,
Err(e) => return ArgValidation::SchemaUnusable(e.to_string()),
};
let violations: Vec<String> = validator.iter_errors(instance).map(render_error).collect();
if violations.is_empty() {
return ArgValidation::Valid;
}
let reported = violations
.iter()
.take(MAX_REPORTED_ERRORS)
.cloned()
.collect::<Vec<_>>()
.join("; ");
let suffix = match violations.len() > MAX_REPORTED_ERRORS {
true => format!("; (and {} more)", violations.len() - MAX_REPORTED_ERRORS),
false => String::new(),
};
ArgValidation::Invalid(format!("[error] {subject}: {reported}{suffix}"))
}
fn render_error(error: jsonschema::ValidationError<'_>) -> String {
let message = error.to_string();
let message = leviath_core::truncate_at_boundary(&message, MAX_ERROR_LEN);
let path = error.instance_path().to_string();
match path.is_empty() {
true => message.to_string(),
false => format!("at {path}: {message}"),
}
}
#[cfg(test)]
mod tests;