sniff-cli 0.1.1

An exhaustive LLM-backed slop finder for codebases
Documentation
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::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()),
        }
    }

    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::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("; "))
    }
}