alef-core 0.8.3

Core types, config schema, and backend trait for the alef polyglot binding generator
Documentation
use super::extras::Language;
use super::output::{BuildCommandConfig, StringOrVec};
use super::tools::{LangContext, require_tool, wrap_command as wrap};

/// Return the default build configuration for a language.
///
/// The `output_dir` is the package directory where scaffolded files live
/// (e.g. `packages/python`). The `crate_name` is the name of the core crate
/// (e.g. `my-lib`). Both are substituted into command templates. `ctx`
/// provides tool selection and run_wrapper.
pub(crate) fn default_build_config(
    lang: Language,
    output_dir: &str,
    crate_name: &str,
    ctx: &LangContext,
) -> BuildCommandConfig {
    match lang {
        Language::Rust => BuildCommandConfig {
            precondition: Some(require_tool("cargo")),
            before: None,
            build: Some(StringOrVec::Single("cargo build --workspace".to_string())),
            build_release: Some(StringOrVec::Single("cargo build --release --workspace".to_string())),
        },
        Language::Python => BuildCommandConfig {
            precondition: Some(require_tool("maturin")),
            before: None,
            build: Some(StringOrVec::Single(format!(
                "maturin develop --manifest-path crates/{crate_name}-py/Cargo.toml"
            ))),
            build_release: Some(StringOrVec::Single(format!(
                "maturin develop --manifest-path crates/{crate_name}-py/Cargo.toml --release"
            ))),
        },
        Language::Node => BuildCommandConfig {
            precondition: Some(require_tool("napi")),
            before: None,
            build: Some(StringOrVec::Single(format!(
                "napi build --manifest-path crates/{crate_name}-node/Cargo.toml -o crates/{crate_name}-node"
            ))),
            build_release: Some(StringOrVec::Single(format!(
                "napi build --manifest-path crates/{crate_name}-node/Cargo.toml -o crates/{crate_name}-node --release"
            ))),
        },
        Language::Wasm => BuildCommandConfig {
            precondition: Some(require_tool("wasm-pack")),
            before: None,
            build: Some(StringOrVec::Single(format!(
                "wasm-pack build crates/{crate_name}-wasm --dev"
            ))),
            build_release: Some(StringOrVec::Single(format!(
                "wasm-pack build crates/{crate_name}-wasm --release"
            ))),
        },
        Language::Go => {
            let cmd = format!("cd {output_dir} && go build ./...");
            BuildCommandConfig {
                precondition: Some(require_tool("go")),
                before: None,
                build: Some(StringOrVec::Single(wrap(cmd.clone(), ctx.run_wrapper))),
                build_release: Some(StringOrVec::Single(wrap(cmd, ctx.run_wrapper))),
            }
        }
        Language::Ruby => BuildCommandConfig {
            precondition: Some(require_tool("cargo")),
            before: None,
            build: Some(StringOrVec::Single(format!("cargo build -p {crate_name}-rb"))),
            build_release: Some(StringOrVec::Single(format!("cargo build --release -p {crate_name}-rb"))),
        },
        Language::Php => BuildCommandConfig {
            precondition: Some(require_tool("cargo")),
            before: None,
            build: Some(StringOrVec::Single(format!("cargo build -p {crate_name}-php"))),
            build_release: Some(StringOrVec::Single(format!(
                "cargo build --release -p {crate_name}-php"
            ))),
        },
        Language::Ffi => BuildCommandConfig {
            precondition: Some(require_tool("cargo")),
            before: None,
            build: Some(StringOrVec::Single(format!("cargo build -p {crate_name}-ffi"))),
            build_release: Some(StringOrVec::Single(format!(
                "cargo build --release -p {crate_name}-ffi"
            ))),
        },
        Language::Java => {
            let (build_path, release_path) = if let Some(proj) = ctx.project_file {
                (
                    format!("mvn -f {proj} package -DskipTests -q"),
                    format!("mvn -f {proj} package -DskipTests -q"),
                )
            } else {
                (
                    format!("mvn -f {output_dir}/pom.xml package -DskipTests -q"),
                    format!("mvn -f {output_dir}/pom.xml package -DskipTests -q"),
                )
            };
            BuildCommandConfig {
                precondition: Some(require_tool("mvn")),
                before: None,
                build: Some(StringOrVec::Single(wrap(build_path, ctx.run_wrapper))),
                build_release: Some(StringOrVec::Single(wrap(release_path, ctx.run_wrapper))),
            }
        }
        Language::Csharp => {
            let (build_path, release_path) = if let Some(proj) = ctx.project_file {
                (
                    format!("dotnet build {proj} --configuration Debug -q"),
                    format!("dotnet build {proj} --configuration Release -q"),
                )
            } else {
                (
                    format!("dotnet build {output_dir} --configuration Debug -q"),
                    format!("dotnet build {output_dir} --configuration Release -q"),
                )
            };
            BuildCommandConfig {
                precondition: Some(require_tool("dotnet")),
                before: None,
                build: Some(StringOrVec::Single(wrap(build_path, ctx.run_wrapper))),
                build_release: Some(StringOrVec::Single(wrap(release_path, ctx.run_wrapper))),
            }
        }
        Language::Elixir => BuildCommandConfig {
            precondition: Some(require_tool("mix")),
            before: None,
            build: Some(StringOrVec::Single(format!("cd {output_dir} && mix compile"))),
            build_release: Some(StringOrVec::Single(format!("cd {output_dir} && mix compile"))),
        },
        Language::R => BuildCommandConfig {
            precondition: Some(require_tool("cargo")),
            before: None,
            build: Some(StringOrVec::Single(format!("cargo build -p {crate_name}-r"))),
            build_release: Some(StringOrVec::Single(format!("cargo build --release -p {crate_name}-r"))),
        },
    }
}

