perl-dap 0.13.3

Debug Adapter Protocol server for Perl
// Build script - panics are acceptable for build failures.
// Wave Final PR B: perl-feature-catalog absorbed; catalog logic is package-local
// so crates.io archives build outside the workspace.
#![allow(clippy::pedantic, clippy::panic)]

use serde::Deserialize;
use std::env;
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};

const DEFAULT_DAP_FEATURES: &[&str] = &["dap.breakpoints.basic", "dap.core", "dap.inline_values"];

#[derive(Debug, Deserialize)]
struct FeatureCatalog {
    feature: Vec<Feature>,
}

#[derive(Debug, Deserialize)]
struct Feature {
    id: String,
    #[serde(default)]
    area: String,
    #[serde(default)]
    advertised: bool,
}

#[derive(Debug)]
struct CatalogSource {
    path: PathBuf,
}

fn resolve_catalog_source(manifest_dir: &Path) -> Result<CatalogSource, Box<dyn Error>> {
    if let Ok(override_path) = env::var("FEATURES_TOML_OVERRIDE") {
        let override_path = PathBuf::from(override_path);
        if override_path.exists() {
            return Ok(CatalogSource { path: override_path });
        }
    }

    let local = manifest_dir.join("features.toml");
    if local.exists() {
        return Ok(CatalogSource { path: local });
    }

    let workspace = manifest_dir
        .parent()
        .and_then(Path::parent)
        .map(|parent| parent.join("features.toml"))
        .filter(|path| path.exists());
    if let Some(path) = workspace {
        return Ok(CatalogSource { path });
    }

    let vendored = manifest_dir.join("features_sot.toml");
    if vendored.exists() {
        return Ok(CatalogSource { path: vendored });
    }

    Err(format!("features catalog not found for manifest dir: {}", manifest_dir.display()).into())
}

fn load_catalog_for_build(
    manifest_dir: &Path,
) -> Result<(FeatureCatalog, CatalogSource), Box<dyn Error>> {
    let source = resolve_catalog_source(manifest_dir)?;
    let content = fs::read_to_string(&source.path)?;
    let catalog = toml::from_str(&content)?;
    Ok((catalog, source))
}

fn render_dap_feature_catalog_module(ids: &[String]) -> String {
    let mut sorted = ids.to_vec();
    sorted.sort_unstable();
    sorted.dedup();

    let mut code = String::new();
    code.push_str("// @generated by build.rs; DO NOT EDIT.\n\n");
    code.push_str("pub const ADVERTISED_DAP_FEATURES: &[&str] = &[\n");
    for id in &sorted {
        code.push_str(&format!("    {:?},\n", id));
    }
    code.push_str("];\n\n");
    code.push_str(
        "pub fn advertised_features() -> &'static [&'static str] { ADVERTISED_DAP_FEATURES }\n\n",
    );
    code.push_str(
        "pub fn has_feature(id: &str) -> bool { ADVERTISED_DAP_FEATURES.contains(&id) }\n",
    );
    code
}

fn render_dap_fallback_module(default_features: &[&str]) -> String {
    let features =
        default_features.iter().map(|feature| (*feature).to_string()).collect::<Vec<_>>();
    render_dap_feature_catalog_module(&features)
}

fn generate_catalog_module() -> Result<(), Box<dyn Error>> {
    let out_dir = std::env::var("OUT_DIR")?;
    let dest_path = Path::new(&out_dir).join("dap_feature_catalog.rs");

    println!("cargo:rerun-if-env-changed=FEATURES_TOML_OVERRIDE");
    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?;

    let code = match load_catalog_for_build(Path::new(&manifest_dir)) {
        Ok((catalog, source)) => {
            println!("cargo:rerun-if-changed={}", source.path.display());
            let mut source_features = catalog
                .feature
                .iter()
                .filter(|feature| feature.area == "debug" && feature.advertised)
                .map(|feature| feature.id.clone())
                .collect::<Vec<_>>();
            source_features.sort_unstable();
            source_features.dedup();
            render_dap_feature_catalog_module(&source_features)
        }
        Err(error) => {
            eprintln!("Warning: failed to load DAP feature catalog from features.toml: {error}");
            render_dap_fallback_module(DEFAULT_DAP_FEATURES)
        }
    };

    fs::write(dest_path, code)?;
    Ok(())
}

fn main() -> Result<(), Box<dyn Error>> {
    generate_catalog_module()
}