git-slop 0.16.4

A deterministic repository token-defragmenter for humans and AI agents.
Documentation
use anyhow::{Context, Result, bail};
use globset::Glob;
use serde_json::{Map, Value, json};

use crate::model::Classification;

pub(super) fn validate_path_command(
    mapping: &Map<String, Value>,
    path: &str,
    index: usize,
) -> Result<()> {
    if mapping
        .get("command")
        .and_then(Value::as_str)
        .is_none_or(|command| command.trim().is_empty())
    {
        bail!("{path}[{index}].command must be a non-empty string");
    }
    Ok(())
}

pub(super) fn validate_generated_override(
    mapping: &Map<String, Value>,
    path: &str,
    index: usize,
) -> Result<()> {
    if let Some(globs) = mapping.get("generated_source_globs") {
        let Some(globs) = globs.as_array() else {
            bail!("{path}[{index}].generated_source_globs must be an array");
        };
        for (glob_index, glob) in globs.iter().enumerate() {
            let Some(glob) = glob.as_str() else {
                bail!("{path}[{index}].generated_source_globs[{glob_index}] must be a string");
            };
            Glob::new(glob).with_context(|| {
                format!("{path}[{index}].generated_source_globs[{glob_index}] is not a valid glob")
            })?;
        }
    }
    for key in ["generator_command", "verification_command"] {
        if mapping
            .get(key)
            .is_some_and(|value| value.as_str().is_none_or(|value| value.trim().is_empty()))
        {
            bail!("{path}[{index}].{key} must be a non-empty string");
        }
    }
    Ok(())
}

pub(super) fn path_command_schema() -> Value {
    json!({
        "type": "object",
        "additionalProperties": false,
        "required": ["path_glob", "command"],
        "properties": {
            "path_glob": {"type": "string", "minLength": 1, "description": "Repository path glob."},
            "command": {"type": "string", "minLength": 1, "description": "Focused verification command."}
        }
    })
}

pub(super) fn path_override_schema() -> Value {
    json!({
        "type": "object",
        "additionalProperties": false,
        "required": ["glob"],
        "properties": {
            "glob": {"type": "string", "minLength": 1},
            "classification": {"type": "string", "enum": Classification::values()},
            "profile": {"type": "string", "enum": ["agent_context", "data_context"]},
            "language": {"type": "string", "minLength": 1},
            "verification_applicability": {"type": "string", "enum": ["auto", "applicable", "not_applicable"]},
            "generated_source_globs": {"type": "array", "items": {"type": "string", "minLength": 1}},
            "generator_command": {"type": "string", "minLength": 1},
            "verification_command": {"type": "string", "minLength": 1}
        },
        "anyOf": [
            {"required": ["classification"]},
            {"required": ["profile"]},
            {"required": ["language"]},
            {"required": ["verification_applicability"]},
            {"required": ["generated_source_globs"]},
            {"required": ["generator_command"]},
            {"required": ["verification_command"]}
        ]
    })
}

#[cfg(test)]
mod tests {
    use std::fs;

    use serde_json::json;
    use tempfile::tempdir;

    use super::super::{config_path, load};

    #[test]
    fn verification_commands_accept_non_empty_entries_and_reject_blank_entries() {
        let repository = tempdir().expect("temporary repository");
        let path = config_path(repository.path());
        fs::create_dir_all(path.parent().expect("config parent")).expect("config directory");
        fs::write(
            &path,
            serde_yaml::to_string(&json!({
                "schema_version": 2,
                "verification": {"commands": ["cargo test --locked"]}
            }))
            .expect("serialize config"),
        )
        .expect("config");
        let normalized = load(repository.path()).expect("valid command");
        assert_eq!(
            normalized["verification"]["commands"],
            json!(["cargo test --locked"])
        );

        fs::write(
            &path,
            "schema_version: 2\nverification:\n  commands:\n    - '   '\n",
        )
        .expect("config");
        let error = load(repository.path()).expect_err("blank command must fail closed");
        assert!(
            error.to_string().contains("must be a non-empty string"),
            "{error:#}"
        );
    }
}