#[cfg(test)]
mod tests {
    use super::super::tools::ToolsConfig;
    use super::*;

    fn all_languages() -> Vec<Language> {
        vec![
            Language::Python,
            Language::Node,
            Language::Wasm,
            Language::Ruby,
            Language::Php,
            Language::Go,
            Language::Java,
            Language::Csharp,
            Language::Elixir,
            Language::R,
            Language::Ffi,
            Language::Rust,
        ]
    }

    fn cfg(lang: Language, dir: &str, crate_name: &str) -> BuildCommandConfig {
        let tools = ToolsConfig::default();
        let ctx = LangContext::default(&tools);
        default_build_config(lang, dir, crate_name, &ctx)
    }

    #[test]
    fn every_language_has_build_and_build_release() {
        for lang in all_languages() {
            let c = cfg(lang, "packages/test", "my-lib");
            assert!(c.build.is_some(), "{lang} should have a default build command");
            assert!(
                c.build_release.is_some(),
                "{lang} should have a default build_release command"
            );
        }
    }

    #[test]
    fn every_language_has_default_precondition() {
        for lang in all_languages() {
            let c = cfg(lang, "packages/test", "my-lib");
            let pre = c
                .precondition
                .unwrap_or_else(|| panic!("{lang} should have a precondition"));
            assert!(pre.starts_with("command -v "));
        }
    }

    #[test]
    fn rust_uses_cargo_build_workspace() {
        let c = cfg(Language::Rust, "packages/rust", "my-lib");
        let build = c.build.unwrap().commands().join(" ");
        let release = c.build_release.unwrap().commands().join(" ");
        assert!(build.contains("cargo build --workspace"));
        assert!(release.contains("cargo build --release --workspace"));
    }

    #[test]
    fn python_uses_maturin_develop() {
        let c = cfg(Language::Python, "packages/python", "my-lib");
        let build = c.build.unwrap().commands().join(" ");
        let release = c.build_release.unwrap().commands().join(" ");
        assert!(build.contains("maturin develop"));
        assert!(build.contains("my-lib-py"));
        assert!(release.contains("--release"));
    }

    #[test]
    fn node_uses_napi_build() {
        let c = cfg(Language::Node, "packages/node", "my-lib");
        let build = c.build.unwrap().commands().join(" ");
        let release = c.build_release.unwrap().commands().join(" ");
        assert!(build.contains("napi build"));
        assert!(build.contains("my-lib-node"));
        assert!(release.contains("--release"));
    }

    #[test]
    fn wasm_uses_wasm_pack() {
        let c = cfg(Language::Wasm, "packages/wasm", "my-lib");
        let build = c.build.unwrap().commands().join(" ");
        let release = c.build_release.unwrap().commands().join(" ");
        assert!(build.contains("wasm-pack build"));
        assert!(build.contains("my-lib-wasm"));
        assert!(build.contains("--dev"));
        assert!(release.contains("--release"));
    }

    #[test]
    fn ffi_uses_cargo_build_p() {
        let c = cfg(Language::Ffi, "packages/ffi", "my-lib");
        let build = c.build.unwrap().commands().join(" ");
        let release = c.build_release.unwrap().commands().join(" ");
        assert!(build.contains("cargo build -p my-lib-ffi"));
        assert!(release.contains("cargo build --release -p my-lib-ffi"));
    }

    #[test]
    fn ruby_uses_cargo_build_rb() {
        let c = cfg(Language::Ruby, "packages/ruby", "my-lib");
        let build = c.build.unwrap().commands().join(" ");
        assert!(build.contains("cargo build -p my-lib-rb"));
    }

    #[test]
    fn php_uses_cargo_build_php() {
        let c = cfg(Language::Php, "packages/php", "my-lib");
        let build = c.build.unwrap().commands().join(" ");
        assert!(build.contains("cargo build -p my-lib-php"));
    }

    #[test]
    fn r_uses_cargo_build_r() {
        let c = cfg(Language::R, "packages/r", "my-lib");
        let build = c.build.unwrap().commands().join(" ");
        assert!(build.contains("cargo build -p my-lib-r"));
    }

    #[test]
    fn java_uses_maven_package() {
        let c = cfg(Language::Java, "packages/java", "my-lib");
        let build = c.build.unwrap().commands().join(" ");
        assert!(build.contains("mvn"));
        assert!(build.contains("package"));
        assert!(build.contains("-DskipTests"));
    }

    #[test]
    fn csharp_uses_dotnet_build_configurations() {
        let c = cfg(Language::Csharp, "packages/csharp", "my-lib");
        let build = c.build.unwrap().commands().join(" ");
        let release = c.build_release.unwrap().commands().join(" ");
        assert!(build.contains("dotnet build"));
        assert!(build.contains("--configuration Debug"));
        assert!(release.contains("--configuration Release"));
    }

    #[test]
    fn elixir_uses_mix_compile() {
        let c = cfg(Language::Elixir, "packages/elixir", "my-lib");
        let build = c.build.unwrap().commands().join(" ");
        assert!(build.contains("mix compile"));
    }

    #[test]
    fn crate_name_substituted_in_commands() {
        let c = cfg(Language::Python, "packages/python", "custom-crate");
        let build = c.build.unwrap().commands().join(" ");
        assert!(build.contains("custom-crate-py"));
    }

    #[test]
    fn output_dir_substituted_in_go_commands() {
        let c = cfg(Language::Go, "my/custom/path", "my-lib");
        let build = c.build.unwrap().commands().join(" ");
        assert!(build.contains("my/custom/path"));
    }
}