main-spex 0.23.0

AI-first development framework that turns specifications into deterministic, verifiable code
use std::path::Path;
use std::process::ExitCode;

use clap::Args;
use eros::{Context, IntoAnyUnion, bail};

use crate::config::Package;
use crate::config::spex::Spex;
use crate::error::Result;
use crate::rule::RULES_DIR;
use crate::support::ai_view::ai_view;
use crate::support::fs::write_file;
use crate::support::yaml_dir::load_yaml_dir;

#[cfg(test)]
#[path = "rules_test.rs"]
mod rules_test;

const FIELDS_AI_DOES_NOT_NEED: [&str; 2] = ["sync_with_upstream", "checks"];

#[derive(Args)]
pub struct RulesArgs {
    #[arg(long, help = "Drop fields AI doesn't need, add ones it does")]
    ai: bool,
    #[arg(long, num_args = 1.., help = "Filter by rule ids")]
    id: Vec<String>,
    #[arg(long, num_args = 1.., help = "Filter by package names")]
    package: Vec<String>,
    #[arg(long, help = "Write the output to this file")]
    output: Option<String>,
}

pub fn run(args: RulesArgs) -> Result<ExitCode> {
    let spex = Spex::discover()?;
    let yaml = render(&spex, &args)?;
    match &args.output {
        Some(path) => {
            write_file(Path::new(path), &yaml)?;
        }
        None => print!("{yaml}"),
    }
    Ok(ExitCode::SUCCESS)
}

fn render(spex: &Spex, args: &RulesArgs) -> Result<String> {
    let packages = spex.select_packages(&args.package)?;

    let rules: Vec<serde_json::Value> =
        load_yaml_dir::<serde_json::Value>(&spex.dir.join(RULES_DIR))?
            .iter()
            .map(|file| {
                if args.ai {
                    ai_view(file, &FIELDS_AI_DOES_NOT_NEED)
                } else {
                    file.item.clone()
                }
            })
            .collect();

    for id in &args.id {
        if !rules
            .iter()
            .any(|rule| rule.get("id").and_then(|v| v.as_str()) == Some(id.as_str()))
        {
            bail!("unknown rule id `{id}` — not found in {RULES_DIR}");
        }
    }

    let output: Vec<&serde_json::Value> = rules
        .iter()
        .filter(|rule| {
            packages
                .iter()
                .any(|package| package_has_rule(package, rule))
        })
        .filter(|rule| id_selected(rule, &args.id))
        .collect();

    if output.is_empty() {
        return serde_yaml_ng::to_string(&output)
            .context("could not serialize rules")
            .any_union();
    }

    output
        .iter()
        .map(|rule| {
            serde_yaml_ng::to_string(&[rule])
                .context("could not serialize rules")
                .any_union()
        })
        .collect::<Result<Vec<_>>>()
        .map(|docs| docs.join("\n"))
}

fn package_has_rule(package: &Package, rule: &serde_json::Value) -> bool {
    rule.get("id")
        .and_then(|v| v.as_str())
        .is_some_and(|rule_id| package.rules.iter().any(|id| id.as_str() == rule_id))
}

fn id_selected(value: &serde_json::Value, ids: &[String]) -> bool {
    ids.is_empty()
        || value
            .get("id")
            .and_then(|v| v.as_str())
            .is_some_and(|value_id| ids.iter().any(|id| id == value_id))
}