cloudformation-validate-data-source 1.11.0

AWS CloudFormation Validate
Documentation
use crate::process::SchemaTop;
use log::info;
use serde_json::json;
use std::collections::HashMap;
use std::fs;
use std::path::Path;

pub fn generate(generated_dir: &Path, handwritten_dir: &Path) -> anyhow::Result<()> {
    let schema_source = generated_dir.join("patched_schemas");
    if !schema_source.exists() {
        anyhow::bail!("Patched schema directory not found: {}\nRun process step first.", schema_source.display());
    }

    let rules_dir = generated_dir.join("cel-rules");
    fs::create_dir_all(&rules_dir)?;
    info!("CEL codegen: schemas={} rules={}", schema_source.display(), rules_dir.display());

    let mut raw_schemas: HashMap<String, serde_json::Value> = HashMap::new();
    for entry in fs::read_dir(&schema_source)? {
        let path = entry?.path();
        if path.extension().and_then(|e| e.to_str()) != Some("json") {
            continue;
        }
        let content = fs::read_to_string(&path)?;
        // patched_schemas/ is generated by this pipeline - a parse failure or
        // missing typeName means corruption, so fail rather than skip.
        let json: serde_json::Value =
            serde_json::from_str(&content).map_err(|e| anyhow::anyhow!("failed to parse {}: {}", path.display(), e))?;
        let Some(type_name) = json.get("typeName").and_then(|v| v.as_str()) else {
            anyhow::bail!("patched schema {} has no 'typeName'", path.display());
        };
        raw_schemas.insert(type_name.to_string(), json);
    }
    anyhow::ensure!(!raw_schemas.is_empty(), "no patched schemas found in {}", schema_source.display());
    info!("Loaded {} patched schemas", raw_schemas.len());

    let mut schemas: HashMap<String, SchemaTop> = HashMap::new();
    for (type_name, json) in &raw_schemas {
        let schema: SchemaTop = serde_json::from_value(json.clone())
            .map_err(|error| anyhow::anyhow!("failed to deserialize patched schema '{}': {}", type_name, error))?;
        anyhow::ensure!(
            schema.type_name.as_deref() == Some(type_name),
            "patched schema '{}' did not preserve its required typeName",
            type_name
        );
        schemas.insert(type_name.clone(), schema);
    }
    info!("Parsed {} schemas for CEL codegen", schemas.len());

    let ds_data = generated_dir.join("data");
    let all_rules = generate_data_driven_rules(&ds_data, handwritten_dir);

    let rules_json = json!({ "rules": all_rules });
    fs::write(rules_dir.join("generated_rules.json"), serde_json::to_string_pretty(&rules_json)?)?;
    info!("Generated {} CEL rules total", all_rules.len());

    Ok(())
}

fn generate_data_driven_rules(_generated_dir: &Path, _handwritten_dir: &Path) -> Vec<serde_json::Value> {
    // Deprecated resource types are handled by a native rule elsewhere in the
    // pipeline, so no data-driven CEL rules are currently needed here.
    let rules = Vec::new();
    info!("Generated {} data-driven best practice rules", rules.len());
    rules
}