main-spex 0.26.0

AI-first development framework that turns specifications into verified code
use std::path::{Path, PathBuf};

use crate::commands::check_schema;
use crate::config::{PACKAGE_CONFIG_FILE, Package};
use crate::error::Result;
use crate::rule::{RULES_DIR, Rule};
use crate::spec::{SPECS_DIR, Spec};
use crate::support::yaml_dir::yaml_files;
use crate::templates::SCHEMAS_DIRNAME;

use super::super::finding::{Finding, Location};
use super::conform_finding;

pub(super) fn validate_schema(
    spex_dir: &Path,
    packages: &[Package],
    specs: &[Spec],
    rules: &[Rule],
) -> Result<Vec<Finding>> {
    let mut findings = Vec::new();

    let spex_yaml = spex_dir.join(crate::config::SPEX_CONFIG_FILE);
    if spex_yaml.is_file() {
        findings.extend(validate_schema_group(
            spex_dir,
            &[spex_yaml],
            "spex.schema.json",
        ));
    } else {
        findings.push(conform_finding(
            None,
            format!("{} not found", crate::config::SPEX_CONFIG_FILE),
        ));
    }

    let package_configs: Vec<PathBuf> = packages
        .iter()
        .map(|package| package.path.join(PACKAGE_CONFIG_FILE))
        .filter(|path| path.is_file())
        .collect();
    findings.extend(validate_schema_group(
        spex_dir,
        &package_configs,
        "spex.package.schema.json",
    ));

    findings.extend(validate_schema_group(
        spex_dir,
        &yaml_files(&spex_dir.join(SPECS_DIR))?,
        "spec.schema.json",
    ));
    findings.extend(validate_schema_group(
        spex_dir,
        &yaml_files(&spex_dir.join(RULES_DIR))?,
        "rule.schema.json",
    ));

    for spec in specs {
        findings.extend(check_filename_matches_id(&spec.path, spec.id.as_str()));
    }
    for rule in rules {
        findings.extend(check_filename_matches_id(&rule.path, rule.id.as_str()));
    }
    Ok(findings)
}

fn validate_schema_group(spex_dir: &Path, files: &[PathBuf], schema_name: &str) -> Vec<Finding> {
    if files.is_empty() {
        return Vec::new();
    }
    let schema_path = spex_dir.join(SCHEMAS_DIRNAME).join(schema_name);
    let validator = match check_schema::build_validator(&schema_path) {
        Ok(validator) => validator,
        Err(err) => {
            return vec![conform_finding(
                None,
                format!("could not load schema `{}`: {err:?}", schema_path.display()),
            )];
        }
    };

    let mut findings = Vec::new();
    for file in files {
        match check_schema::validate_file(&validator, file) {
            Ok(messages) => {
                for message in messages {
                    findings.push(conform_finding(Some(Location::in_file(file)), message));
                }
            }
            Err(err) => {
                findings.push(conform_finding(
                    Some(Location::in_file(file)),
                    format!("{err:?}"),
                ));
            }
        }
    }
    findings
}

fn check_filename_matches_id(path: &Path, id: &str) -> Option<Finding> {
    let stem = path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or_default();
    (id != stem).then(|| {
        conform_finding(
            Some(Location::in_file(path)),
            format!("filename stem `{stem}` does not match id `{id}`"),
        )
    })
}