main-spex 0.20.0

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

use anstream::eprintln;
use eros::bail;
use owo_colors::OwoColorize;

use crate::config::{Layout, Package, PackageConfig, SPEX_CONFIG_FILE, SpexConfig};
use crate::error::Result;
use crate::rule::{self, Rule};
use crate::spec::{self, Spec, SpecCoverageIgnore};
use crate::support::fs::{current_dir, normalize};

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

pub struct Spex {
    pub dir: PathBuf,
    pub project_name: String,
    pub layout: Layout,
    pub agent_root: PathBuf,
    pub packages: Vec<Package>,
    pub coverage_ignore: Vec<SpecCoverageIgnore>,
}

impl Spex {
    pub fn locate_spex_config() -> Result<PathBuf> {
        SpexConfig::find_file(&current_dir()?)
    }

    pub fn discover() -> Result<Self> {
        let (dir, config) = Self::discover_config()?;
        Self::from_config(dir, config).with_package_configs()
    }

    pub fn discover_config() -> Result<(PathBuf, SpexConfig)> {
        let config_path = Self::locate_spex_config()?;
        let config = SpexConfig::load(&config_path)?;
        warn_if_cli_outdated(&config);
        Ok((SpexConfig::dir_of(&config_path).to_path_buf(), config))
    }

    pub fn from_config(dir: PathBuf, config: SpexConfig) -> Self {
        let packages = config
            .packages
            .into_iter()
            .map(|package| Package::from_config(package, &dir))
            .collect();
        Self {
            agent_root: normalize(&dir.join(&config.agent_root)),
            dir,
            project_name: config.project_name,
            layout: config.layout,
            packages,
            coverage_ignore: config.coverage_ignore,
        }
    }

    fn with_package_configs(mut self) -> Result<Self> {
        for package in &mut self.packages {
            let package_config = PackageConfig::load(&package.path)?;
            self.coverage_ignore.extend(package_config.coverage_ignore);
            package.ignored_errors.extend(package_config.ignored_errors);
        }
        Ok(self)
    }

    pub fn rules(&self) -> Result<Vec<Rule>> {
        Rule::load_all(&self.dir.join(rule::RULES_DIR))
    }

    pub fn specs(&self) -> Result<Vec<Spec>> {
        let mut specs = Spec::load_all(&self.dir.join(spec::SPECS_DIR))?;
        for spec in &mut specs {
            let waivers = self
                .coverage_ignore
                .iter()
                .filter(|waiver| waiver.spec_id == spec.id)
                .map(|waiver| waiver.ignore.clone());
            spec.coverage_ignore.extend(waivers);
        }
        Ok(specs)
    }

    pub fn validate_package_names(&self, names: &[String]) -> Result<()> {
        for name in names {
            if !self
                .packages
                .iter()
                .any(|package| package.matches_any_package_name(name))
            {
                bail!("unknown package `{name}` — not listed in {SPEX_CONFIG_FILE}");
            }
        }
        Ok(())
    }

    pub fn select_packages(&self, names: &[String]) -> Result<Vec<&Package>> {
        self.validate_package_names(names)?;
        Ok(self
            .packages
            .iter()
            .filter(|package| {
                names.is_empty()
                    || names
                        .iter()
                        .any(|name| package.matches_any_package_name(name))
            })
            .collect())
    }

    pub fn validate_scopes(&self, scopes: &[String]) -> Result<()> {
        for short_name in scopes {
            if !self
                .packages
                .iter()
                .any(|package| &package.package_short_name == short_name)
            {
                bail!(
                    "unknown package short name `{short_name}` — not listed in {SPEX_CONFIG_FILE}"
                );
            }
        }
        Ok(())
    }
}

fn warn_if_cli_outdated(config: &SpexConfig) {
    if let Some(warning) = config.version_warning() {
        eprintln!("{} {warning}", "warning:".yellow());
    }
}