gatekpr-validators 0.2.3

Validation rule engine for Shopify and WooCommerce app compliance (GDPR, billing, security)
//! Build script for gatekpr-validators
//!
//! Auto-discovers all `.toml` rule files under `../../rules/` and generates
//! a Rust source file that embeds them via `include_str!`. Rules are organized
//! by platform directory:
//!
//! - `rules/*.toml` → BASE_RULES (loaded for all platforms)
//! - `rules/shopify/*.toml` → SHOPIFY_RULES
//! - `rules/woocommerce/*.toml` → WOOCOMMERCE_RULES

use std::env;
use std::fs;
use std::path::{Path, PathBuf};

fn main() {
    let out_dir = env::var("OUT_DIR").unwrap();
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();

    // Rules directory: try local rules/ first (for crates.io), then workspace root (for dev)
    let local_rules = PathBuf::from(&manifest_dir).join("rules");
    let workspace_rules = PathBuf::from(&manifest_dir).join("../../rules");
    let rules_dir = if local_rules.is_dir() {
        local_rules
    } else {
        workspace_rules
            .canonicalize()
            .unwrap_or(workspace_rules)
    };

    // Tell Cargo to re-run if any rule file changes
    println!("cargo:rerun-if-changed={}", rules_dir.display());

    let mut base_entries = Vec::new();
    let mut shopify_entries = Vec::new();
    let mut woocommerce_entries = Vec::new();

    // Collect base rules (rules/*.toml)
    if let Ok(entries) = fs::read_dir(&rules_dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_file() && path.extension().map(|e| e == "toml").unwrap_or(false) {
                println!("cargo:rerun-if-changed={}", path.display());
                if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                    base_entries.push((name.to_string(), path.clone()));
                }
            }
        }
    }

    // Collect shopify rules (rules/shopify/*.toml)
    collect_platform_rules(&rules_dir.join("shopify"), &mut shopify_entries);

    // Collect woocommerce rules (rules/woocommerce/*.toml)
    collect_platform_rules(&rules_dir.join("woocommerce"), &mut woocommerce_entries);

    // Sort for deterministic output
    base_entries.sort_by(|a, b| a.0.cmp(&b.0));
    shopify_entries.sort_by(|a, b| a.0.cmp(&b.0));
    woocommerce_entries.sort_by(|a, b| a.0.cmp(&b.0));

    // Generate the Rust source file
    let mut code = String::new();
    code.push_str("// Auto-generated by build.rs — do not edit\n\n");

    // Base rules
    code.push_str("/// Base validation rules embedded at compile time.\n");
    code.push_str("/// These are loaded for all platforms as a fallback.\n");
    code.push_str("pub const BASE_RULES: &[(&str, &str)] = &[\n");
    for (name, path) in &base_entries {
        code.push_str(&format!(
            "    (\"{}\", include_str!(\"{}\")),\n",
            name,
            path.display().to_string().replace('\\', "/")
        ));
    }
    code.push_str("];\n\n");

    // Shopify rules
    code.push_str("/// Shopify-specific validation rules embedded at compile time.\n");
    code.push_str("pub const SHOPIFY_RULES: &[(&str, &str)] = &[\n");
    for (name, path) in &shopify_entries {
        code.push_str(&format!(
            "    (\"{}\", include_str!(\"{}\")),\n",
            name,
            path.display().to_string().replace('\\', "/")
        ));
    }
    code.push_str("];\n\n");

    // WooCommerce rules
    code.push_str("/// WooCommerce-specific validation rules embedded at compile time.\n");
    code.push_str("pub const WOOCOMMERCE_RULES: &[(&str, &str)] = &[\n");
    for (name, path) in &woocommerce_entries {
        code.push_str(&format!(
            "    (\"{}\", include_str!(\"{}\")),\n",
            name,
            path.display().to_string().replace('\\', "/")
        ));
    }
    code.push_str("];\n");

    let dest = Path::new(&out_dir).join("embedded_rules_gen.rs");
    fs::write(&dest, code).expect("Failed to write generated embedded rules");
}

fn collect_platform_rules(dir: &Path, entries: &mut Vec<(String, PathBuf)>) {
    if let Ok(dir_entries) = fs::read_dir(dir) {
        println!("cargo:rerun-if-changed={}", dir.display());
        for entry in dir_entries.flatten() {
            let path = entry.path();
            if path.is_file() && path.extension().map(|e| e == "toml").unwrap_or(false) {
                println!("cargo:rerun-if-changed={}", path.display());
                if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                    entries.push((name.to_string(), path.clone()));
                }
            }
        }
    }
}