pub mod commands;
pub mod paths;
pub mod unified_path;
use jsonschema::ValidationError;
use jsonschema::error::{TypeKind, ValidationErrorKind};
use serde_json::Value;
pub fn condensed_schema_hint(schema: &Value) -> Option<Value> {
let properties = schema.get("properties").and_then(Value::as_object)?;
let required: Vec<Value> = schema.get("required").and_then(Value::as_array).cloned().unwrap_or_default();
let mut prop_types = serde_json::Map::new();
for (name, def) in properties {
let type_str = def.get("type").and_then(Value::as_str).unwrap_or("any").to_string();
let rendered = match def.get("enum").and_then(Value::as_array) {
Some(options) if !options.is_empty() => {
let joined = options
.iter()
.map(|option| match option {
Value::String(s) => s.clone(),
other => other.to_string(),
})
.collect::<Vec<_>>()
.join("|");
format!("{type_str}({joined})")
}
_ => type_str,
};
prop_types.insert(name.clone(), Value::String(rendered));
}
Some(serde_json::json!({
"required": required,
"properties": prop_types,
}))
}
pub fn describe_jsonschema_error(err: &ValidationError<'_>) -> String {
let path = err.instance_path().to_string();
let path_label = if path.is_empty() { "(root)".to_string() } else { path };
let value = err.instance();
let value_str = match &**value {
Value::String(s) => format!("\"{s}\""),
other => other.to_string(),
};
match err.kind() {
ValidationErrorKind::Enum { options } => {
let opts = options
.as_array()
.map(|items| {
items
.iter()
.map(|v| match v {
Value::String(s) => s.clone(),
other => other.to_string(),
})
.collect::<Vec<_>>()
.join(", ")
})
.unwrap_or_default();
format!("field '{path_label}' value {value_str} is not one of the allowed enum: [{opts}]")
}
ValidationErrorKind::Constant { expected_value } => {
format!("field '{path_label}' value {value_str} must equal the required const {expected_value}")
}
ValidationErrorKind::Type { kind } => {
let expected = match kind {
TypeKind::Single(t) => t.to_string(),
TypeKind::Multiple(set) => format!("{set:?}"),
};
format!("field '{path_label}' has wrong type: expected {expected}, got {value_str}")
}
ValidationErrorKind::Required { property } => {
let name = match property {
Value::String(s) => s.clone(),
other => other.to_string(),
};
format!("missing required property '{name}'")
}
ValidationErrorKind::AdditionalProperties { unexpected } => {
format!("unexpected field(s) {unexpected:?} not allowed by the schema (did you use the right field name?)")
}
_ => format!("field '{path_label}' failed validation: {value_str}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn enum_error_names_field_and_valid_options() {
let schema = json!({
"type": "object",
"properties": {
"format": {"type": "string", "enum": ["github", "sarif", "files_with_matches", "count"]}
}
});
let instance = json!({ "format": "content" });
let error = jsonschema::validate(&schema, &instance).unwrap_err();
let msg = describe_jsonschema_error(&error);
assert!(msg.contains("field '/format'"), "msg was: {msg}");
assert!(msg.contains("\"content\""), "msg was: {msg}");
assert!(msg.contains("github, sarif, files_with_matches, count"), "msg was: {msg}");
}
#[test]
fn multiple_errors_are_described_independently() {
let schema = json!({
"type": "object",
"required": ["action", "format"],
"properties": {
"action": {"type": "string"},
"format": {"type": "string", "enum": ["github", "sarif"]}
}
});
let instance = json!({ "format": "content" });
let validator = jsonschema::validator_for(&schema).expect("schema is valid");
let errors: Vec<_> = validator.iter_errors(&instance).collect();
assert!(errors.len() >= 2, "expected both missing-action and bad-format errors, got {}", errors.len());
let messages: Vec<String> = errors.iter().map(describe_jsonschema_error).collect();
assert!(messages.iter().any(|m| m.contains("missing required property 'action'")));
assert!(
messages
.iter()
.any(|m| m.contains("field '/format'") && m.contains("\"content\""))
);
}
#[test]
fn missing_required_names_property() {
let schema = json!({
"type": "object",
"required": ["action"],
"properties": { "action": {"type": "string"} }
});
let instance = json!({});
let error = jsonschema::validate(&schema, &instance).unwrap_err();
let msg = describe_jsonschema_error(&error);
assert!(msg.contains("missing required property 'action'"), "msg was: {msg}");
}
}