use super::ResponseSchema;
pub(super) fn schema_description(schema: ResponseSchema) -> &'static str {
match schema {
ResponseSchema::MethodReview => {
"Required fields: smelly (bool), tier (string), evidence (string), reason (string)."
}
ResponseSchema::SemanticMethodReview => {
"Required fields: smelly (bool), tier (string), pattern (string), intent (string), necessity_check (string), evidence (array of objects with start_line (number), end_line (number), quote (string)). reason is a string when present and is required for non-clean results; clean results may omit it."
}
ResponseSchema::FileReview => {
"Required fields: smelly (bool), tier (string), evidence (string), cohesive (bool), name_accurate (bool), reason (string)."
}
ResponseSchema::RoleClassification => "Required fields: role (string), reason (string).",
}
}
pub(super) fn validate_schema(
value: &serde_json::Value,
schema: ResponseSchema,
) -> Result<(), String> {
let Some(obj) = value.as_object() else {
return Err("response was not a JSON object".to_string());
};
let mut missing = Vec::new();
let mut wrong_type = Vec::new();
fn check_bool(
obj: &serde_json::Map<String, serde_json::Value>,
name: &str,
missing: &mut Vec<String>,
wrong_type: &mut Vec<String>,
) {
match obj.get(name) {
Some(v) if v.is_boolean() => {}
Some(_) => wrong_type.push(name.to_string()),
None => missing.push(name.to_string()),
}
}
fn check_string(
obj: &serde_json::Map<String, serde_json::Value>,
name: &str,
missing: &mut Vec<String>,
wrong_type: &mut Vec<String>,
) {
match obj.get(name) {
Some(v) if v.is_string() => {}
Some(_) => wrong_type.push(name.to_string()),
None => missing.push(name.to_string()),
}
}
fn check_optional_string(
obj: &serde_json::Map<String, serde_json::Value>,
name: &str,
wrong_type: &mut Vec<String>,
) {
if let Some(value) = obj.get(name)
&& !value.is_string()
{
wrong_type.push(name.to_string());
}
}
fn check_number(
obj: &serde_json::Map<String, serde_json::Value>,
name: &str,
missing: &mut Vec<String>,
wrong_type: &mut Vec<String>,
) {
match obj.get(name) {
Some(v) if v.is_u64() || v.is_i64() => {}
Some(_) => wrong_type.push(name.to_string()),
None => missing.push(name.to_string()),
}
}
fn check_semantic_evidence(
obj: &serde_json::Map<String, serde_json::Value>,
missing: &mut Vec<String>,
wrong_type: &mut Vec<String>,
) {
let Some(value) = obj.get("evidence") else {
missing.push("evidence".to_string());
return;
};
let Some(entries) = value.as_array() else {
wrong_type.push("evidence".to_string());
return;
};
for (index, entry) in entries.iter().enumerate() {
let Some(entry) = entry.as_object() else {
wrong_type.push(format!("evidence[{index}]"));
continue;
};
check_number(entry, "start_line", missing, wrong_type);
check_number(entry, "end_line", missing, wrong_type);
check_string(entry, "quote", missing, wrong_type);
}
}
match schema {
ResponseSchema::MethodReview => {
check_bool(obj, "smelly", &mut missing, &mut wrong_type);
check_string(obj, "tier", &mut missing, &mut wrong_type);
check_string(obj, "evidence", &mut missing, &mut wrong_type);
check_string(obj, "reason", &mut missing, &mut wrong_type);
}
ResponseSchema::SemanticMethodReview => {
check_bool(obj, "smelly", &mut missing, &mut wrong_type);
check_string(obj, "tier", &mut missing, &mut wrong_type);
check_string(obj, "pattern", &mut missing, &mut wrong_type);
check_string(obj, "intent", &mut missing, &mut wrong_type);
check_optional_string(obj, "reason", &mut wrong_type);
check_string(obj, "necessity_check", &mut missing, &mut wrong_type);
check_semantic_evidence(obj, &mut missing, &mut wrong_type);
}
ResponseSchema::FileReview => {
check_bool(obj, "smelly", &mut missing, &mut wrong_type);
check_string(obj, "tier", &mut missing, &mut wrong_type);
check_string(obj, "evidence", &mut missing, &mut wrong_type);
check_string(obj, "reason", &mut missing, &mut wrong_type);
check_bool(obj, "cohesive", &mut missing, &mut wrong_type);
check_bool(obj, "name_accurate", &mut missing, &mut wrong_type);
}
ResponseSchema::RoleClassification => {
check_string(obj, "role", &mut missing, &mut wrong_type);
check_string(obj, "reason", &mut missing, &mut wrong_type);
}
}
if missing.is_empty() && wrong_type.is_empty() {
Ok(())
} else {
let mut parts = Vec::new();
if !missing.is_empty() {
parts.push(format!("missing fields: {}", missing.join(", ")));
}
if !wrong_type.is_empty() {
parts.push(format!("wrong field types: {}", wrong_type.join(", ")));
}
Err(parts.join("; "))
}
}
#[cfg(test)]
mod tests {
use super::{ResponseSchema, validate_schema};
#[test]
fn clean_semantic_review_may_omit_its_explanation() {
let value = serde_json::json!({
"smelly": false,
"tier": "clean",
"pattern": "none",
"intent": "Return a configured value.",
"necessity_check": "The implementation is direct.",
"evidence": []
});
assert!(validate_schema(&value, ResponseSchema::SemanticMethodReview).is_ok());
}
}